├── CHANGELOG.md ├── start.php ├── config └── sluggable.php ├── README.md ├── tests └── sluggable.test.php └── sluggable.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ### Version 1.1 -- 14 September, 2012 5 | 6 | - refactored sluggable to allow for testing 7 | - added unit tests for all of sluggable's current functionalities 8 | 9 | ### Version 1.0 -- 12 September, 2012 10 | 11 | - initial release 12 | - global configuration is done in `application/config/sluggable.php` and 13 | model-level configuration is done in model -------------------------------------------------------------------------------- /start.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bryan te Beek 10 | * @link http://github.com/bryantebeek/laravel-bundle-sluggable 11 | */ 12 | 13 | Autoloader::map( 14 | array('Sluggable' => __DIR__ . DS . 'sluggable.php' 15 | )); 16 | 17 | // Listen to the Eloquent save event so we can sluggify on the fly 18 | Event::listen('eloquent.saving', array('Sluggable', 'make')); 19 | -------------------------------------------------------------------------------- /config/sluggable.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bryan te Beek 10 | * @link http://github.com/bryantebeek/laravel-bundle-sluggable 11 | */ 12 | 13 | 14 | return array( 15 | 16 | /** 17 | * What attributes do we use to build the slug? 18 | * This can be a single field, like "name" which will build a slug from: 19 | * 20 | * $model->name; 21 | * 22 | * Or it can be an array of fields, like ("name", "company"), which builds a slug from: 23 | * 24 | * $model->name . ' ' . $model->company; 25 | * 26 | * If you've defined custom getters in your model, you can use those too, 27 | * since Eloquent will call them when you request a custom attribute. 28 | * 29 | * Defaults to null, which uses the toString() method on your model. 30 | * 31 | */ 32 | 'build_from' => null, 33 | 34 | /** 35 | * What field to we store the slug in? Defaults to "slug". 36 | * You need to configure this when building the SQL for your database, e.g.: 37 | * 38 | * Schema::create('users', function($table) 39 | * { 40 | * $table->string('slug'); 41 | * }); 42 | * 43 | */ 44 | 'save_to' => 'slug', 45 | 46 | /** 47 | * What style should we use to build the slug? 48 | * 49 | * - "slug" uses Laravel's Str::slug() 50 | * 51 | * Defaults to "slug" (only option for now). 52 | */ 53 | 'style' => 'slug', 54 | 55 | /** 56 | * Separator to use. Defaults to a hyphen. 57 | */ 58 | 'separator' => '-', 59 | 60 | /** 61 | * Enforce uniqueness of slugs? Defaults to true. 62 | * If a generated slug already exists, an incremental numeric 63 | * value will be appended to the end until a unique slug is found. e.g.: 64 | * 65 | * my-slug 66 | * my-slug-1 67 | * my-slug-2 68 | */ 69 | 'unique' => true, 70 | 71 | /** 72 | * Whether to update the slug value when a model is being 73 | * re-saved (i.e. already exists). Defaults to false, which 74 | * means slugs are not updated. 75 | */ 76 | 'on_update' => false, 77 | 78 | ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sluggable 2 | ========= 3 | 4 | Easy automatic slug generation for your Eloquent models. 5 | 6 | 7 | 8 | ## Installing the Bundle 9 | 10 | Install the bundle using Artisan: 11 | 12 | ``` 13 | php artisan bundle::install sluggable 14 | ``` 15 | 16 | Update your `application/bundles.php` file with: 17 | 18 | ```php 19 | 'sluggable' => array( 'auto' => true ), 20 | ``` 21 | 22 | ## Updating your Models 23 | 24 | Define a public static property `$sluggable` with the definitions 25 | (see [#Configuration] below for details): 26 | 27 | ```php 28 | class Post extends Eloquent 29 | { 30 | 31 | public static $sluggable = array( 32 | 'build_from' => 'title', 33 | 'save_to' => 'slug', 34 | ); 35 | 36 | } 37 | ``` 38 | 39 | That's it ... your model is now "sluggable"! 40 | 41 | 42 | ## Using the Class 43 | 44 | Saving a model is easy: 45 | 46 | ```php 47 | $post = new Post(array( 48 | 'title' => 'My Awesome Blog Post' 49 | )); 50 | 51 | $post->save(); 52 | ``` 53 | 54 | And so is retrieving the slug: 55 | 56 | ```php 57 | echo $post->slug; 58 | ``` 59 | 60 | 61 | 62 | ## Configuration 63 | 64 | Configuration was designed to be as flexible as possible. You can set up 65 | defaults for all of your Eloquent models, and then override those settings 66 | for individual models. 67 | 68 | By default, global configuration can be set in the 69 | `application/config/sluggable.php` file. If a configuration isn't set, 70 | then the bundle defaults from `bundles/sluggable/config/sluggable.php` 71 | are used. Here is an example configuration, with all the settings shown: 72 | 73 | ```php 74 | return array( 75 | 'build_from' => null, 76 | 'save_to' => 'slug', 77 | 'style' => 'slug', 78 | 'separator' => '-', 79 | 'unique' => true, 80 | 'on_update' => false, 81 | ); 82 | ``` 83 | 84 | `build_from` is the field or array of fields from which to build the slug. 85 | Each `$model->field` is contactenated (with space separation) to build the 86 | sluggable string. This can be model attribues (i.e. fields in the database) 87 | or custom getters. So, for example, this works: 88 | 89 | ```php 90 | class Person extends Eloquent { 91 | 92 | public static $sluggable = array( 93 | 'build_from' => 'fullname' 94 | ); 95 | 96 | public function get_fullname() { 97 | return $this->firstname . ' ' . $this->lastname; 98 | } 99 | 100 | } 101 | ``` 102 | 103 | If `build_from` is empty, false or null, then the value of `$model->__toString()` 104 | is used. 105 | 106 | `save_to` is the field in your model where the slug is stored. By default, 107 | this is "slug". You need to create this column in your table when defining 108 | your schema: 109 | 110 | ```php 111 | Schema::create('posts', function($table) 112 | { 113 | $table->increments('id'); 114 | $table->string('title'); 115 | $table->string('body'); 116 | $table->string('slug'); 117 | $table->timestamps(); 118 | }); 119 | ``` 120 | 121 | `style` defines the method used to turn the sluggable string into a slug. 122 | Right now (version 1.0) the only option is "slug" which uses Laravel's 123 | `Str::slug()` method. 124 | 125 | `separator` defines the separator used when building a slug. Default is a 126 | hyphen. 127 | 128 | `unique` is a boolean defining whether slugs should be unique among all 129 | models of the given type. For example, if you have two blog posts and both 130 | are called "My Blog Post", then they will both sluggify to "my-blog-post" 131 | (when using Sluggable's default settings). This could be a problem, e.g. if you 132 | use the slug in URLs. 133 | 134 | By turning `unique` on, then the second Post model will sluggify to 135 | "my-blog-post-1". If there is a third post with the same title, it will 136 | sluggify to "my-blog-post-2" and so on. Each subsequent model will get 137 | an incremental value appended to the end of the slug, ensuring uniqueness. 138 | 139 | `on_update` is a boolean. If it is `false` (the default value), then slugs 140 | will not be updated if a model is resaved (e.g. if you change the title 141 | of your blog post, the slug will remain the same) or the slug value has already 142 | been set. You can set it to `true` (or manually change the $model->slug value 143 | in your own code) if you want to override this behaviour. 144 | 145 | (If you want to manually set the slug value using your model's Sluggable settings, 146 | you can run `Sluggable::make($model, true)`. The second arguement forces 147 | Sluggable to update the slug field.) 148 | 149 | 150 | ## Credits 151 | 152 | The idea for this bundle came from using `actAs Sluggable` from the Doctrine ORM. -------------------------------------------------------------------------------- /tests/sluggable.test.php: -------------------------------------------------------------------------------- 1 | model = new Model(); 11 | $this->model->title = 'This is a test'; 12 | $this->model->other_title_field = 'This is another test'; 13 | } 14 | 15 | public function testSlug() 16 | { 17 | Sluggable::make($this->model, false, array()); 18 | $this->assertEquals("this-is-a-test", $this->model->slug); 19 | } 20 | 21 | public function testExistsWithoutIndex() 22 | { 23 | $dummy = new Model(); 24 | $dummy->slug = 'this-is-a-test'; 25 | 26 | Sluggable::make($this->model, false, array($dummy)); 27 | $this->assertEquals("this-is-a-test-1", $this->model->slug); 28 | } 29 | 30 | public function testExistsWithIndex() 31 | { 32 | $dummy = new Model(); 33 | $dummy->slug = 'this-is-a-test-5'; 34 | 35 | Sluggable::make($this->model, false, array($dummy)); 36 | $this->assertEquals("this-is-a-test-6", $this->model->slug); 37 | } 38 | 39 | public function testOnUpdateTrue() { 40 | $class = get_class($this->model); 41 | $class::$sluggable['on_update'] = true; 42 | 43 | $this->model->exists = true; 44 | $this->model->slug = 'random'; 45 | 46 | Sluggable::make($this->model, false, array()); 47 | $this->assertEquals("this-is-a-test", $this->model->slug); 48 | 49 | $class::$sluggable['on_update'] = null; 50 | } 51 | 52 | public function testOnUpdateFalse() { 53 | $class = get_class($this->model); 54 | $class::$sluggable['on_update'] = false; 55 | 56 | $this->model->exists = true; 57 | $this->model->slug = 'random'; 58 | 59 | Sluggable::make($this->model, false, array()); 60 | $this->assertEquals("random", $this->model->slug); 61 | 62 | $class::$sluggable['on_update'] = null; 63 | } 64 | 65 | public function testForce() { 66 | $class = get_class($this->model); 67 | $class::$sluggable['on_update'] = false; 68 | 69 | $this->model->exists = true; 70 | $this->model->slug = 'random'; 71 | 72 | Sluggable::make($this->model, true, array()); 73 | $this->assertEquals("this-is-a-test", $this->model->slug); 74 | } 75 | 76 | public function testSaveTo() { 77 | $class = get_class($this->model); 78 | $class::$sluggable['save_to'] = 'other_slug_field'; 79 | 80 | Sluggable::make($this->model, false, array()); 81 | $this->assertEquals("this-is-a-test", $this->model->other_slug_field); 82 | 83 | $class::$sluggable['save_to'] = 'slug'; 84 | } 85 | 86 | public function testBuildFrom() { 87 | $class = get_class($this->model); 88 | $class::$sluggable['build_from'] = 'other_title_field'; 89 | 90 | Sluggable::make($this->model, false, array()); 91 | $this->assertEquals("this-is-another-test", $this->model->slug); 92 | 93 | $class::$sluggable['build_from'] = 'title'; 94 | } 95 | 96 | public function testSeparator() { 97 | $class = get_class($this->model); 98 | $class::$sluggable['separator'] = '/'; 99 | 100 | Sluggable::make($this->model, false, array()); 101 | $this->assertEquals("this/is/a/test", $this->model->slug); 102 | 103 | $class::$sluggable['separator'] = '-'; 104 | } 105 | 106 | public function testNotUnique() { 107 | $class = get_class($this->model); 108 | $class::$sluggable['unique'] = false; 109 | 110 | $dummy = new Model(); 111 | $dummy->slug = 'this-is-a-test'; 112 | 113 | Sluggable::make($this->model, false, array($dummy)); 114 | $this->assertEquals("this-is-a-test", $this->model->slug); 115 | 116 | $class::$sluggable['unique'] = true; 117 | } 118 | 119 | public function testMultipleBuildFrom() { 120 | $class = get_class($this->model); 121 | $class::$sluggable['build_from'] = array('title', 'other_title_field'); 122 | 123 | Sluggable::make($this->model, false, array()); 124 | $this->assertEquals("this-is-a-test-this-is-another-test", $this->model->slug); 125 | 126 | $class::$sluggable['build_from'] = 'title'; 127 | } 128 | 129 | public function tearDown() { 130 | $this->model = null; 131 | } 132 | } 133 | 134 | class Model extends Eloquent 135 | { 136 | public static $sluggable = array('build_from' => 'title'); 137 | } -------------------------------------------------------------------------------- /sluggable.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bryan te Beek 10 | * @link http://github.com/bryantebeek/laravel-bundle-sluggable 11 | */ 12 | class Sluggable 13 | { 14 | /** 15 | * @var null|\Laravel\Database\Eloquent\Model The model that is going to be sluggified is stored here. 16 | */ 17 | private $model = null; 18 | /** 19 | * @var null|array The configuration array is stored here for internal use. 20 | */ 21 | private $configuration = null; 22 | 23 | /** 24 | * @param \Laravel\Database\Eloquent\Model $model The model that needs to be sluggified. 25 | * @param bool $force Force the model to generate a new slug, usefull if 'on_update' needs to bypassed. Defaults to false. 26 | * @param array $objects An array of {@link \Laravel\Database\Eloquent\Model} objects which the slug should be tested against, this defaults to null in which case Sluggable will check the database to determine the slug. 27 | */ 28 | public static function make($model, $force = false, $objects = null) 29 | { 30 | $class = get_class($model); 31 | 32 | // If the class has no sluggable configuration, there is nothing to be done so we stop. 33 | if (!isset($class::$sluggable)) 34 | { 35 | return; 36 | } 37 | 38 | $instance = new Sluggable($model); 39 | 40 | // Check if the model is going to be created or updated, if it's going to update 41 | // and 'on_update' is false we can stop, unless we forced the update with the force parameter. 42 | if (($model->exists && $instance->configuration['on_update'] == false) && !$force) 43 | { 44 | return; 45 | } 46 | 47 | // Create the slug 48 | $slug = $instance->slug(); 49 | 50 | // Check if the slug is already present and weither we want to have unique slugs 51 | if (($object = $instance->exists($slug, $objects)) && $instance->configuration['unique']) 52 | { 53 | // If the slug exists, we get the next available slug 54 | $slug = $instance->next($slug, $object); 55 | } 56 | 57 | // We set the 'save_to' field of the model to the newly created slug, yeah! 58 | $model->{$instance->configuration['save_to']} = $slug; 59 | } 60 | 61 | /** 62 | * The constructor for Sluggable 63 | * 64 | * @param $model 65 | */ 66 | protected function __construct($model) 67 | { 68 | $this->model = $model; 69 | $this->configure(); 70 | } 71 | 72 | /** 73 | * This functions load our configuration in a few steps: 74 | * 1. We get the configuration from the model 75 | * 2. We get the 'sluggable' configuration from the main application 76 | * 3. We get the 'sluggable' configuration from our bundle 77 | * Then we merge the configurations and save them for later use. 78 | */ 79 | protected function configure() 80 | { 81 | $class = get_class($this->model); 82 | $model_configuration = $class::$sluggable; 83 | $default_configuration = Config::get('sluggable', Config::get('sluggable::sluggable', array())); 84 | 85 | $this->configuration = array_merge($default_configuration, $model_configuration); 86 | } 87 | 88 | /** 89 | * Creates the actual slug from the 'build_from' field using the 'separator' configuration. 90 | * 91 | * @return string 92 | */ 93 | protected function slug() 94 | { 95 | $build_from = $this->configuration['build_from']; 96 | if (isset($build_from)) 97 | { 98 | $build_from = !is_array($build_from) ? array($build_from) : $build_from; 99 | 100 | $string = ''; 101 | foreach ($build_from as $field) 102 | { 103 | $string .= $this->model->{$field}.' '; 104 | } 105 | } else 106 | { 107 | $string = $this->model->__toString(); 108 | } 109 | 110 | $separator = $this->configuration['separator']; 111 | return Str::slug($string, $separator); 112 | } 113 | 114 | /** 115 | * @param string $slug The slug which we are going to determine the existence of 116 | * @param \Laravel\Database\Eloquent\Model $objects An array of {@link \Laravel\Database\Eloquent\Model} which we are going to test our slug against to see if it already exists. 117 | * 118 | * @return mixed If an object exists with the same slug, return it, otherwise return false. 119 | */ 120 | protected function exists($slug, $objects = null) 121 | { 122 | $class = get_class($this->model); 123 | 124 | if (!isset($objects)) 125 | { 126 | // Get the objects with a slug like our preffered one and choose the one with the highest index prepended. 127 | $object = $class::where($this->configuration['save_to'], 'LIKE', $slug.'%')->order_by( 128 | $this->configuration['save_to'], 129 | 'DESC' 130 | )->first(); 131 | return $object; 132 | } else 133 | { 134 | foreach ($objects as $object) 135 | { 136 | if (strstr($object->{$this->configuration['save_to']}, $slug) !== false) 137 | { 138 | return $object; 139 | } 140 | } 141 | } 142 | return null; 143 | } 144 | 145 | /** 146 | * @param string $slug The current slug, which we are going to append an index to. 147 | * @param \Laravel\Database\Eloquent\Model $object The object with the slug with the highest index 148 | * 149 | * @return string The newly calculated slug 150 | */ 151 | protected function next($slug, $object) 152 | { 153 | $idx = substr($object->{$this->configuration['save_to']}, strlen($slug)); 154 | $idx = ltrim($idx, $this->configuration['separator']); 155 | $idx = intval($idx); 156 | $idx++; 157 | 158 | return $slug .= $this->configuration['separator'].$idx; 159 | } 160 | } --------------------------------------------------------------------------------