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();
}
}
@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