Skip to content

Sluggable Behavior

The sluggable behavior allows a model to offer a human-readable identifier that can be used for search-engine-friendly URLs.

In schema.xml, use the <behavior> tag to add the sluggable behavior to a table:

<table name="post">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" primaryString="true" />
<behavior name="sluggable" />
</table>

Rebuild your model, run the table creation SQL again, and you’re ready to go. The model now has an additional getter for its slug, which is automatically set before the object is saved:

$p1 = new Post();
$p1->setTitle('Hello, World!');
$p1->save();
echo $p1->getSlug(); // 'hello-world'

By default, the behavior uses the string representation of the object to build the slug. In the example above, the title column is defined as primaryString, so the slug uses this column as its base string. The string is then cleaned up so it can appear in a URL — blanks and special characters are replaced by a dash, and the string is lowercased.

The generated model query offers a findOneBySlug() method to easily retrieve a model object based on its slug:

$p = PostQuery::create()->findOneBySlug('hello-world');

By default, the behavior adds one column to the model. If this column is already described in the schema, the behavior detects it and doesn’t add it a second time. The behavior parameters let you use custom patterns for the slug composition. The following schema illustrates a complete customization of the behavior:

<table name="post">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" primaryString="true" />
<column name="url" type="varchar" size="100" />
<behavior name="sluggable">
<parameter name="slug_column" value="url" />
<parameter name="slug_pattern" value="/posts/{Title}" />
<parameter name="replace_pattern" value="/[^\w\/]+/u" />
<parameter name="replacement" value="-" />
<parameter name="separator" value="/" />
<parameter name="permanent" value="true" />
</behavior>
</table>

Whatever slug_column name you choose, the sluggable behavior always adds the following proxy methods, mapped to the correct column:

$post->getSlug(); // returns $post->url
$post->setSlug($slug); // $post->url = $slug

The slug_pattern parameter is the rule used to build the raw slug based on the object’s properties. Any substring enclosed in brackets ({}) is turned into a getter call, so the Post class generates slugs as follows:

protected function createRawSlug(): string
{
return '/posts/' . $this->cleanupSlugPart($this->getTitle());
}

That means you can use names that don’t match a real column’s phpName, as long as your model provides a getter for it.

The replace_pattern parameter is a regular expression matching all the characters that will be replaced by the replacement parameter. In the example above, special characters like ! or : are replaced by -, but not letters, digits, or /.

The separator parameter is the character that separates the slug from the incremental index added in case of non-uniqueness. Set to /, it makes Post objects sharing the same title produce slugs like:

'/posts/hello-world'
'/posts/hello-world/1'
'/posts/hello-world/2'
...

A permanent slug is not automatically updated when the fields that constitute it change. This is useful when the slug serves as a permalink that should keep working even when the model object’s properties change. You can still manually change the slug on a permanent model by calling setSlug().

The slug is generated by the object when it’s saved, via the createSlug() method, which does several operations on a simple string:

protected function createSlug(): string
{
// create the slug based on the `slug_pattern` and the object properties
$slug = $this->createRawSlug();
// truncate the slug to accommodate the size of the slug column
$slug = $this->limitSlugSize($slug);
// add an incremental index to make sure the slug is unique
$slug = $this->makeSlugUnique($slug);
return $slug;
}
protected function createRawSlug(): string
{
// here comes the string composition code, generated according to `slug_pattern`
$slug = 'posts/' . $this->cleanupSlugPart($this->getTitle());
// cleanupSlugPart() cleans up the slug part
// based on the `replace_pattern` and `replacement` parameters
return $slug;
}

You can override any of these methods in your model class to implement custom slug logic.