├── CHANGELOG.md ├── database └── migrations │ └── create_slugidable_table.php.stub ├── LICENSE.md ├── composer.json ├── src └── Slugidable.php └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-slugidable` will be documented in this file. 4 | 5 | ## 1.3.0 - 2025-02-28 6 | 7 | - Updated to Laravel 12 8 | 9 | ## 1.2.0 - 2024-03-12 10 | 11 | - Updated to Laravel 11 12 | 13 | ## 1.1.1 - 2023-02-06 14 | 15 | - Laravel 10 Update 16 | 17 | ## 1.1.0 - 2022-02-10 18 | 19 | - Force the system to always slug from `slug_from` regardless if `slug_to` is present 20 | 21 | ## 1.0.0 - 2022-02-09 22 | 23 | - Initial Release 24 | -------------------------------------------------------------------------------- /database/migrations/create_slugidable_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('title'); 15 | $table->string('slug'); 16 | 17 | $table->timestamps(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) josezenem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "josezenem/laravel-slugidable", 3 | "description": "A package for Laravel that creates slugs for Eloquent models based on title and ID", 4 | "keywords": [ 5 | "josezenem", 6 | "laravel", 7 | "laravel-slugidable", 8 | "slugidable", 9 | "sluggable", 10 | "models", 11 | "php" 12 | ], 13 | "homepage": "https://github.com/josezenem/laravel-slugidable", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Jose Jimenez", 18 | "email": "jose@jimenez.dev", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.0", 24 | "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0" 26 | }, 27 | "require-dev": { 28 | "nunomaduro/larastan": "^1.0", 29 | "orchestra/testbench": "6.23|^7.0|^8.0|^9.0|^10.0", 30 | "pestphp/pest": "^1.22|^2.34|^3.7", 31 | "pestphp/pest-plugin-laravel": "^1.3|^2.3|^3.1", 32 | "phpstan/extension-installer": "^1.1", 33 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 34 | "phpstan/phpstan-phpunit": "^1.0|^2.0", 35 | "phpunit/phpunit": "^9.5|^10.5|^11.5.3" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Josezenem\\Slugidable\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Josezenem\\Slugidable\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "analyse": "vendor/bin/phpstan analyse", 49 | "test": "vendor/bin/pest", 50 | "test-coverage": "vendor/bin/pest --coverage" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true 61 | } 62 | -------------------------------------------------------------------------------- /src/Slugidable.php: -------------------------------------------------------------------------------- 1 | slugidableSettings = [ 16 | 'slug_from' => 'title', 17 | 'slug_to' => 'slug', 18 | 'using_key_name' => $this->getKeyName(), 19 | 'on' => 'suffix', 20 | 'using_separator' => '-', 21 | 'force_slug_from' => false, 22 | ]; 23 | } 24 | 25 | /** 26 | * Boot the trait 27 | * 28 | * @return void 29 | */ 30 | protected static function bootSlugidable() 31 | { 32 | static::saving(function ($model) { 33 | $model->slug = $model->getSlugidableSlug(); 34 | }); 35 | 36 | static::saved(function ($model) { 37 | if (Str::startsWith($model->slug, [':t', ':u'])) { 38 | $model->updateQuietly(['slug' => $model->getSlugidableSlug()]); 39 | } 40 | }); 41 | } 42 | 43 | protected function getSlugidableSlug(): UuidInterface|string 44 | { 45 | $this->configureSlugidableSettings(); 46 | 47 | $using_key_name = $this->slugidableSettings['using_key_name']; 48 | 49 | $slug_to = $this->getAttribute($this->slugidableSettings['slug_to']); 50 | $slug_from = $this->getAttribute($this->slugidableSettings['slug_from']); 51 | 52 | if ($this->getAttribute($using_key_name)) { 53 | if (Str::startsWith($slug_to, ':u') !== false) { 54 | $preffered_slug = $slug_from; 55 | } elseif (Str::startsWith($slug_to, ':t') !== false) { 56 | $preffered_slug = substr($slug_to, 2); 57 | } elseif ($this->slugidableSettings['force_slug_from'] === false && $slug_to) { 58 | $preffered_slug = $slug_to; 59 | } else { 60 | $preffered_slug = $slug_from; 61 | } 62 | if ($this->slugidableSettings['on'] === 'prefix') { 63 | $slug = Str::start(Str::slug($preffered_slug, $this->slugidableSettings['using_separator']), $this->getAttribute($using_key_name) . $this->slugidableSettings['using_separator']); 64 | } else { 65 | $slug = Str::finish(Str::slug($preffered_slug, $this->slugidableSettings['using_separator']), $this->slugidableSettings['using_separator'] . $this->getAttribute($using_key_name)); 66 | } 67 | } elseif ($this->slugidableSettings['force_slug_from'] === false && $slug_to) { 68 | $slug = ':t' . $slug_to; 69 | } else { 70 | $slug = ':u' . Str::uuid()->toString(); 71 | } 72 | 73 | return $slug; 74 | } 75 | 76 | /** 77 | * Scope a query to search based on the KeyName only 78 | * 79 | * @param Builder $query 80 | * @param string $slug 81 | * @return Builder 82 | */ 83 | public function scopeFromSlugidable($query, $slug) 84 | { 85 | $this->configureSlugidableSettings(); 86 | 87 | if ($this->slugidableSettings['using_separator'] === 'prefix') { 88 | $slug_id = Str::before($slug, $this->slugidableSettings['using_separator']); 89 | } else { 90 | $slug_id = Str::afterLast($slug, $this->slugidableSettings['using_separator']); 91 | } 92 | 93 | $query->where($this->slugidableSettings['using_key_name'], $slug_id); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slugidable Generate slugs based on title and ID 2 | 3 | [![Latest Stable Version](http://poser.pugx.org/josezenem/laravel-slugidable/v)](https://packagist.org/packages/josezenem/laravel-slugidable) 4 | [![Tests](https://github.com/josezenem/laravel-slugidable/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/josezenem/laravel-slugidable/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/josezenem/laravel-slugidable.svg?style=flat-square)](https://packagist.org/packages/josezenem/laravel-slugidable) 6 | 7 | A package for Laravel that creates slugs for Eloquent models based on both a title and ID 8 | 9 | 10 | ```php 11 | $model = new Blog(); 12 | $model->title = 'Dwight jumped over the fence'; 13 | $model->save(); 14 | 15 | echo $model->slug; // output: dwight-jumped-over-the-fence-012422 16 | ``` 17 | 18 | All settings are fully configurable for each model. 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require josezenem/laravel-slugidable 26 | ``` 27 | 28 | ## Usage 29 | 30 | Simply `Josezenem\Slugidable\Slugidable` trait to your model. 31 | ```php 32 | // App\Models\Blog 33 | 'My dog Dwight jumped over the fence', 48 | ]) 49 | 50 | // When this is created, it will make the slug of 51 | //my-dog-dwight-jumped-over-the-fence-1 52 | ``` 53 | 54 | We have included a handy scope method: **fromSlugidable()** that will extract the ID from the slug and search the model 55 | 56 | ```php 57 | $blog = Blog::fromSlugidable('my-dog-dwight-012422')->first(); 58 | 59 | // in this scenario only ID: 012422 is used inside the scope to find the slug. 60 | 61 | ``` 62 | 63 | #### Configuration 64 | 65 | By default we use id, title, slug columns from the model, but you can override these settings by adding the following method to your model. 66 | 67 | ```php 68 | protected function configureSlugidableSettings():void 69 | { 70 | $this->slugidableSettings = [ 71 | 'slug_from' => 'title', 72 | 'slug_to' => 'slug', 73 | 'using_key_name' => $this->getKeyName(), 74 | 'on' => 'suffix', 75 | 'using_separator' => '-', 76 | 'force_slug_from' => false, 77 | ]; 78 | } 79 | ``` 80 | * **slug_from** is where the slug will grab the the slug from 81 | * **slug_to** the place where the slug lives 82 | * **using_key_name** the ID field used to prefix or suffix the ID 83 | * **on** could be "prefix" to have the ID before the slug text, or "suffix" to have it after. 84 | * **using_separator** the seperator to use during slug creation 85 | * **force_slug_from** force the system to always slug from 'slug_from' regardless if `slug_to` is present 86 | 87 | 88 | ## Testing 89 | 90 | ```bash 91 | composer test 92 | ``` 93 | 94 | ## Changelog 95 | 96 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 97 | 98 | ## Contributing 99 | 100 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 101 | 102 | ## Security Vulnerabilities 103 | 104 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 105 | 106 | ## Credits 107 | 108 | - [Jose Jimenez](https://github.com/josezenem) 109 | - [All Contributors](../../contributors) 110 | 111 | ## License 112 | 113 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 114 | --------------------------------------------------------------------------------