-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
69f71f0
commit 8406027
Showing
5 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Changelog | ||
|
||
## 1.0.0 - 2017-07-07 | ||
- Initial release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "[email protected]", | ||
"homepage": "http://bpocallaghan.co.za", | ||
"role": "Developer" | ||
} | ||
], | ||
"require": { | ||
"php": ">=5.6.4" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Bpocallaghan\\Sluggable\\": "src/" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
<?php | ||
|
||
namespace Bpocallaghan\Sluggable; | ||
|
||
use Illuminate\Database\Eloquent\Model; | ||
|
||
trait HasSlug | ||
{ | ||
/** @var \Bpocallaghan\Sluggable\SlugOptions */ | ||
protected $slugOptions; | ||
|
||
/** | ||
* Get the options for generating the slug. | ||
*/ | ||
protected function getSlugOptions() | ||
{ | ||
return SlugOptions::create(); | ||
} | ||
|
||
/** | ||
* Boot the trait. | ||
*/ | ||
protected static function bootHasSlug() | ||
{ | ||
static::creating(function (Model $model) { | ||
$model->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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?php | ||
|
||
namespace Bpocallaghan\Sluggable; | ||
|
||
class SlugOptions | ||
{ | ||
/** @var string|array */ | ||
public $generateSlugFrom = 'name'; | ||
|
||
/** @var string */ | ||
public $slugField = 'slug'; | ||
|
||
/** @var bool */ | ||
public $generateUniqueSlug = true; | ||
|
||
/** @var string */ | ||
public $slugSeparator = '-'; | ||
|
||
/** | ||
* Create new instance of the Slug Options | ||
* @return static | ||
*/ | ||
public static function create() | ||
{ | ||
return new static(); | ||
} | ||
|
||
/** | ||
* @param string|array|callable $fieldName | ||
* | ||
* @return \Bpocallaghan\Sluggable\SlugOptions | ||
*/ | ||
public function generateSlugFrom($fieldName) | ||
{ | ||
$this->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; | ||
} | ||
} |