Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to generate custom suffixes #289

Merged
merged 4 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ We highly appreciate you sending us a postcard from your hometown, mentioning wh
## Installation

You can install the package via composer:
``` bash

```bash
composer require spatie/laravel-sluggable
```

Expand Down Expand Up @@ -281,7 +282,6 @@ public function getSlugOptions() : SlugOptions

### Using scopes


If you have a global scope that should be taken into account, you can define this as well with `extraScope`. For example if you have a pages table containing pages of multiple websites and every website has it's own unique slugs.

```php
Expand All @@ -308,6 +308,38 @@ public function getSlugOptions() : SlugOptions
}
```

### Generating slug suffix on first occurrence
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a description of the functionality, could you also add a simple example on how such a slug would look like. That would really drive the point home.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Feel free to suggest any other change or improvement!


By default, the first occurence of a slug will not have a suffix. You can force the first occurence to also have a suffix so, even if the slug is unique as it is, it will be suffixed.

```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->useSuffixOnFirstOccurrence();
}
```

### Generating a custom slug suffix

By default, the mechanism to make slugs unique is to append an autoincremental value to the slug. You can generate a custom slug suffix such as a random string or hash with `usingSuffixGenerator`.

It accepts a callable that receives the base slug (without any suffix) and the iteration number, which represents how many times the suffix generation process has been run to ensure uniqueness. This number could be useful to monitor the collision rate of the generation process.

```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->usingSuffixGenerator(
fn(string $slug, int $iteration) => bin2hex(random_bytes(4))
); // Sample dummy method to generate a random hex code of length 8
}
```

### 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.
Expand Down Expand Up @@ -419,7 +451,6 @@ $model = Article::findBySlug('my-article');

`findBySlug` also accepts a second parameter `$columns` just like the default Eloquent `find` method.


## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
Expand Down
20 changes: 17 additions & 3 deletions src/HasSlug.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,29 @@ protected function getSlugSourceStringFromCallable(): string
protected function makeSlugUnique(string $slug): string
{
$originalSlug = $slug;
$i = $this->slugOptions->startSlugSuffixFrom;
$iteration = 0;

while ($this->otherRecordExistsWithSlug($slug) || $slug === '') {
$slug = $originalSlug.$this->slugOptions->slugSeparator.$i++;
while (
$slug === '' ||
$this->otherRecordExistsWithSlug($slug) ||
($this->slugOptions->useSuffixOnFirstOccurrence && $iteration === 0)
) {
$suffix = $this->generateSuffix($originalSlug, $iteration++);
$slug = $originalSlug . $this->slugOptions->slugSeparator . $suffix;
}

return $slug;
}

protected function generateSuffix(string $originalSlug, int $iteration): string
{
if ($this->slugOptions->suffixGenerator) {
return call_user_func($this->slugOptions->suffixGenerator, $originalSlug, $iteration);
}

return strval($this->slugOptions->startSlugSuffixFrom + $iteration);
}

protected function otherRecordExistsWithSlug(string $slug): bool
{
$query = static::where($this->slugOptions->slugField, $slug)
Expand Down
23 changes: 23 additions & 0 deletions src/SlugOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class SlugOptions
/** @var callable */
public $extraScopeCallback;

/** @var (callable(string $slug, int $iteration): string)|null */
public $suffixGenerator;

public string $slugField;

public bool $generateUniqueSlugs = true;
Expand All @@ -32,6 +35,8 @@ class SlugOptions

public int $startSlugSuffixFrom = 1;

public bool $useSuffixOnFirstOccurrence = false;

public static function create(): static
{
return new static();
Expand Down Expand Up @@ -133,4 +138,22 @@ public function startSlugSuffixFrom(int $startSlugSuffixFrom): self

return $this;
}

public function useSuffixOnFirstOccurrence(): self
{
$this->useSuffixOnFirstOccurrence = true;

return $this;
}


/**
* @param callable(string $slug, int $iteration): string $generator
*/
public function usingSuffixGenerator(callable $generator): self
{
$this->suffixGenerator = $generator;

return $this;
}
}
34 changes: 34 additions & 0 deletions tests/HasSlugTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,40 @@ public function getSlugOptions(): SlugOptions
expect($replica->url)->toEqual('this-is-a-test-2');
});

it('can generate slug suffix on first occurrence', function () {
$model = new class () extends TestModel {
public function getSlugOptions(): SlugOptions
{
return parent::getSlugOptions()->useSuffixOnFirstOccurrence();
}
};

$model->name = 'this is a test';
$model->save();

expect($model->url)->toEqual('this-is-a-test-1');
});

it('can generate a custom slug suffix using a callable', function () {
$model = new class () extends TestModel {
public function getSlugOptions(): SlugOptions
{
return parent::getSlugOptions()->usingSuffixGenerator(
fn(string $slug, int $iteration) => 'random-with-access-base-slug-(' . $slug[0] . '_' . $iteration . ')'
);
}
};

$model->name = 'this is a test';
$model->save();

$replica = $model->replicate();
$replica->save();

expect($model->url)->toEqual('this-is-a-test');
expect($replica->url)->toEqual('this-is-a-test-random-with-access-base-slug-(t_0)');
});

it('can find models using findBySlug alias', function () {
$model = new class () extends TestModel {
public function getSlugOptions(): SlugOptions
Expand Down