Skip to content

Instantly share code, notes, and snippets.

@ericlbarnes
Created June 11, 2016 15:49
Show Gist options
  • Save ericlbarnes/3b5d3c49482f2a190619699de660ee9f to your computer and use it in GitHub Desktop.
Save ericlbarnes/3b5d3c49482f2a190619699de660ee9f to your computer and use it in GitHub Desktop.
<?php
namespace App\Services;
use App\Post;
class Slug
{
/**
* @param $title
* @param int $id
* @return string
* @throws \Exception
*/
public function createSlug($title, $id = 0)
{
// Normalize the title
$slug = str_slug($title);
// Get any that could possibly be related.
// This cuts the queries down by doing it once.
$allSlugs = $this->getRelatedSlugs($slug, $id);
// If we haven't used it before then we are all good.
if (! $allSlugs->contains('slug', $slug)){
return $slug;
}
// Just append numbers like a savage until we find not used.
for ($i = 1; $i <= 10; $i++) {
$newSlug = $slug.'-'.$i;
if (! $allSlugs->contains('slug', $newSlug)) {
return $newSlug;
}
}
throw new \Exception('Can not create a unique slug');
}
protected function getRelatedSlugs($slug, $id = 0)
{
return Post::select('slug')->where('slug', 'like', $slug.'%')
->where('id', '<>', $id)
->get();
}
}
@AucT
Copy link

AucT commented Jun 12, 2016

you can make more reusable by adding entity. So you can use it not only for posts but for other entity.
here simple example

<?php
namespace App\Services;

class Slug
{
    /**
     * @param $title
     * @param int $id
     * @return string
     * @throws \Exception
     */
    private $entity;

    public function __construct($entity = \App\Post::class)
    {
        $this->entity = $entity;
    }
    public function createSlug($title, $id = 0)
    {
        // Normalize the title
        $slug = str_slug($title);
        // Get any that could possibly be related.
        // This cuts the queries down by doing it once.
        $allSlugs = $this->getRelatedSlugs($slug, $id);
        // If we haven't used it before then we are all good.
        if (!$allSlugs->contains('slug', $slug)) {
            return $slug;
        }
        // Just append numbers like a savage until we find not used.
        for ($i = 1; $i <= 10; $i++) {
            $newSlug = $slug . '-' . $i;
            if (!$allSlugs->contains('slug', $newSlug)) {
                return $newSlug;
            }
        }
        throw new \Exception('Can not create a unique slug');
    }

    protected function getRelatedSlugs($slug, $id = 0)
    {
        return call_user_func(array($this->entity, 'select'), 'slug')->where('slug', 'like', $slug . '%')
            ->where('id', '<>', $id)
            ->get();
    }
}

@quangmach
Copy link

@AucT Hi AucT, what is the function & variable to call? Example please.
Thank you.

@jstoone
Copy link

jstoone commented Jun 13, 2016

I found this little snippet in my old project:

public function setSlugAttribute($value)
{
    $slug = str_slug($value);
    // Look for exisiting slugs
    $existingSlugs = static::whereRaw("slug REGEXP '^{$slug}(-[0-9]*)?$'");

    // If no matching slugs were found, return early
    if ($existingSlugs->count() === 0)
        return $slug;

    // Get slugs in reversed order, and pick the first
    $lastSlugNumber = intval(str_replace($slug . '-', '', $existingSlugs->orderBy('slug', 'desc')->first()->slug));

    return $slug . '-' . ($lastSlugNumber + 1);
}

@AucT
Copy link

AucT commented Jun 18, 2016

@quangmach

//invoking for default (e.g. \App\Post)
$slugLibrary = new \App\Services\Slug();
//invoking for custom entity
$slugLibrary = new \App\Services\Slug(\App\Person::class);

//usage
$slug = $slugLibrary->createSlug('George Lucas');

@umefarooq
Copy link

@ericbarnes and @AucT, a while loop will be much better than for, for loop has limit till 10 slug after that it will throw exceptions

@matthewsuan
Copy link

Helpful utility class. Also on @AucT's edit, added option to set the model slug attribute right in the construct function.

<?php

namespace App\Services;

class Slug
{
    private $entity;
    private $slugAttr;

    public function __construct($entity = \App\Post::class, $slugAttr = 'slug')
    {
        $this->entity = $entity;
        $this->slugAttr = $slugAttr;
    }

    /**
     * @param $title
     * @param int $id
     * @return string
     * @throws \Exception
     */
    public function createSlug($title, $id = 0)
    {
        // Normalize the title
        $slug = str_slug($title);
        // Get any that could possibly be related.
        // This cuts the queries down by doing it once.
        $allSlugs = $this->getRelatedSlugs($slug, $id);
        // If we haven't used it before then we are all good.
        if (!$allSlugs->contains($this->slugAttr, $slug)) {
            return $slug;
        }
        // Just append numbers like a savage until we find not used.
        for ($i = 1; $i <= 10; $i++) {
            $newSlug = $slug . '-' . $i;
            if (!$allSlugs->contains($this->slugAttr, $newSlug)) {
                return $newSlug;
            }
        }
        throw new \Exception('Can not create a unique slug');
    }

    protected function getRelatedSlugs($slug, $id = 0)
    {
        return call_user_func(array($this->entity, 'select'), $this->slugAttr)->where($this->slugAttr, 'like', $slug . '%')
            ->where('id', '<>', $id)
            ->get();
    }
}

@rzani
Copy link

rzani commented Jul 11, 2016

Hey folks, I kinda changed:

<?php

namespace App\Services;

/**
 * Slug Class
 */
class Slug
{

    public $entity;

    public $attribute;

    /**
     *
     */
    public function __construct($entity, $attribute = 'slug')
    {
        $this->entity = $entity;
        $this->attribute = $attribute;
    }

    /**
     * @param $title
     * @param int $id
     * @return string
     * @throws \Exception
     */
    public function create($title)
    {
        if (empty($title)) {
            throw new \Exception('Title is empty');
        }

        // Normalize the title
        $slug = str_slug($title);

        $slugStoraged = $this->getRelated($slug);

        if ($slugStoraged == null) {
           return $slug;
        }

        $lastNum = intval(str_replace($slug . '-', '', $slugStoraged->{$this->attribute}));

        if (is_numeric($lastNum)) {
           return  $slug . '-' . ++$lastNum;
        }
    }

    public function getRelated($slug)
    {
        return call_user_func([$this->entity, 'select'], $this->attribute)
            ->where($this->attribute, 'like', $slug . '%')
            ->orderBy($this->attribute, 'desc')
            ->first();
    }
}

@mbmohib
Copy link

mbmohib commented Mar 16, 2017

I'm new to laravel, I have created a file called Slug.php and put it into App\Providers folder, When I Call
$slug = new \App\Services\Slug(); in my PostController it throws "Class 'App\Services\Slug' not found" error! Where should I put that file ?

@yasser4ever1
Copy link

yasser4ever1 commented Feb 4, 2018

I need help because i am new to laravel too sorry my question might look silly but i need to understand .. Is this class is a service provider or a helper or what? and where should i locate this file i am confused a bit because i don't know about namespace App\Services is for service provider or something else

If it's a service provider then why it's not namespaced with namespace App\Providers and why it doesn't extend extends ServiceProvider in addition that class name should end with the word Provider

When i tried to use this class this error appeared
majaz org eshows admin shows

My code:

use App\Services\Slug;
$slugLibrary = new Slug(\App\Show::class);
$slug = $slugLibrary->createSlug($show->title);

@NewEXE
Copy link

NewEXE commented Dec 31, 2018

I'm write simple trait which you can use in Eloquent models for slug auto-generation.

<?php

namespace App\Models\Traits;

use Illuminate\Database\Eloquent\Model;

/**
 * Trait Sluggable.
 *
 * Sources:
 * @see https://github.com/martinbean/laravel-sluggable-trait/
 * @see https://gist.github.com/ericlbarnes/3b5d3c49482f2a190619699de660ee9f
 * @see https://interworks.com.mk/the-easiest-way-to-create-unique-slugs-for-blog-posts-in-laravel/
 */
trait Sluggable
{
    /**
     * Boot the sluggable trait for a model.
     *
     * @return void
     */
    public static function bootSluggable()
    {
        static::saving(function (Model $model) {
            if (empty($model->getSlug())) {
                $slug = self::generateUniqueSlug($model);

                $model->setSlug($slug);
            }
        });
    }

    /**
     * The name of the column to use for slugs.
     *
     * @return string
     */
    public function getSlugColumnName()
    {
        return 'slug';
    }

    /**
     * Get the string to create a slug from.
     *
     * @return string
     */
    public function getSluggableString()
    {
        return $this->getAttribute('name');
    }

    /**
     * Get the current slug value.
     *
     * @return string
     */
    public function getSlug()
    {
        return $this->getAttribute($this->getSlugColumnName());
    }

    /**
     * Set the slug to the given value.
     *
     * @param  string  $value
     * @return $this
     */
    public function setSlug($value)
    {
        $this->setAttribute($this->getSlugColumnName(), $value);

        return $this;
    }

    /**
     * @param Model $model
     * @return string
     * @throws \Exception
     */
    private static function generateUniqueSlug(Model $model): string
    {
        $slug = empty($model->getSlug()) ? trim(str_slug($model->getSluggableString())) : $model->getSlug();
        $attribute = trim($model->getSlugColumnName());

        if (empty($slug) || empty($attribute)) {
            throw new \Exception('Incorrect slug attribute or sluggable string for model! Check your "fillable" array.');
        }

        $modelsWithRelatedSlug = $model
            ->withoutGlobalScopes()
            ->withTrashed()
            ->where($attribute, 'LIKE', $slug.'%')
            ->get([$attribute]);

        $i = 0;
        while ($modelsWithRelatedSlug->contains($attribute, $slug)) {
            ++$i;
            $matches = [];
            if (preg_match('/^(.*?)-(\d+)$/', $slug, $matches)) {
                $nextNum = $matches[2] + $i;
                $slug = "{$matches[1]}-$nextNum";
            } else {
                $slug = "$slug-$i";
            }
        }

        $model = $model
            ->withoutGlobalScopes()
            ->where($attribute, $slug)
            ->first([$attribute]);

        if ($model) {
            // Still not unique...
            $slug = self::generateUniqueSlug($model);
        }

        return $slug;
    }
}

@gentritabazi
Copy link

Usage:

use App\Helpers\CoolSlug;
$slugLibrary = new CoolSlug(\App\Post::class);
return $slugLibrary->createSlug('test');

CoolSlug Class:

<?php
namespace App\Helpers;

class CoolSlug {
    private $entity;

    /**
     * Instantiate a new CoolSlug instance.
     */
    public function __construct($entity) {
        $this->entity = $entity;
    }
    
    /**
     * Generate a URL friendly "slug" from the given string.
     * 
     * @param $title String
     * @return string
     * @throws \Exception
     */
    public function createSlug($title) {
        // Normalize the title
        $slug = \Illuminate\Support\Str::slug($title, '-');

        // Get any that could possibly be related.
        // This cuts the queries down by doing it once.
        $allSlugs = $this->getRelatedSlugs($slug);

        // If we haven't used it before then we are all good.
        if($allSlugs == 0) {
            return $slug;
        }

        // Just append numbers like a savage until we find not used.
        for($i = 1; $i <= 20; $i++) {
            $newSlug = $slug. '-'. $i;
            if($this->getRelatedSlugs($newSlug) == 0) {
                return $newSlug;
            }
        }

        throw new \Exception('Can not create a unique slug.');
    }

    protected function getRelatedSlugs($slug) {
        return call_user_func(array($this->entity, 'select'), 'permalink')->where('permalink', $slug)->count();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment