Table of contents

What we want to achieve?

Let’s say you want to create or save an Article. That Article at some point, most likely, will have a slug if it’s meant for the public. You’ll want that slug to be unique.

I like the approach WordPress has. When you create an article and already there is one with the same slug, it will just append a number at the end of the slug.
Example: your-slug-that-is-used-1, your-slug-that-is-used-2, and so on.

So we’re trying to replicate that in Laravel using a Trait that can be applied to any Model.

Create a function to generate unique slugs

The most common approach is to have a save() method in the ArticleController where you validate some fields, slug included, and save it.

Your ArticleController might look like this. We generating the slug based on the title.

public function save(Request $request) {
    $validated = $this->validateFields($request);
    $article = Article::create($validated);

    return redirect('/dashboard/articles/'.$article->id.'/edit')->with('success', 'Article was successfully saved');
}

public function validateFields(Request $request) {
    $validated = $request->validate([
        'title' => 'required',
        'content' => 'required',
        'seo_description' => '',
        'status' => '',
        'user_id' => '',
    ]);

    $user_id = Auth::id(); 
    $validated['user_id'] = $user_id;

    $slug = Str::slug($request['title'], '-');
    $validated['slug'] = $slug;
    
    return $validated;
}

At this point were not checking if the slug is unique, we just create the slug based on the title provided in the Article creation form.

We’ll create a function called uniqueSlug which will make sure that the Article has a unique slug, if not it will append a number to it.

So if you already have an article with the slug “my-amazing-article”, it will check and create “my-amazing-article-1”. If you have “my-amazing-article-1” it will create “my-amazing-article-2” and so on.

public function uniqueSlug($string) {
    $baseSlug = Str::slug($string, '-');

    if(Article::where('slug', $baseSlug)->count() == 0) {
        return $baseSlug;
    }

    for($i = 1; $i > 0; $i++) {
        $slug = $baseSlug . '-' . $i;

        if (Article::where('slug', $slug)->count() == 0)
            return $slug;
    }

    return $slug;
}

Now in the validateFields function, we can use the uniqueSlug to generate the slug.

public function validateFields(Request $request) {
    ...

    $slug = $this->uniqueSlug($request['title']);

    ... 
    
    return $validated;
}

Implement it as a Trait

The example from above is ok and it works well for the Article, but what happens if you also have other Models that need unique slugs?

It’s not very pleasant to copy-paste this method in all the Controllers. You could create a utility class, call it in the Controller and use that, but we can use a Trait.

We’ll create a new Trait called SlugTrait.php in app/Models/Traits that will look like this

<?php

namespace App\Traits;

use Illuminate\Support\Str;

trait SlugTrait {
    public static function uniqueSlug($string) {
        $baseSlug = Str::slug($string, '-');

        if(static::where('slug', $baseSlug)->count() == 0) {
            return $baseSlug;
        }

        for($i = 1; $i > 0; $i++) {
            $slug = $baseSlug . '-' . $i;

			if (static::where('slug', $slug)->count() == 0)
				return $slug;
        }

        return $slug;
    }
}

As you can see is the same function we used in the Controller above, but we replace the Model with static that will help reference the Model where it’s used.

Now in the Article Model we add the SlugTrait

<?php

namespace App\Models;

use App\Traits\SlugTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory;
    use SlugTrait;

Now in the ArticleController we adjust the reference in the validateFields function to use the Article Trait that we just created.

public function validateFields(Request $request) {

    ... 

    $slug = Article::uniqueSlug($request['title']);

    ...
    
    return $validated;
}

And you’re done. You’re now using a new Trait to generate unique slugs.

You can go and add that Trait to other Models and call it inside the Controller where you need it.

public function validateFields(Request $request) {

    ... 

    $slug = Product::uniqueSlug($request['title']);

    ...
    
    return $validated;
}

Conclusion

Here you have a method to create a unique slug from the title.

You might tweak the above example based on your needs, let’s say you allow to manually input a slug. You’ll have to do a check and see if the request for slug is empty or not and based on that condition decide how to use the uniqueSlug trait.

I have something like this, where I don’t bother checking if the manually inputted slug has a duplicate.

if(empty($request->filled('slug'))) {
    $slug = Article::uniqueSlug($request['title']);
    $validated['slug'] = $slug;
}