🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,15 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
@@ -0,0 +1 @@
github: spatie
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
@@ -0,0 +1,40 @@
name: dependabot-auto-merge
on: pull_request_target
permissions:
pull-requests: write
contents: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.3.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
compat-lookup: true
- name: Auto-merge Dependabot PRs for semver-minor updates
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Auto-merge Dependabot PRs for semver-patch updates
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90%
if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -0,0 +1,23 @@
name: Check & fix styling
on: [push]
jobs:
php-cs-fixer:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Run PHP CS Fixer
uses: docker://oskarstark/php-cs-fixer-ga
with:
args: --config=.php-cs-fixer.dist.php --allow-risky=yes
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Fix styling
@@ -0,0 +1,65 @@
name: run-tests
on:
- push
- pull_request
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.3, 8.2, 8.1, 8.0]
laravel: ['8.*', '9.*', '10.*', '11.*', '12.*']
stability: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
- laravel: 10.*
testbench: 8.*
- laravel: 9.*
testbench: 7.*
- laravel: 8.*
testbench: 6.23
- laravel: 12.*
testbench: 10.*
exclude:
- laravel: 11.*
php: 8.1
- laravel: 11.*
php: 8.0
- laravel: 10.*
php: 8.0
- laravel: 12.*
php: 8.1
- laravel: 12.*
php: 8.0
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
coverage: none
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/pest
@@ -0,0 +1,28 @@
name: "Update Changelog"
on:
release:
types: [released]
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
- name: Update Changelog
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ github.event.release.name }}
release-notes: ${{ github.event.release.body }}
- name: Commit updated CHANGELOG
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: main
commit_message: Update CHANGELOG
file_pattern: CHANGELOG.md
@@ -0,0 +1,38 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->notPath('bootstrap/*')
->notPath('storage/*')
->notPath('storage/*')
->notPath('resources/view/mail/*')
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config)
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
]
])
->setFinder($finder);
@@ -0,0 +1,330 @@
# Changelog
All notable changes to `laravel-sluggable` will be documented in this file
## 3.7.4 - 2025-02-28
### What's Changed
* Allow to generate custom suffixes by @josepdecid in https://github.com/spatie/laravel-sluggable/pull/289
### New Contributors
* @josepdecid made their first contribution in https://github.com/spatie/laravel-sluggable/pull/289
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.7.3...3.7.4
## 3.7.3 - 2025-02-20
### What's Changed
* Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/287
* Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-sluggable/pull/288
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.7.2...3.7.3
## 3.7.2 - 2024-12-30
### What's Changed
* Fix query for translatable slug by @andrii-trush in https://github.com/spatie/laravel-sluggable/pull/285
### New Contributors
* @andrii-trush made their first contribution in https://github.com/spatie/laravel-sluggable/pull/285
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.7.1...3.7.2
## 3.7.1 - 2024-12-23
### What's Changed
* Add explicit nullable type for additionalQuery parameter by @RomkaLTU in https://github.com/spatie/laravel-sluggable/pull/286
### New Contributors
* @RomkaLTU made their first contribution in https://github.com/spatie/laravel-sluggable/pull/286
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.7.0...3.7.1
## 3.7.0 - 2024-12-09
### What's Changed
* Changing reference to Laravel docs without Laravel version by @abishekrsrikaanth in https://github.com/spatie/laravel-sluggable/pull/274
* Bump dependabot/fetch-metadata from 1.6.0 to 2.1.0 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/278
* Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/281
* Add Fallback Locale and Additional Query Support to `findBySlug` by @mkeremcansev in https://github.com/spatie/laravel-sluggable/pull/284
### New Contributors
* @abishekrsrikaanth made their first contribution in https://github.com/spatie/laravel-sluggable/pull/274
* @mkeremcansev made their first contribution in https://github.com/spatie/laravel-sluggable/pull/284
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.6.0...3.7.0
## 3.6.0 - 2024-02-26
### What's Changed
* Bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/257
* Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/261
* Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/264
* Add laravel 11 support by @mokhosh in https://github.com/spatie/laravel-sluggable/pull/271
### New Contributors
* @mokhosh made their first contribution in https://github.com/spatie/laravel-sluggable/pull/271
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.5.0...3.6.0
## 3.5.0 - 2023-05-29
### What's Changed
- Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/250
- Fix badge with `build` status in `README.md` by @gomzyakov in https://github.com/spatie/laravel-sluggable/pull/252
- Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/255
- feat: add findBySlug alias by @IsraelOrtuno in https://github.com/spatie/laravel-sluggable/pull/256
### New Contributors
- @gomzyakov made their first contribution in https://github.com/spatie/laravel-sluggable/pull/252
- @IsraelOrtuno made their first contribution in https://github.com/spatie/laravel-sluggable/pull/256
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.4.2...3.5.0
## 3.4.2 - 2023-01-23
- update for Laravel 10
## 3.4.1 - 2022-12-07
### What's Changed
- Update README.md by @furiouskj in https://github.com/spatie/laravel-sluggable/pull/240
- composer allow-plugins config by @hotsaucejake in https://github.com/spatie/laravel-sluggable/pull/241
- Normalize composer.json by @patinthehat in https://github.com/spatie/laravel-sluggable/pull/247
- Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-sluggable/pull/246
- Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/laravel-sluggable/pull/245
- Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/laravel-sluggable/pull/248
- Allow set slug suffix starting number by @Vediovis in https://github.com/spatie/laravel-sluggable/pull/249
### New Contributors
- @furiouskj made their first contribution in https://github.com/spatie/laravel-sluggable/pull/240
- @hotsaucejake made their first contribution in https://github.com/spatie/laravel-sluggable/pull/241
- @dependabot made their first contribution in https://github.com/spatie/laravel-sluggable/pull/248
- @Vediovis made their first contribution in https://github.com/spatie/laravel-sluggable/pull/249
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.4.0...3.4.1
## 3.4.0 - 2022-03-28
## What's Changed
- Converts Test cases to Pest tests by @marvin-wtt in https://github.com/spatie/laravel-sluggable/pull/223
- Add ability to skip the slug generation by a condition by @masterix21 in https://github.com/spatie/laravel-sluggable/pull/227
## New Contributors
- @masterix21 made their first contribution in https://github.com/spatie/laravel-sluggable/pull/227
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.3.1...3.4.0
## 3.3.1 - 2022-03-09
## What's Changed
- Add support for spatie/laravel-translatable:^6.0 by @mziraki in https://github.com/spatie/laravel-sluggable/pull/224
## New Contributors
- @mziraki made their first contribution in https://github.com/spatie/laravel-sluggable/pull/224
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.3.0...3.3.1
## 3.3.0 - 2022-01-13
- support Laravel 9
## 3.2.0 - 2021-12-15
## What's Changed
- Adds support for implicit route model binding with translated slugs by @marvin-wtt in https://github.com/spatie/laravel-sluggable/pull/213
## New Contributors
- @marvin-wtt made their first contribution in https://github.com/spatie/laravel-sluggable/pull/213
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.1.1...3.2.0
## 3.1.1 - 2021-12-13
## What's Changed
- Migrate to PHP-CS-Fixer 3.x by @shuvroroy in https://github.com/spatie/laravel-sluggable/pull/203
- Adds test case for replicate method by @eduarguz in https://github.com/spatie/laravel-sluggable/pull/212
- Fix Deprecation: currentSlug is null by @phh in https://github.com/spatie/laravel-sluggable/pull/218
## New Contributors
- @shuvroroy made their first contribution in https://github.com/spatie/laravel-sluggable/pull/203
- @eduarguz made their first contribution in https://github.com/spatie/laravel-sluggable/pull/212
- @phh made their first contribution in https://github.com/spatie/laravel-sluggable/pull/218
**Full Changelog**: https://github.com/spatie/laravel-sluggable/compare/3.1.0...3.1.1
## 3.1.0 - 2021-06-04
- add extra scope callback option (#201)
## 3.0.2 - 2021-05-07
- bugfix for updating slugs generated from a callback (#200)
## 3.0.1 - 2021-04-22
- update slug on non unique names (#195)
## 3.0.0 - 2021-03-01
- require PHP 8+
- drop support for PHP 7.x
- convert syntax to PHP 8
- move Exceptions to `Exceptions` folder to match structure of other packages
## 2.6.2 - 2021-03-20
- Added translatable slug overriding (#190)
## 2.6.1 - 2020-01-31
- fix Eloquent model checking (#186)
## 2.6.0 - 2020-10-28
- add `preventOverwrite`
- add support for PHP 8
## 2.5.2 - 2020-10-01
- fixed an incompatibility bug with postgresql uuid column (#173)
## 2.5.1 - 2020-09-07
- add support for Laravel 8
## 2.5.0 - 2020-06-15
- add helper trait to integrate with `laravel-translatable` #155
## 2.4.2 - 2020-04-20
- fix bug that causes empty slugs when dealing with multi-bytes chars (#152)
## 2.4.1 - 2020-04-09
- use method for retrieving incrementing status of the model (#151)
## 2.4.0 - 2020-03-03
- add support for Laravel 7, drop support for Laravel 6
## 2.3.0 - 2019-12-06
- drop support for anything below PHP 7.4 and Laravel 6
## 2.2.1 - 2019-09-16
- Changed: Updated Laravel 6 compatibility for future versions
## 2.2.0 - 2019-09-04
- Drop support for PHP 7.1
- Add support for Laravel 6.0
## 2.1.8 - 2019-06-08
- ensure slugs are unique when using soft deletes
## 2.1.7 - 2019-02-26
- Add support for Laravel 5.8
## 2.1.6 - 2018-02-14
- performance improvements
## 2.1.5 - 2018-01-10
- improve compatibility with json fields
## 2.1.4 - 2018-08-28
- add support for Laravel 5.7
## 2.1.3 - 2018-02-15
- fix for models with non incrementing primary keys
## 2.1.2 - 2018-02-08
- Support Laravel 5.6
## 2.1.1 - 2017-01-28
- improve compatibility with Lumen
## 2.1.0 - 2017-09-13
- add `usingLanguage`
## 2.0.0 - 2017-08-31
- add support for Laravel 5.5, drop support for all older versions of the framework
## 1.5.2 - 2018-05-08
- make compatible with PHP 7.2
## 1.5.1 - 2017-08-19
- fix bugs when using a custom separator
## 1.5.0 - 2017-04-13
- add `usingSeparator()`
## 1.4.1 - 2017-04-11
- ignore global scopes when determining a unique slug
## 1.4.0 - 2017-01-24
- add support for Laravel 5.4
## 1.3.0 - 2016-11-14
- add `doNotGenerateSlugsOnCreate` and `doNotGenerateSlugsOnUpdate`
## 1.2.0 - 2016-06-13
- Added the ability to generate slugs from a callable
## 1.1.0 - 2016-01-24
- Allow custom slugs
## 1.0.2 - 2016-01-12
- Fix bug when creating slugs from null values
## 1.0.1 - 2015-12-27
- Fix Postgres bug
## 1.0.0 - 2015-12-24
- Initial release
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,481 @@
# Generate slugs when saving Eloquent models
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-sluggable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-sluggable)
[![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-sluggable/run-tests.yml)](https://github.com/spatie/laravel-sluggable/actions)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-sluggable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-sluggable)
This package provides a trait that will generate a unique slug when saving any Eloquent model.
```php
$model = new EloquentModel();
$model->name = 'activerecord is awesome';
$model->save();
echo $model->slug; // outputs "activerecord-is-awesome"
```
The slugs are generated with Laravels `Str::slug` method, whereby spaces are converted to '-'.
Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-sluggable.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-sluggable)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Installation
You can install the package via composer:
```bash
composer require spatie/laravel-sluggable
```
## Usage
Your Eloquent models should use the `Spatie\Sluggable\HasSlug` trait and the `Spatie\Sluggable\SlugOptions` class.
The trait contains an abstract method `getSlugOptions()` that you must implement yourself.
Your models' migrations should have a field to save the generated slug to.
Here's an example of how to implement the trait:
```php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Illuminate\Database\Eloquent\Model;
class YourEloquentModel extends Model
{
use HasSlug;
/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
}
```
With its migration:
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateYourEloquentModelTable extends Migration
{
public function up()
{
Schema::create('your_eloquent_models', function (Blueprint $table) {
$table->increments('id');
$table->string('slug'); // Field name same as your `saveSlugsTo`
$table->string('name');
$table->timestamps();
});
}
}
```
### Using slugs in routes
To use the generated slug in routes, remember to use Laravel's [implicit route model binding](https://laravel.com/docs/routing#implicit-binding):
```php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Illuminate\Database\Eloquent\Model;
class YourEloquentModel extends Model
{
use HasSlug;
/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
}
```
### Using multiple fields to create the slug
Want to use multiple field as the basis for a slug? No problem!
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['first_name', 'last_name'])
->saveSlugsTo('slug');
}
```
### Customizing slug generation
You can also pass a `callable` to `generateSlugsFrom`.
By default the package will generate unique slugs by appending '-' and a number, to a slug that already exists.
You can disable this behaviour by calling `allowDuplicateSlugs`.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->allowDuplicateSlugs();
}
```
### Limiting the length of a slug
You can also put a maximum size limit on the created slug:
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->slugsShouldBeNoLongerThan(50);
}
```
The slug may be slightly longer than the value specified, due to the suffix which is added to make it unique.
You can also use a custom separator by calling `usingSeparator`
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->usingSeparator('_');
}
```
### Setting the slug language
To set the language used by `Str::slug` you may call `usingLanguage`
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->usingLanguage('nl');
}
```
### Overriding slugs
You can also override the generated slug just by setting it to another value than the generated slug.
```php
$model = EloquentModel::create(['name' => 'my name']); //slug is now "my-name";
$model->slug = 'my-custom-url';
$model->save(); //slug is now "my-custom-url";
```
## Prevents slugs from being generated on some conditions
If you don't want to create the slug when the model has a state, you can use the `skipGenerateWhen` function.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->skipGenerateWhen(fn () => $this->state === 'draft');
}
```
### Prevent slugs from being generated on creation
If you don't want to create the slug when the model is initially created you can set use the `doNotGenerateSlugsOnCreate()` function.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->doNotGenerateSlugsOnCreate();
}
```
### Prevent slug updates
Similarly, if you want to prevent the slug from being updated on model updates, call `doNotGenerateSlugsOnUpdate()`.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->doNotGenerateSlugsOnUpdate();
}
```
This can be helpful for creating permalinks that don't change until you explicitly want it to.
```php
$model = EloquentModel::create(['name' => 'my name']); //slug is now "my-name";
$model->save();
$model->name = 'changed name';
$model->save(); //slug stays "my-name"
```
### Regenerating slugs
If you want to explicitly update the slug on the model you can call `generateSlug()` on your model at any time to make the slug according to your other options. Don't forget to `save()` the model to persist the update to your database.
### Preventing overwrites
You can prevent slugs from being overwritten.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->preventOverwrite();
}
```
### Using scopes
If you have a global scope that should be taken into account, you can define this as well with `extraScope`. For example if you have a pages table containing pages of multiple websites and every website has it's own unique slugs.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->extraScope(fn ($builder) => $builder->where('scope_id', $this->scope_id));
}
```
### Setting the slug suffix starting index
By default, suffix index starts from 1, you can set starting number.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->startSlugSuffixFrom(2);
}
```
### Generating slug suffix on first occurrence
With the default behavior (assuming that we haven't disabled slug uniqueness with `allowDuplicateSlugs`), the generated slugs for two records with the same source values would be `this-is-an-example` and `this-is-an-example-1`.
When using this option, we are forcing the first occurence to also have a suffix so, even if the slug is unique as it is, it will be suffixed, resulting in `this-is-an-example-1` and `this-is-an-example-2`.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->useSuffixOnFirstOccurrence();
}
```
### Generating a custom slug suffix
By default, the mechanism to make slugs unique is to append an autoincremental value to the slug. You can generate a custom slug suffix such as a random string or hash with `usingSuffixGenerator`.
It accepts a callable that receives the base slug (without any suffix) and the iteration number, which represents how many times the suffix generation process has been run to ensure uniqueness. This number could be useful to monitor the collision rate of the generation process.
```php
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->usingSuffixGenerator(
fn(string $slug, int $iteration) => bin2hex(random_bytes(4))
); // Sample dummy method to generate a random hex code of length 8
}
```
### Integration with laravel-translatable
You can use this package along with [laravel-translatable](https://github.com/spatie/laravel-translatable) to generate a slug for each locale. Instead of using the `HasSlug` trait, you must use the `HasTranslatableSlug` trait, and add the name of the slug field to the `$translatable` array. For slugs that are generated from a single field _or_ multiple fields, you don't have to change anything else.
```php
namespace App;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;
use Illuminate\Database\Eloquent\Model;
class YourEloquentModel extends Model
{
use HasTranslations, HasTranslatableSlug;
public $translatable = ['name', 'slug'];
/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
}
```
For slugs that are generated from a callable, you need to instantiate the `SlugOptions` with the `createWithLocales` method. The callable now takes two arguments instead of one. Both the `$model` and the `$locale` are available to generate a slug from.
```php
namespace App;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;
use Illuminate\Database\Eloquent\Model;
class YourEloquentModel extends Model
{
use HasTranslations, HasTranslatableSlug;
public $translatable = ['name', 'slug'];
/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::createWithLocales(['en', 'nl'])
->generateSlugsFrom(function($model, $locale) {
return "{$locale} {$model->id}";
})
->saveSlugsTo('slug');
}
}
```
#### Implicit route model binding
You can also use Laravels [implicit route model binding](https://laravel.com/docs/routing#implicit-binding) inside your controller to automatically resolve the model. To use this feature, make sure that the slug column matches the `routeNameKey`.
Currently, only some database types support JSON operations. Further information about which databases support JSON can be found in the [Laravel docs](https://laravel.com/docs/queries#json-where-clauses).
```php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Illuminate\Database\Eloquent\Model;
class YourEloquentModel extends Model
{
use HasTranslations, HasTranslatableSlug;
public $translatable = ['name', 'slug'];
/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
}
```
### Find models by slug
For convenience, you can use the alias `findBySlug` to retrieve a model. The query will compare against the field passed to `saveSlugsTo` when defining the `SlugOptions`.
```php
$model = Article::findBySlug('my-article');
```
`findBySlug` also accepts a second parameter `$columns` just like the default Eloquent `find` method.
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Testing
``` bash
composer test
```
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security
If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
## Credits
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
@@ -0,0 +1,49 @@
{
"name": "spatie/laravel-sluggable",
"description": "Generate slugs when saving Eloquent models",
"license": "MIT",
"keywords": [
"spatie",
"laravel-sluggable"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"homepage": "https://github.com/spatie/laravel-sluggable",
"require": {
"php": "^8.0",
"illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0"
},
"require-dev": {
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.20|^2.0|^3.7",
"spatie/laravel-translatable": "^5.0|^6.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\Sluggable\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\Sluggable\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
"test": "vendor/bin/pest"
}
}
@@ -0,0 +1,23 @@
<?php
namespace Spatie\Sluggable\Exceptions;
use Exception;
class InvalidOption extends Exception
{
public static function missingFromField(): static
{
return new static('Could not determine which fields should be sluggified');
}
public static function missingSlugField(): static
{
return new static('Could not determine in which field the slug should be saved');
}
public static function invalidMaximumLength(): static
{
return new static('Maximum length should be greater than zero');
}
}
@@ -0,0 +1,232 @@
<?php
namespace Spatie\Sluggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Spatie\Sluggable\Exceptions\InvalidOption;
trait HasSlug
{
protected SlugOptions $slugOptions;
abstract public function getSlugOptions(): SlugOptions;
protected static function bootHasSlug(): void
{
static::creating(function (Model $model) {
$model->generateSlugOnCreate();
});
static::updating(function (Model $model) {
$model->generateSlugOnUpdate();
});
}
protected function generateSlugOnCreate(): void
{
$this->slugOptions = $this->getSlugOptions();
if ($this->slugOptions->skipGenerate) {
return;
}
if (! $this->slugOptions->generateSlugsOnCreate) {
return;
}
if ($this->slugOptions->preventOverwrite) {
if ($this->{$this->slugOptions->slugField} !== null) {
return;
}
}
$this->addSlug();
}
protected function generateSlugOnUpdate(): void
{
$this->slugOptions = $this->getSlugOptions();
if ($this->slugOptions->skipGenerate) {
return;
}
if (! $this->slugOptions->generateSlugsOnUpdate) {
return;
}
if ($this->slugOptions->preventOverwrite) {
if ($this->{$this->slugOptions->slugField} !== null) {
return;
}
}
$this->addSlug();
}
public function generateSlug(): void
{
$this->slugOptions = $this->getSlugOptions();
$this->addSlug();
}
protected function addSlug(): void
{
$this->ensureValidSlugOptions();
$slug = $this->generateNonUniqueSlug();
if ($this->slugOptions->generateUniqueSlugs) {
$slug = $this->makeSlugUnique($slug);
}
$slugField = $this->slugOptions->slugField;
$this->$slugField = $slug;
}
protected function generateNonUniqueSlug(): string
{
$slugField = $this->slugOptions->slugField;
if ($this->hasCustomSlugBeenUsed() && ! empty($this->$slugField)) {
return $this->$slugField;
}
return Str::slug($this->getSlugSourceString(), $this->slugOptions->slugSeparator, $this->slugOptions->slugLanguage);
}
protected function hasCustomSlugBeenUsed(): bool
{
$slugField = $this->slugOptions->slugField;
return $this->getOriginal($slugField) != $this->$slugField;
}
protected function getSlugSourceString(): string
{
if (is_callable($this->slugOptions->generateSlugFrom)) {
$slugSourceString = $this->getSlugSourceStringFromCallable();
return $this->generateSubstring($slugSourceString);
}
$slugSourceString = collect($this->slugOptions->generateSlugFrom)
->map(fn (string $fieldName): string => data_get($this, $fieldName, ''))
->implode($this->slugOptions->slugSeparator);
return $this->generateSubstring($slugSourceString);
}
protected function getSlugSourceStringFromCallable(): string
{
return call_user_func($this->slugOptions->generateSlugFrom, $this);
}
protected function makeSlugUnique(string $slug): string
{
$originalSlug = $slug;
$iteration = 0;
while (
$slug === '' ||
$this->otherRecordExistsWithSlug($slug) ||
($this->slugOptions->useSuffixOnFirstOccurrence && $iteration === 0)
) {
$suffix = $this->generateSuffix($originalSlug, $iteration++);
$slug = $originalSlug . $this->slugOptions->slugSeparator . $suffix;
}
return $slug;
}
protected function generateSuffix(string $originalSlug, int $iteration): string
{
if ($this->slugOptions->suffixGenerator) {
return call_user_func($this->slugOptions->suffixGenerator, $originalSlug, $iteration);
}
return strval($this->slugOptions->startSlugSuffixFrom + $iteration);
}
protected function otherRecordExistsWithSlug(string $slug): bool
{
$query = static::where($this->slugOptions->slugField, $slug)
->withoutGlobalScopes();
if ($this->slugOptions->extraScopeCallback) {
$query->where($this->slugOptions->extraScopeCallback);
}
if ($this->exists) {
$query->where($this->getKeyName(), '!=', $this->getKey());
}
if ($this->usesSoftDeletes()) {
$query->withTrashed();
}
return $query->exists();
}
protected function usesSoftDeletes(): bool
{
return in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($this), true);
}
protected function ensureValidSlugOptions(): void
{
if (is_array($this->slugOptions->generateSlugFrom) && ! count($this->slugOptions->generateSlugFrom)) {
throw InvalidOption::missingFromField();
}
if (! strlen($this->slugOptions->slugField)) {
throw InvalidOption::missingSlugField();
}
if ($this->slugOptions->maximumLength <= 0) {
throw InvalidOption::invalidMaximumLength();
}
}
/**
* Helper function to handle multi-bytes strings if the module mb_substr is present,
* default to substr otherwise.
*/
protected function generateSubstring($slugSourceString)
{
if (function_exists('mb_substr')) {
return mb_substr($slugSourceString, 0, $this->slugOptions->maximumLength);
}
return substr($slugSourceString, 0, $this->slugOptions->maximumLength);
}
public static function findBySlug(string $slug, array $columns = ['*'], ?callable $additionalQuery = null)
{
$modelInstance = new static();
$field = $modelInstance->getSlugOptions()->slugField;
$query = static::query();
if (in_array(HasTranslatableSlug::class, class_uses_recursive(static::class))) {
$currentLocale = $modelInstance->getLocale();
$fallbackLocale = config('app.fallback_locale');
$currentField = "{$field}->{$currentLocale}";
$fallbackField = "{$field}->{$fallbackLocale}";
$query->where(fn ($query) => $query->where($currentField, $slug)->orWhere($fallbackField, $slug));
} else {
$query->where($field, $slug);
}
if (is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->first($columns);
}
}
@@ -0,0 +1,141 @@
<?php
namespace Spatie\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Localizable;
trait HasTranslatableSlug
{
use HasSlug;
use Localizable;
protected function getLocalesForSlug(): Collection
{
$generateSlugFrom = $this->slugOptions->generateSlugFrom;
if (is_callable($generateSlugFrom)) {
// returns a collection of locales that were given to the SlugOptions object
// when it was instantiated with the 'createWithLocales' method.
return Collection::make($this->slugOptions->translatableLocales);
}
// collects all locales for all translatable fields
return Collection::wrap($generateSlugFrom)
->filter(fn ($fieldName) => $this->isTranslatableAttribute($fieldName))
->flatMap(fn ($fieldName) => $this->getTranslatedLocales($fieldName));
}
protected function addSlug(): void
{
$this->ensureValidSlugOptions();
$this->getLocalesForSlug()->unique()->each(function ($locale) {
$this->withLocale($locale, function () use ($locale) {
$slug = $this->generateNonUniqueSlug();
$slugField = $this->slugOptions->slugField;
if ($this->slugOptions->generateUniqueSlugs) {
// temporarly change the 'slugField' of the SlugOptions
// so the 'otherRecordExistsWithSlug' method queries
// the locale JSON column instead of the 'slugField'.
$this->slugOptions->saveSlugsTo("{$slugField}->{$locale}");
$slug = $this->makeSlugUnique($slug);
// revert the change for the next iteration
$this->slugOptions->saveSlugsTo($slugField);
}
$this->setTranslation($slugField, $locale, $slug);
});
});
}
protected function generateNonUniqueSlug(): string
{
$slugField = $this->slugOptions->slugField;
$slugString = $this->getSlugSourceString();
$slug = $this->getTranslations($slugField)[$this->getLocale()] ?? null;
$slugGeneratedFromCallable = is_callable($this->slugOptions->generateSlugFrom);
$hasCustomSlug = $this->hasCustomSlugBeenUsed() && ! empty($slug);
$hasNonChangedCustomSlug = ! $slugGeneratedFromCallable && ! empty($slug) && ! $this->slugIsBasedOnTitle();
if ($hasCustomSlug || $hasNonChangedCustomSlug) {
$slugString = $slug;
}
return Str::slug($slugString, $this->slugOptions->slugSeparator, $this->slugOptions->slugLanguage);
}
protected function getSlugSourceStringFromCallable(): string
{
return call_user_func($this->slugOptions->generateSlugFrom, $this, $this->getLocale());
}
protected function slugIsBasedOnTitle(): bool
{
$slugField = $this->slugOptions->slugField;
$titleSlug = Str::slug($this->getOriginalSourceString(), $this->slugOptions->slugSeparator, $this->slugOptions->slugLanguage);
$currentSlug = $this->getTranslations($slugField)[$this->getLocale()] ?? null;
if (! str_starts_with($currentSlug, $titleSlug) || $titleSlug === '') {
return false;
}
if ($titleSlug === $currentSlug) {
return true;
}
$slugSeparator = $currentSlug[strlen($titleSlug)];
$slugIdentifier = substr($currentSlug, strlen($titleSlug) + 1);
return $slugSeparator === $this->slugOptions->slugSeparator && is_numeric($slugIdentifier);
}
protected function getOriginalSourceString(): string
{
if (is_callable($this->slugOptions->generateSlugFrom)) {
$slugSourceString = $this->getSlugSourceStringFromCallable();
return $this->generateSubstring($slugSourceString);
}
$slugSourceString = collect($this->slugOptions->generateSlugFrom)
->map(fn (string $fieldName): string => $this->getOriginal($fieldName)[$this->getLocale()] ?? '')
->implode($this->slugOptions->slugSeparator);
return $this->generateSubstring($slugSourceString);
}
protected function hasCustomSlugBeenUsed(): bool
{
$slugField = $this->slugOptions->slugField;
$originalSlug = $this->getOriginal($slugField)[$this->getLocale()] ?? null;
$newSlug = $this->getTranslations($slugField)[$this->getLocale()] ?? null;
return $originalSlug !== $newSlug;
}
public function resolveRouteBindingQuery($query, $value, $field = null): Builder|Relation
{
$field = $field ?? $this->getRouteKeyName();
$slug = $this->getSlugOptions()->slugField;
if (str_contains($field, '.') && str_ends_with($field, ".{$slug}")) {
return $query->where("{$field}->{$this->getLocale()}", $value);
}
if ($field === $slug) {
return $query->where("{$field}->{$this->getLocale()}", $value);
}
return parent::resolveRouteBindingQuery($query, $value, $field);
}
}
@@ -0,0 +1,159 @@
<?php
namespace Spatie\Sluggable;
class SlugOptions
{
/** @var array|callable */
public $generateSlugFrom;
/** @var callable */
public $extraScopeCallback;
/** @var (callable(string, int): string)|null */
public $suffixGenerator;
public string $slugField;
public bool $generateUniqueSlugs = true;
public int $maximumLength = 250;
public bool $skipGenerate = false;
public bool $generateSlugsOnCreate = true;
public bool $generateSlugsOnUpdate = true;
public bool $preventOverwrite = false;
public string $slugSeparator = '-';
public string $slugLanguage = 'en';
public array $translatableLocales = [];
public int $startSlugSuffixFrom = 1;
public bool $useSuffixOnFirstOccurrence = false;
public static function create(): static
{
return new static();
}
public static function createWithLocales(array $locales): static
{
$slugOptions = static::create();
$slugOptions->translatableLocales = $locales;
return $slugOptions;
}
public function generateSlugsFrom(string | array | callable $fieldName): self
{
if (is_string($fieldName)) {
$fieldName = [$fieldName];
}
$this->generateSlugFrom = $fieldName;
return $this;
}
public function saveSlugsTo(string $fieldName): self
{
$this->slugField = $fieldName;
return $this;
}
public function allowDuplicateSlugs(): self
{
$this->generateUniqueSlugs = false;
return $this;
}
public function slugsShouldBeNoLongerThan(int $maximumLength): self
{
$this->maximumLength = $maximumLength;
return $this;
}
public function skipGenerateWhen(callable $callable): self
{
$this->skipGenerate = $callable() === true;
return $this;
}
public function doNotGenerateSlugsOnCreate(): self
{
$this->generateSlugsOnCreate = false;
return $this;
}
public function doNotGenerateSlugsOnUpdate(): self
{
$this->generateSlugsOnUpdate = false;
return $this;
}
public function preventOverwrite(): self
{
$this->preventOverwrite = true;
return $this;
}
public function usingSeparator(string $separator): self
{
$this->slugSeparator = $separator;
return $this;
}
public function usingLanguage(string $language): self
{
$this->slugLanguage = $language;
return $this;
}
public function extraScope(callable $callbackMethod): self
{
$this->extraScopeCallback = $callbackMethod;
return $this;
}
public function startSlugSuffixFrom(int $startSlugSuffixFrom): self
{
$this->startSlugSuffixFrom = max(1, $startSlugSuffixFrom);
return $this;
}
public function useSuffixOnFirstOccurrence(): self
{
$this->useSuffixOnFirstOccurrence = true;
return $this;
}
/**
* @param callable(string $slug, int $iteration): string $generator
*/
public function usingSuffixGenerator(callable $generator): self
{
$this->suffixGenerator = $generator;
return $this;
}
}