A CakePHP plugin that ensures user-chosen slugs are safe and conflict-free for use in URLs.
| CakePHP | PHP | Plugin | Branch |
|---|---|---|---|
| 5.x | >= 8.2 | 5.x | cake5 |
- SlugValidator validates that a string is well-formed for use as a subdomain label or URL path segment (lowercase alphanumeric + hyphens, length constraints).
- IsNotReservedSlug compares slugs against a reserved-word list so they never collide with system routes or well-known paths.
- IsNotRouteConflict compares slugs against first-level URL segments extracted from the CakePHP route table to prevent collisions with application routes.
- A CakePHP 5.x application
- PHP >= 8.2
- A database connection configured in your app (
config/app_local.php)
-
Install the plugin via Composer:
composer require elstc/cakephp-slug-guard
-
Load the plugin. This adds
$this->addPlugin('Elastic/SlugGuard')to yoursrc/Application.php:bin/cake plugin load Elastic/SlugGuard
-
Run the migration to create the
reserved_slugstable:bin/cake migrations migrate --plugin Elastic/SlugGuard
-
Import the default reserved slugs (~710 common reserved words):
bin/cake slug_guard sync
Run the following command to confirm the reserved slugs were imported:
bin/cake slug_guard list --countExpected: a count of approximately 710 reserved slugs.
Add the IsNotReservedSlug rule to your table's buildRules() method:
use Elastic\SlugGuard\Model\Rule\IsNotReservedSlug;
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add(new IsNotReservedSlug('slug'), 'reservedSlug', [
'errorField' => 'slug',
'message' => 'This slug is reserved.',
]);
return $rules;
}$rules->add(new IsNotReservedSlug('username'), 'reservedSlug', [
'errorField' => 'username',
'message' => 'This username is reserved.',
]);You can use any table as the reserved slugs backend by implementing SlugExistenceInterface:
// src/Model/Table/CustomReservedSlugsTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
use Elastic\SlugGuard\Model\Table\SlugExistenceInterface;
class CustomReservedSlugsTable extends Table implements SlugExistenceInterface
{
public function slugExists(string $slug): bool
{
return $this->exists(['slug' => $slug]);
}
}Then pass the table name to IsNotReservedSlug:
$rules->add(new IsNotReservedSlug('slug', 'CustomReservedSlugs'), 'reservedSlug', [
'errorField' => 'slug',
'message' => 'This slug is reserved.',
]);Add the IsNotRouteConflict rule to prevent slugs from colliding with application routes (e.g. /admin, /api, /posts):
use Elastic\SlugGuard\Model\Rule\IsNotRouteConflict;
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add(new IsNotRouteConflict('slug'), 'routeConflict', [
'errorField' => 'slug',
'message' => 'This slug conflicts with an application route.',
]);
return $rules;
}This rule extracts first-level URL path segments from all registered CakePHP routes at runtime and rejects any slug that matches.
The SlugValidator class provides a static method for validating slug format (lowercase alphanumeric and hyphens):
use Elastic\SlugGuard\Validation\SlugValidator;
// Use as a Validator provider
$validator->setProvider('slugValidator', SlugValidator::class);
$validator->add('slug', 'validSlug', [
'rule' => ['isValid'],
'provider' => 'slugValidator',
'message' => 'Only lowercase letters, numbers, and hyphens are allowed.',
]);Minimum slug length. Default: 4
Maximum slug length. Default: 24
$validator->add('slug', 'validSlug', [
'rule' => ['isValid', 3, 32],
'provider' => 'slugValidator',
]);bin/cake slug_guard list
bin/cake slug_guard list --count
bin/cake slug_guard list --search adminbin/cake slug_guard routes
bin/cake slug_guard routes --countOutput is pipe-friendly (one path per line, no decoration). See Route-based workflow below.
bin/cake slug_guard add my-reserved-slug
bin/cake slug_guard add slug-one slug-two slug-threeSupports stdin for pipe usage:
bin/cake slug_guard routes | bin/cake slug_guard addbin/cake slug_guard remove my-reserved-slug
bin/cake slug_guard remove slug-one slug-two slug-threeAlso supports stdin:
cat slugs-to-remove.txt | bin/cake slug_guard removebin/cake slug_guard import /path/to/slugs.txtFile format: one slug per line, # for comments, empty lines are ignored.
# Sync (auto-detects app config file or falls back to plugin built-in)
bin/cake slug_guard sync
# Sync with a specific file
bin/cake slug_guard sync --file /path/to/slugs.txt
# Preview changes without applying
bin/cake slug_guard sync --dry-runWhen --file is not specified, the sync command resolves the seed file in the following order:
- Application config file:
config/reserved-slugs.txt(in app root) - Plugin built-in seed file
You can override the application config file path via Configure:
// In config/app.php or config/app_local.php
'SlugGuard' => [
'syncFile' => CONFIG . 'my-custom-slugs.txt',
],The plugin ships with a default reserved slugs list at config/reserved-slugs.txt. This file contains ~710 common reserved words (e.g. admin, api, login, settings, www) that could conflict with system routes or well-known paths.
- One slug per line
- Lines starting with
#are comments - Empty lines are ignored
# System routes
admin
api
login
# Social media
facebook
twitter
youtube
The built-in list is used as a fallback by the sync command when no app-level config file exists. You can also import it directly:
bin/cake slug_guard import vendor/elstc/cakephp-slug-guard/config/reserved-slugs.txtCreate your own file following the same format and use it with import or sync:
# Import additional slugs from a custom file
bin/cake slug_guard import /path/to/my-slugs.txt
# Sync the database to match your custom file exactly
bin/cake slug_guard sync --file /path/to/my-slugs.txtNote:
importadds slugs from the file to the database (existing slugs are preserved).syncmakes the database match the file exactly — slugs not in the file will be removed.
The slug_guard routes command extracts first-level URL path segments from your application's route table. Use it to automatically reserve paths that would conflict with user-generated slugs.
bin/cake slug_guard routes | bin/cake slug_guard addbin/cake slug_guard routes >> config/reserved-slugs.txt
bin/cake slug_guard sync