Skip to content

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
bpocallaghan committed Jul 7, 2017
1 parent 69f71f0 commit 8406027
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## 1.0.0 - 2017-07-07
- Initial release
67 changes: 67 additions & 0 deletions README.md
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.
28 changes: 28 additions & 0 deletions composer.json
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/"
}
}
}
194 changes: 194 additions & 0 deletions src/HasSlug.php
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;
}
}
75 changes: 75 additions & 0 deletions src/SlugOptions.php
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;
}
}

0 comments on commit 8406027

Please sign in to comment.