sgsm74 / eloquent-sluggable-plus
在 Laravel 中轻松为 Eloquent 模型创建别名
Requires
- php: ^8.2
- cocur/slugify: ^4.3
- illuminate/config: ^11.0
- illuminate/database: ^11.0
- laravel/framework: ^10.0
Requires (Dev)
- mockery/mockery: ^1.4.4
- orchestra/testbench: ^9.0
- pestphp/pest: ^2.28
This package is not auto-updated.
Last update: 2024-09-23 16:16:38 UTC
README
在 Laravel 中为 Eloquent 模型轻松创建别名。
注意:以下说明适用于 Laravel 的最新版本。
如果您使用的是旧版本,请安装与您的 Laravel 版本相对应的包版本。
- Eloquent-Sluggable
背景:什么是别名?
别名是字符串的简化版本,通常是 URL 友好的。对字符串进行“别名”操作通常涉及将其转换为统一的大小写,并删除任何非 URL 友好的字符(空格、重音符号、和号等)。生成的字符串可以用来标识特定的资源。
例如,如果您有一个带有文章的博客,您可以通过 ID 引用每篇文章
http://example.com/post/1
http://example.com/post/2
...但这不是很友好(尤其是对于SEO)。您可能希望使用文章的标题作为 URL,但如果文章标题是“我与安德烈和弗朗索瓦的晚餐”,这看起来也很丑
http://example.com/post/My+Dinner+With+Andr%C3%A9+%26+Fran%C3%A7ois
解决方案是为标题创建一个别名并使用它。您可能想使用 Laravel 内置的 Str::slug()
方法将标题转换为更友好的格式
http://example.com/post/my-dinner-with-andre-francois
这样的 URL 会让用户更满意(它可读,易于输入等)。
有关更多信息,您可能想阅读维基百科上的此描述。
别名通常也是唯一的。因此,如果您再写一篇标题相同的文章,您需要以某种方式区分它们,通常是在别名的末尾添加一个递增计数器
http://example.com/post/my-dinner-with-andre-francois
http://example.com/post/my-dinner-with-andre-francois-1
http://example.com/post/my-dinner-with-andre-francois-2
这样可以保持 URL 的唯一性。
Laravel 的 Eloquent-Sluggable 包旨在自动处理所有这些,而配置量最小。
安装
根据您的 Laravel 版本,您应该安装不同版本的包。
注意:从版本 6.0 开始,包的版本应与 Laravel 版本相匹配。
Laravel 旧版本可以使用包的旧版本,尽管它们不再受支持或维护。有关具体信息,请参阅 CHANGELOG.md 和 UPGRADING.md,并确保您正在阅读您版本的正确 README.md(GitHub 默认在 master 分支显示版本,这可能不是您想要的)。
-
通过 Composer 安装包
$ composer require cviebrock/eloquent-sluggable
该包将自动注册其服务提供者。
-
可选:如果您想更改任何默认设置,请发布配置文件
php artisan vendor:publish --provider="Cviebrock\EloquentSluggable\ServiceProvider"
更新您的 Eloquent 模型
您的模型应该使用Sluggable特性,该特性包含一个抽象方法sluggable()
,您需要对其进行定义。这是设置任何模型特定配置的地方(有关详细信息,请参阅下方的配置)
use Cviebrock\EloquentSluggable\Sluggable; class Post extends Model { use Sluggable; /** * Return the sluggable configuration array for this model. * * @return array */ public function sluggable(): array { return [ 'slug' => [ 'source' => 'title' ] ]; } }
当然,您的模型和数据库需要一列来存储别名。您可以使用slug
或您想要的任何其他适当名称;您的配置数组将确定数据将存储在哪个字段。您需要通过自己的迁移手动添加该列(应设置为NULLABLE
)。
就这样...您的模型现在是“可别名的”了!
用法
保存模型很简单
$post = Post::create([ 'title' => 'My Awesome Blog Post', ]);
获取别名也同样简单
echo $post->slug;
注意:如果您正在使用Eloquent的
replicate()
方法复制模型,则包将在之后自动重新生成别名以确保唯一性。
$post = Post::create([ 'title' => 'My Awesome Blog Post', ]); // $post->slug is "my-awesome-blog-post" $newPost = $post->replicate(); // $newPost->slug is "my-awesome-blog-post-1"
注意:空字符串、非字符串或其他“奇怪”的源值将生成不同的别名
(上述值将受到任何唯一或其他检查的影响。)
SlugService 类
生成别名的所有逻辑都由\Cviebrock\EloquentSluggable\Services\SlugService
类处理。
通常,您不需要直接访问此类,尽管有一个静态方法可以用于为给定的字符串生成别名,而无需创建或保存相关的模型。
use \Cviebrock\EloquentSluggable\Services\SlugService; $slug = SlugService::createSlug(Post::class, 'slug', 'My First Post');
这在对Ajax控制器或类似控制器有用的场景中很有用,在这些场景中,您想在创建模型之前显示用户给定测试输入的唯一别名。该方法的前两个参数是正在测试的模型和别名字段,第三个参数是用于测试别名的源字符串。
您还可以传递一个可选的配置值数组作为第四个参数。这些值将覆盖正在测试的别名字段的正常配置值。例如,如果您的模型配置为使用唯一别名,但您出于某种原因想生成别名的“基本”版本,则可以这样做
$slug = SlugService::createSlug(Post::class, 'slug', 'My First Post', ['unique' => false]);
何时对模型进行别名处理?
目前,模型在Eloquent的saving
事件上别名的。这意味着在将任何新数据写入数据库之前会生成别名。
对于新模型,这意味着主键尚未设置,因此不能作为别名源的一部分,例如
public function sluggable(): array { return [ 'slug' => [ 'source' => ['title', 'id'] ] ]; }
$model->id
在模型保存之前是null
。然而,将钩子连接到saving
事件的优点是我们只需要进行一次数据库查询即可保存模型的所有数据,包括别名。
可选的,模型可以在Eloquent的saved
事件上别名的。
这意味着所有其他模型属性都已持久化到数据库,并且是作为别名源可用的。因此,上述配置将起作用。唯一的缺点是,将模型保存到数据库需要额外的查询:第一个用于保存所有非别名字段,然后第二个用于只更新别名字段。
这种行为是一个破坏性变化,可能不会影响大多数用户(除非您在模型的别名字段上执行一些预保存验证)。我们认为优点超过了缺点,因此这可能会成为未来主要版本中包的新默认行为。尽管如此,为了使过渡更容易,您可以通过特性提供的sluggableEvent
方法配置此行为。
public function sluggableEvent(): string { /** * Default behaviour -- generate slug before model is saved. */ return SluggableObserver::SAVING; /** * Optional behaviour -- generate slug after model is saved. * This will likely become the new default in the next major release. */ return SluggableObserver::SAVED; }
请记住,如果您想将模型的主键用作别名的源字段,则需要使用SluggableObserver::SAVED
。
事件
可别名的模型将触发两个Eloquent模型事件:“slugging”和“slugged”。
“slugging”事件在生成别名之前触发。如果此事件的回调返回false
,则不会执行别名的生成。如果返回任何其他值,包括null
,则将执行别名的生成。
“slugged”事件在生成别名后触发。如果模型不需要别名的生成(由needsSlugging()
方法确定),则不会调用此事件。
您可以将钩子连接到这些事件之一,就像连接任何其他Eloquent模型事件一样
Post::registerModelEvent('slugging', static function($post) { if ($post->someCondition()) { // the model won't be slugged return false; } }); Post::registerModelEvent('slugged', static function($post) { Log::info('Post slugged: ' . $post->getSlug()); });
配置
配置被设计得尽可能灵活。您可以设置所有Eloquent模型的全局默认值,然后为单个模型覆盖这些设置。
默认情况下,全局配置设置在config/sluggable.php
文件中。如果没有设置配置,则使用包的默认值。以下是一个配置示例,显示了所有默认设置
return [ 'source' => null, 'method' => null, 'onUpdate' => false, 'separator' => '-', 'unique' => true, 'uniqueSuffix' => null, 'firstUniqueSuffix' => 2, 'includeTrashed' => false, 'reserved' => null, 'maxLength' => null, 'maxLengthKeepWords' => true, 'slugEngineOptions' => [], ];
对于单个模型,配置由您需要实现的sluggable()
方法处理。该方法应返回一个索引数组,其中键代表存储slug值的字段,值是该字段的配置。这意味着您可以根据不同的源字符串和不同的配置选项为同一模型创建多个slug。
public function sluggable(): array { return [ 'title-slug' => [ 'source' => 'title' ], 'author-slug' => [ 'source' => ['author.lastname', 'author.firstname'], 'separator' => '_' ], ]; }
source
这是构建slug的字段或字段数组。每个$model->field
通过空格分隔连接起来,以构建可缓存的字符串。这些可以是模型属性(即数据库中的字段)、关系属性或自定义获取器。
要引用相关模型的字段,请使用点表示法。例如,以下书籍的slug将从其作者的姓名和书籍的标题生成
class Book extends Eloquent { use Sluggable; protected $fillable = ['title']; public function sluggable(): array { return [ 'slug' => [ 'source' => ['author.name', 'title'] ] ]; } public function author(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(Author::class); } } ... class Author extends Eloquent { protected $fillable = ['name']; }
使用自定义获取器的示例
class Person extends Eloquent { use Sluggable; public function sluggable(): array { return [ 'slug' => [ 'source' => 'fullname' ] ]; } public function getFullnameAttribute(): string { return $this->firstname . ' ' . $this->lastname; } }
如果source
为空、false或null,则使用$model->__toString()
的值作为slug生成的源。
method
定义将可缓存的字符串转换为slug的方法。此配置有三个可能的选项
-
当
method
为null(默认设置)时,该包使用默认的slugging引擎 -- cocur/slugify -- 来创建slug。 -
当
method
是一个可调用的函数时,则使用该函数或类方法。该函数/方法应期望两个参数:要处理的字符串和分隔符字符串。例如,要使用Laravel的Str::slug
,您可以这样做
'method' => ['Illuminate\\Support\\Str', 'slug'],
- 您也可以将
method
定义为闭包(同样,期望两个参数)
'method' => static function(string $string, string $separator): string { return strtolower(preg_replace('/[^a-z]+/i', $separator, $string)); },
method
的任何其他值都将抛出异常。
有关更复杂的slugging要求,请参阅下面的扩展Sluggable。
onUpdate
默认情况下,更新模型时不会尝试生成新的slug值。假设一旦生成了slug,您就不希望它改变(这可能在您使用slug作为URL时尤其如此,并且不希望破坏您的SEO效果)。
如果您想重新生成模型的一个或多个slug字段,可以在更新之前将这些字段设置为null或空字符串
$post->slug = null; $post->update(['title' => 'My New Title']);
如果您希望在每次更新模型时都执行此行为,则将onUpdate
选项设置为true。
separator
这定义了构建slug时使用的分隔符,并传递给上面定义的method
。默认值是连字符。
unique
这是一个布尔值,定义slug是否应在给定类型的所有模型中唯一。例如,如果您有两个博客文章,并且它们的名称都是"My Blog Post",那么如果unique
为false,它们都将转换为"my-blog-post"。这可能会出现问题,例如,如果您在URL中使用slug。
将unique
设置为true,则第二个Post模型将转换为"my-blog-post-1"。如果有第三个具有相同标题的文章,它将转换为"my-blog-post-2",依此类推。每个后续模型将在slug的末尾附加增量值,以确保唯一性。
uniqueSuffix
如果您想使用不同的唯一标识方式(除了自增整数),可以将 uniqueSuffix
配置项设置为一个函数或可调用的对象,它会为您生成“唯一”值。
该函数应接受四个参数:
- 基本别名(即非唯一的别名)
- 分隔字符串
- 一个包含所有以相同别名开头的其他别名字符串的
\Illuminate\Support\Collection
- 要使用的第一个后缀(对于需要变为唯一的第一个别名)您可以为生成的新后缀做任何事情,以确保它没有被集合中的任何别名使用过。例如,如果您想使用字母而不是数字作为后缀,这是实现这一目标的一种方法
'uniqueSuffix' => static function(string $slug, string $separator, Collection $list, $firstSuffix): string { $size = count($list); return chr($size + 96); }
firstUniqueSuffix
添加唯一后缀时,我们从“2”开始计数,因此生成的别名列表将类似于
my-unique-slug
my-unique-slug-2
my-unique-slug-3
- 等等。
如果您想从不同的数字开始计数(或将不同的值传递给上面的自定义 uniqueSuffix
函数),则可以在此处定义它。
注意:该包的早期版本从唯一后缀
1
开始。从版本 8.0.5 开始,改为使用2
,因为这是一个更“直观”的后缀值,可以附加到第二个别名。
includeTrashed
将此设置为 true
还会在尝试强制唯一性时检查已删除的模型。这仅影响使用 软删除 特性的 Eloquent 模型。默认为 false
,因此软删除的模型在检查唯一性时不会被计算。
reserved
一个不允许作为别名的值的数组,例如,防止与现有路由或控制器方法等冲突。这可以是一个数组,也可以是一个返回数组的闭包。默认为 null
:没有保留的别名名称。
maxLength
将此设置为正整数将确保生成的别名被限制在最大长度内(例如,确保它们适合数据库字段)。默认值为 null
,不强制限制。
注意:如果启用了
unique
(默认情况下是启用的),并且您预计会有多个具有相同别名的模型,那么您应该将此值设置为数据库字段长度少几个字符。原因在于,类将追加 "-2","-3","-4",等等,以保持后续模型的唯一性。这些增量扩展不包括在maxLength
计算的部分中。
maxLengthKeepWords
如果您正在使用 maxLength
设置截断别名,那么您可能还想确保别名不会在单词的中间被截断。例如,如果您的源字符串是“我的第一篇帖子”,并且您的 maxLength
是 10,生成的别名将变成“my-first-p”,这并不理想。
默认情况下,maxLengthKeepWords
的值设置为 true,这将在别名的末尾删除部分单词,结果为“my-first”而不是“my-first-p”。
如果您想保留部分单词,则将此配置设置为 false。
slugEngineOptions
当 method
为 null(默认设置)时,该包使用默认的别名生成引擎 -- cocur/slugify -- 来创建别名。如果您想将自定义的选项集传递给当引擎实例化时的 Slugify 构造函数,您将在此处定义这些选项。有关 Slugify 的选项,请参阅 文档。还要查看 customizeSlugEngine 以获取其他自定义 Slugify 的方法。
简短配置
该包支持非常简洁的配置语法,如果您确实很懒的话。
public function sluggable(): array { return ['slug']; }
这将使用来自 config/sluggable.php
的所有默认选项,使用模型的 __toString()
方法作为源,并将缩略名存储在 slug
字段中。
扩展 Sluggable
有时配置选项不足以满足复杂需求(例如,唯一性测试可能需要考虑其他属性)。
在这种情况下,该包提供了对缩略名工作流程的钩子,您可以在每个模型的基础上或在自己的特质中扩展包的特质来使用自己的函数。
注意:如果您将这些方法放入自己的特质中,您需要在模型中指明 PHP 应该使用 您的 特质方法而不是包的方法(因为一个类不能使用具有相同方法的两个特质),例如:
/** * Your trait where you collect your common Sluggable extension methods */ class MySluggableTrait { public function customizeSlugEngine(...) {} public function scopeWithUniqueSlugConstraints(...) {} // etc. } /** * Your model */ class MyModel { // Tell PHP to use your methods instead of the packages: use Sluggable, MySluggableTrait { MySluggableTrait::customizeSlugEngine insteadof Sluggable; MySluggableTrait::scopeWithUniqueSlugConstraints insteadof Sluggable; } // ... }
customizeSlugEngine
/** * @param \Cocur\Slugify\Slugify $engine * @param string $attribute * @return \Cocur\Slugify\Slugify */ public function customizeSlugEngine(Slugify $engine, string $attribute): \Cocur\Slugify\Slugify { // ... return $engine; }
如果您扩展此方法,则可以在缩略名发生之前自定义 Slugify 引擎。这可能是在这里更改使用的字符映射或修改语言文件等的地方。
您可以在每个模型和每个属性的基础上自定义引擎(也许您的模型有两个缩略名字段,其中一个需要自定义)。
请查看 tests/Models/PostWithCustomEngine.php
以获取示例。
此外,请查看 slugEngineOptions 配置以了解其他自定义 Slugify 的方法。
scopeWithUniqueSlugConstraints
/** * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $model * @param string $attribute * @param array $config * @param string $slug * @return \Illuminate\Database\Eloquent\Builder */ public function scopeWithUniqueSlugConstraints( Builder $query, Model $model, string $attribute, array $config, string $slug ): Builder { // ... }
此方法应用于用于确定给定缩略名是否唯一的查询。传递给作用域的参数包括
$model
-- 正在缩略的对象$attribute
-- 正在生成的缩略名字段$config
-- 给定模型和属性的配置数组$slug
-- "基础"缩略名(在应用任何唯一后缀之前)
请随意在查询作用域中使用这些值。例如,请查看 tests/Models/PostWithUniqueSlugConstraints.php
,其中缩略名是从标题生成的,但缩略名是作用域到作者。因此,Bob 可以有一篇与 Pam 的帖子相同的标题的帖子,但两者的缩略名相同。
scopeFindSimilarSlugs
/** * Query scope for finding "similar" slugs, used to determine uniqueness. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $attribute * @param array $config * @param string $slug * @return \Illuminate\Database\Eloquent\Builder */ public function scopeFindSimilarSlugs(Builder $query, string $attribute, array $config, string $slug): Builder { // ... }
这是查找模型“类似”缩略名的默认作用域。基本上,该包寻找与 $slug
参数相同的现有缩略名,或以 $slug
加分隔符字符串开始的缩略名。结果集合是传递给 uniqueSuffix
处理器的集合。
通常,应该保持这个查询作用域(它在 Sluggable 特质中定义)不变。但是,您可以在模型中自由地覆盖它。
SluggableScopeHelpers 特性
将可选的 SluggableScopeHelpers
特质添加到您的模型中允许您处理模型及其缩略名。例如:
$post = Post::whereSlug($slugString)->get(); $post = Post::findBySlug($slugString); $post = Post::findBySlugOrFail($slugString);
因为模型可以有多个缩略名,这需要更多的配置。请参阅 SCOPE-HELPERS.md 了解所有详细信息。
路由模型绑定
请参阅 ROUTE-MODEL-BINDING.md 了解详细信息。
错误、建议、贡献和支持
感谢 所有 为此项目做出贡献的人!
请使用 GitHub 报告错误,发表评论或提出建议。
请参阅 CONTRIBUTING.md 了解如何贡献更改。
版权和许可
eloquent-sluggable 由 Colin Viebrock 编写,并使用 MIT 许可证 发布。
版权所有 (c) 2013 Colin Viebrock