diff --git a/README.md b/README.md index 0fe209b..0841875 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,20 @@ public function getSlugOptions() : SlugOptions } ``` +### Reserving slugs + +If you have words that you wish to avoid having as slugs you can reserve them. Common when preventing collission with existing routes (eg. `admin`, `api` etc). + +```php +public function getSlugOptions() : SlugOptions +{ + return SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug') + ->slugsShouldNotEqual(['admin', 'api']); +} +``` + ### Integration with laravel-translatable You can use this package along with [laravel-translatable](https://github.com/spatie/laravel-translatable) to generate a slug for each locale. Instead of using the `HasSlug` trait, you must use the `HasTranslatableSlug` trait, and add the name of the slug field to the `$translatable` array. For slugs that are generated from a single field _or_ multiple fields, you don't have to change anything else. diff --git a/src/HasSlug.php b/src/HasSlug.php index 137525b..3c43a53 100644 --- a/src/HasSlug.php +++ b/src/HasSlug.php @@ -78,6 +78,10 @@ protected function addSlug(): void $slug = $this->generateNonUniqueSlug(); + if (!empty($this->slugOptions->reservedSlugs)) { + $slug = $this->ensureSlugIsNotReserved($slug, $this->slugOptions->reservedSlugs); + } + if ($this->slugOptions->generateUniqueSlugs) { $slug = $this->makeSlugUnique($slug); } @@ -125,6 +129,18 @@ protected function getSlugSourceStringFromCallable(): string return call_user_func($this->slugOptions->generateSlugFrom, $this); } + protected function ensureSlugIsNotReserved(string $slug, array $reservedSlugs): string + { + $originalSlug = $slug; + $i = $this->slugOptions->startSlugSuffixFrom; + + while (in_array($slug, $reservedSlugs)) { + $slug = $originalSlug.$this->slugOptions->slugSeparator.$i++; + } + + return $slug; + } + protected function makeSlugUnique(string $slug): string { $originalSlug = $slug; diff --git a/src/HasTranslatableSlug.php b/src/HasTranslatableSlug.php index 2c1cdab..b0e3e27 100644 --- a/src/HasTranslatableSlug.php +++ b/src/HasTranslatableSlug.php @@ -37,6 +37,14 @@ protected function addSlug(): void $this->withLocale($locale, function () use ($locale) { $slug = $this->generateNonUniqueSlug(); + if (!empty($this->slugOptions->reservedSlugs)) { + $slug = $this->ensureSlugIsNotReserved($slug, $this->slugOptions->reservedSlugs); + } + + if (array_key_exists($locale, $this->slugOptions->reservedSlugsForLocales) && !empty($this->slugOptions->reservedSlugsForLocales[$locale])) { + $slug = $this->ensureSlugIsNotReserved($slug, $this->slugOptions->reservedSlugsForLocales[$locale]); + } + $slugField = $this->slugOptions->slugField; if ($this->slugOptions->generateUniqueSlugs) { diff --git a/src/SlugOptions.php b/src/SlugOptions.php index 8cb7b65..ea33122 100644 --- a/src/SlugOptions.php +++ b/src/SlugOptions.php @@ -32,6 +32,10 @@ class SlugOptions public int $startSlugSuffixFrom = 1; + public array $reservedSlugs = []; + + public array $reservedSlugsForLocales = []; + public static function create(): static { return new static(); @@ -133,4 +137,24 @@ public function startSlugSuffixFrom(int $startSlugSuffixFrom): self return $this; } + + public function slugsShouldNotEqual(string | array $slugs): self + { + if (is_string($slugs)) { + $slugs = [$slugs]; + } + $this->reservedSlugs = $slugs; + + return $this; + } + + public function slugsShouldNotEqualForLocale(string $locale, string | array $slugs): self + { + if (is_string($slugs)) { + $slugs = [$slugs]; + } + $this->reservedSlugsForLocales[$locale] = $slugs; + + return $this; + } } diff --git a/tests/HasSlugTest.php b/tests/HasSlugTest.php index 593dd85..bb4102c 100644 --- a/tests/HasSlugTest.php +++ b/tests/HasSlugTest.php @@ -349,3 +349,36 @@ public function getSlugOptions(): SlugOptions expect($savedModel->id)->toEqual($model->id); }); + +it('can reserve a slug', function () { + $model = new class () extends TestModel { + public function getSlugOptions(): SlugOptions + { + return parent::getSlugOptions()->slugsShouldNotEqual('reserved'); + } + }; + + $model->name = 'reserved'; + $model->save(); + + expect($model->url)->toEqual('reserved-1'); +}); + +it('can reserve multiple slugs', function () { + $model = new class () extends TestModel { + public function getSlugOptions(): SlugOptions + { + return parent::getSlugOptions()->slugsShouldNotEqual(['reserved', 'admin']); + } + }; + + $model->name = 'reserved'; + $model->save(); + + expect($model->url)->toEqual('reserved-1'); + + $model->name = 'admin'; + $model->save(); + + expect($model->url)->toEqual('admin-1'); +}); diff --git a/tests/HasTranslatableSlugTest.php b/tests/HasTranslatableSlugTest.php index a2c0df2..454e6c3 100644 --- a/tests/HasTranslatableSlugTest.php +++ b/tests/HasTranslatableSlugTest.php @@ -161,6 +161,25 @@ expect($this->testModel->getTranslation('slug', 'nl'))->toBe('name-nl-2'); }); +it('can reserve slugs for a certain locale', function () { + $this->testModel->useSlugOptions( + SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug') + ->slugsShouldNotEqual('reserved') + ->slugsShouldNotEqualForLocale('nl', 'gereserveerd') + ); + + $this->testModel->setTranslation('name', 'en', 'reserved'); + $this->testModel->setTranslation('name', 'nl', 'gereserveerd'); + $this->testModel->setTranslation('name', 'sv', 'gereserveerd'); + $this->testModel->save(); + + expect($this->testModel->getTranslation('slug', 'en'))->toBe('reserved-1'); + expect($this->testModel->getTranslation('slug', 'nl'))->toBe('gereserveerd-1'); + expect($this->testModel->getTranslation('slug', 'sv'))->toBe('gereserveerd'); +}); + it('can handle overwrites when creating a model', function () { $this->testModel->setTranslation('name', 'en', 'Test value EN'); $this->testModel->setTranslation('name', 'nl', 'Test value NL');