From 840602711f3fb32b8c603ec603e1add066d1e4b7 Mon Sep 17 00:00:00 2001 From: Ben-Piet O'Callaghan Date: Fri, 7 Jul 2017 14:14:30 +0100 Subject: [PATCH] Initial release --- CHANGELOG.md | 4 + README.md | 67 +++++++++++++++ composer.json | 28 +++++++ src/HasSlug.php | 194 ++++++++++++++++++++++++++++++++++++++++++++ src/SlugOptions.php | 75 +++++++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/HasSlug.php create mode 100644 src/SlugOptions.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f8965e7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 1.0.0 - 2017-07-07 +- Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..acfaa20 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Generate slugs when saving Laravel Eloquent models + +Provides a HasSlug trait that will generate a unique slug when saving your Laravel Eloquent model. + +The slugs are generated with Laravel `str_slug` method, whereby spaces are converted to '-'. + +```php +$model = new EloquentModel(); +$model->name = 'laravel is awesome'; +$model->save(); + +echo $model->slug; // ouputs "laravel-is-awesome" +``` + +## Installation + +Update your project's `composer.json` file. + +```bash +composer require bpocallaghan/sluggable +``` + +## Usage + +Your Eloquent models can use the `Bpocallaghan\Sluggable\HasSlug` trait and the `Bpocallaghan\Sluggable\SlugOptions` class. + +The trait has a protected method `getSlugOptions()` that you can implement for customization. + +Here's an example: + +```php +class YourEloquentModel extends Model +{ + use HasSlug; + + protected function getSlugOptions() + { + return SlugOptions::create() + ->slugSeperator('-') + ->generateSlugFrom('name') + ->saveSlugTo('slug'); + } +} +``` + +## Config + +You do not have to add the method in you model (the above will be used as default). It is only needed when you want to change the default behavior. + +By default it will generate a slug from the `name` and save to the `slug` column. + +It will suffix a `-1` to make the slug unique. You can disable it by calling `makeSlugUnique(false)`. + +It will use the `-` as a separator. You can change this by calling `slugSeperator('_')`. + +You can use multiple fields as the source of the slug `generateSlugFrom(['firstname', 'lastname'])`. + +You can also pass a `callable` function to `generateSlugFrom()`. + +Have a look [here for the options](https://github.com/bpocallaghan/sluggable/src/SlugOptions.php) and available config functions. + +## Change log + +Please see the [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +#### Demonstration +See it in action at a [Laravel Admin Starter](https://github.com/bpocallaghan/laravel-admin-starter) project. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c1a9d82 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "bpocallaghan/sluggable", + "description": "Provides a HasSlug trait that will generate a unique slug when saving your Laravel Eloquent model.", + "keywords": [ + "laravel", + "sluggable", + "slug", + "eloquent", + "unique" + ], + "license": "MIT", + "authors": [ + { + "name": "Ben-Piet O'Callaghan", + "email": "bpocallaghan@gmail.com", + "homepage": "http://bpocallaghan.co.za", + "role": "Developer" + } + ], + "require": { + "php": ">=5.6.4" + }, + "autoload": { + "psr-4": { + "Bpocallaghan\\Sluggable\\": "src/" + } + } +} diff --git a/src/HasSlug.php b/src/HasSlug.php new file mode 100644 index 0000000..515a5b5 --- /dev/null +++ b/src/HasSlug.php @@ -0,0 +1,194 @@ +generateSlugOnCreate(); + }); + + static::updating(function (Model $model) { + $model->generateSlugOnUpdate(); + }); + } + + /** + * Generate a slug on create + */ + protected function generateSlugOnCreate() + { + $this->slugOptions = $this->getSlugOptions(); + + $this->createSlug(); + } + + /** + * Handle adding slug on model update. + */ + protected function generateSlugOnUpdate() + { + $this->slugOptions = $this->getSlugOptions(); + + // check updating + $slugNew = $this->generateNonUniqueSlug(); + $slugCurrent = $this->attributes[$this->slugOptions->slugField]; + + // if new base slug is in string as old slug (the slug source's value did not change) + // see if the slug is still unique in database + if (strpos($slugCurrent, $slugNew) === 0) { + $slugUpdate = $this->checkUpdatingSlug($slugCurrent); + // no need to update slug (slug is still unique) + if ($slugUpdate !== false) { + return; + } + } + + $this->createSlug(); + } + + /** + * Handle setting slug on explicit request. + */ + public function generateSlug() + { + $this->slugOptions = $this->getSlugOptions(); + + $this->createSlug(); + } + + /** + * Add the slug to the model. + */ + protected function createSlug() + { + $slug = $this->generateNonUniqueSlug(); + + if ($this->slugOptions->generateUniqueSlug) { + $slug = $this->makeSlugUnique($slug); + } + + $this->attributes[$this->slugOptions->slugField] = $slug; + } + + /** + * Generate a non unique slug for this record. + */ + protected function generateNonUniqueSlug() + { + return str_slug($this->getSlugSourceString(), $this->slugOptions->slugSeparator); + } + + /** + * Get the string that should be used as base for the slug. + */ + protected function getSlugSourceString() + { + // concatenate on the fields and implode on seperator + $slug = collect($this->slugOptions->generateSlugFrom) + ->map(function ($fieldName = '') { + return $this->$fieldName; + })->implode($this->slugOptions->slugSeparator); + + return $slug; + } + + /** + * @param $slug + * @return string + */ + protected function makeSlugUnique($slug) + { + // get existing slugs + $list = $this->getExistingSlugs($slug); + + // slug is already unique + if ($list->count() === 0) { + return $slug; + } + + // generate unique suffix + return $this->generateSlugSuffix($slug, $list); + } + + /** + * Get existing slugs matching slug + * + * @param $slug + * @return \Illuminate\Support\Collection|static + */ + protected function getExistingSlugs($slug) + { + return static::whereRaw("{$this->slugOptions->slugField} LIKE '$slug%'") + ->withoutGlobalScopes()// ignore scopes + ->withTrashed()// trashed, when entry gets activated again + ->orderBy($this->slugOptions->slugField) + ->get() + ->pluck($this->slugOptions->slugField); + } + + /** + * Suffix unique index to slug + * + * @param $slug + * @param $list + * @return string + */ + private function generateSlugSuffix($slug, $list) + { + $seperator = $this->slugOptions->slugSeparator; + + // loop through list and get highest index number + // incase the order is faulty + $index = $list->map(function ($s) use ($slug, $seperator) { + // str_replace instead of explode('-'); + return intval(str_replace($slug . $seperator, '', $s)); + })->sort()->last(); + + return $slug . $seperator . ($index + 1); + } + + /** + * Check if we are updating + * Find entries with same slug + * Exlude current model's entry + * + * @param $slug + * @return bool + */ + private function checkUpdatingSlug($slug) + { + if ($this->id >= 1) { + // find entries matching slug, exclude updating entry + $exist = self::where($this->slugOptions->slugField, $slug) + ->where('id', '!=', $this->id) + ->first(); + + // no entries, save to use current slug + if (!$exist) { + return $slug; + } + } + + // new unique slug needed + return false; + } +} diff --git a/src/SlugOptions.php b/src/SlugOptions.php new file mode 100644 index 0000000..236e369 --- /dev/null +++ b/src/SlugOptions.php @@ -0,0 +1,75 @@ +generateSlugFrom = $fieldName; + + return $this; + } + + /** + * Update the slug field name + * @param string $fieldName + * @return $this + */ + public function saveSlugTo($fieldName) + { + $this->slugField = $fieldName; + + return $this; + } + + /** + * If the slug must be unique + * @param bool $unique + * @return $this + */ + public function makeSlugUnique($unique = true) + { + $this->generateUniqueSlug = $unique; + + return $this; + } + + /** + * Set the slug seperator + * @param string $separator + * @return $this + */ + public function slugSeperator($separator = '-') + { + $this->slugSeparator = $separator; + + return $this; + } +}