A CakePHP behavior to automatically create and store slugs.
- Input data can consist of one or many fields
- Slugs can be unique and persistent, ideal for lookups by slug
- Multibyte aware, umlauts etc will be properly replaced
| Key | Default | Description |
|---|---|---|
| label | `null` |
|
| field | `'slug'` | The slug field name |
| overwriteField | 'overwrite_slug' | The boolean field/property to trigger overwriting if "overwrite" is false |
| mode | `'url'` |
|
| separator | `-` | The separator to use |
| length | `null` | Set to 0 for no length. Will be auto-detected if possible via schema. |
| overwrite | `false` |
has the following values
|
| unique | `false` |
has the following values
|
| uniqueCallback | `null` | A closure to customize the uniqueness check. Receives `(Table $table, array $conditions)` and must return `bool`. Useful when other behaviors modify queries (e.g., multi-tenant scoping) and you need to temporarily disable them during the uniqueness check. |
| case | `null` |
has the following values
|
| replace | see code | Custom replacements as array. `Set to null` to disable. |
| on | `'beforeRules'` | `beforeSave` or `beforeMarshal` or `beforeRules`. |
| scope | `[]` | Conditions to use as scope for uniqueness check. Can be an array or a Closure receiving the entity for dynamic scoping. |
| tidy | `true` | If cleanup should be run on slugging. |
| onDirty | `false` | If true, regenerate slug when label field(s) are dirty, even if `overwrite` is false. Useful for "update slug only when title changes" behavior. |
Attach it to your models in initialize() like so:
$this->addBehavior('Tools.Slugged');We want to store categories and we need a slug for nice SEO URLs like /category/[slugname]/.
$this->addBehavior('Tools.Slugged',
['label' => 'name', 'unique' => true, 'mode' => 'ascii']
);Upon creating and storing a new record it will look for content in "name" and create a slug in "slug" field.
With the above config on "edit" the slug will not be modified if you alter the name. That is important to know. You cannot just change the slug, as the URL is most likely indexed by search engines now.
If you want to do that, you would also need a .htaccess rewrite rule to 301 redirect from the old to the new slug. So if that is the case, you could add an "overwrite field" to your form.
echo $this->Form->field('overwrite_slug', ['type' => 'checkbox']);Once that boolean checkbox is clicked it will then perform the slug update on save.
If we just append the slug to the URL, such as /category/123-[slugname], then we don't need to persist the slug.
$this->addBehavior('Tools.Slugged',
['label' => 'name', 'overwrite' => true, 'mode' => 'ascii', 'unique' => true]
);Note that we don't need "unique" either then.
Each save now re-triggers the slug generation.
You can pass your own callable for slugging into the mode config.
And you can even use a static method on any class this way (given it has a static slug() method):
$this->addBehavior('Tools.Slugged', ['mode' => [MySlugger::class, 'slug']]);
Tip: Use 'mode' => [Text::class, 'slug'] if you want to avoid using the deprecated Inflector::slug() method.
Don't forget the use statement at the top of the file, though (use Tools\Utility\Text;).
If you quickly want to find a record by its slug, use:
->find()->find('slugged', slug: $slug)->firstOrFail();For multi-tenant or multi-site applications, you can use a Closure to scope uniqueness checks based on entity data:
$this->addBehavior('Tools.Slugged', [
'unique' => true,
'scope' => function ($entity) {
return ['site_id' => $entity->get('site_id')];
},
]);This ensures slugs are unique per site, allowing the same slug in different sites.
If you have other behaviors that modify queries (e.g., multi-tenant scoping), you may need to
temporarily disable them during the uniqueness check. Use the uniqueCallback option:
$this->addBehavior('Tools.Slugged', [
'unique' => true,
'uniqueCallback' => function (Table $table, array $conditions): bool {
// Temporarily disable a scoping behavior
$table->behaviors()->unload('TenantScope');
$exists = $table->exists($conditions);
$table->behaviors()->load('TenantScope');
return $exists;
},
]);If you want the slug to update only when the title/label field changes (but not on every save),
use the onDirty option:
$this->addBehavior('Tools.Slugged', [
'onDirty' => true,
]);With this configuration:
- New records always get a slug generated
- Existing records only get their slug updated when the label field is dirty
- Updates to other fields won't affect the slug