cviebrock/eloquent-sluggable

在 Laravel 中轻松为 Eloquent 模型创建短标题

资助包维护!
cviebrock

安装次数: 10 092 685

依赖者: 301

建议者: 2

安全: 0

星标: 3 883

关注者: 94

分支: 461

开放问题: 3


README

在 Laravel 中轻松为 Eloquent 模型创建短标题。

注意:以下说明适用于 Laravel 最新版本。
如果您使用的是较旧版本,请安装与您的 Laravel 版本相对应的包版本。

Build Status Total Downloads Latest Stable Version Latest Unstable Version SensioLabsInsight License

背景:什么是短标题?

短标题是字符串的简化版本,通常是 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.mdUPGRADING.md,并确保您阅读的是您版本的正确README.md(GitHub默认在master分支中显示版本,这可能不是您想要的)。

  1. 通过Composer安装包

    composer require cviebrock/eloquent-sluggable

    该包将自动注册其服务提供者。

  2. 可选的,如果您想更改任何默认设置,则发布配置文件

    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。您可以使用slug或任何其他您想要的适当名称;您的配置数组将确定数据将存储在哪个字段。您需要通过自己的迁移手动添加该列(该列应为NULLABLE)。

就是这样...您的模型现在是“可生成slug的”

使用方法

保存模型很简单

$post = Post::create([
    'title' => 'My Awesome Blog Post',
]);

检索slug同样简单

echo $post->slug;

注意:如果您正在使用Eloquent的replicate()方法复制模型,则包会在之后自动重新生成slug以确保唯一性。

$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"

注意:空字符串、非字符串或其他“奇怪”的源值将产生不同的slug

(上述值将受到任何唯一性或其他检查的影响。)

SlugService 类

生成slug的所有逻辑都由\Cviebrock\EloquentSluggable\Services\SlugService类处理。

通常,您不需要直接访问此类,尽管有一个静态方法可以用来为给定的字符串生成slug,而无需实际创建或保存相关模型。

use \Cviebrock\EloquentSluggable\Services\SlugService;

$slug = SlugService::createSlug(Post::class, 'slug', 'My First Post');

这对于Ajax控制器或类似情况很有用,您希望在创建模型之前先显示用户给定测试输入的唯一slug是什么。该方法的前两个参数是要测试的模型和slug字段,第三个参数是要用于测试slug的源字符串。

您还可以将可选的配置值数组作为第四个参数传递。这些值将覆盖要测试的slug字段的正常配置值。例如,如果您的模型配置为使用唯一slug,但您出于某种原因想要生成slug的“基础”版本,则可以这样做

$slug = SlugService::createSlug(Post::class, 'slug', 'My First Post', ['unique' => false]);

何时对模型进行短标题处理?

目前,模型是在Eloquent的saving事件上被生成slug的。这意味着slug是在将任何新数据写入数据库之前生成的。

对于新模型,这意味着主键尚未设置,因此不能作为slug源的一部分,例如

public function sluggable(): array
{
    return [
        'slug' => [
            'source' => ['title', 'id']
        ]
    ];
}

$model->id在模型保存之前是null。然而,挂钩到saving事件的优点是,我们只需要进行一次数据库查询即可保存所有模型数据,包括slug。

可选的,模型可以在Eloquent的saved事件上生成slug。
这意味着所有其他模型属性都已持久化到数据库,并且可以作为slug源使用。所以上述配置是可行的。唯一的缺点是,将模型保存到数据库需要多进行一次查询:第一次保存所有非slug字段,然后第二次更新仅slug字段。

这种行为是一个破坏性变更,可能不会影响大多数用户(除非您在模型的slug字段上进行了某些预保存验证)。我们认为这种行为的优点超过了缺点,因此这可能会成为包未来主要版本的新默认行为。尽管如此,为了使过渡更容易,您可以通过特质提供的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()方法中处理。该方法应返回一个索引数组,其中键表示存储缩略值字段的位置,值是该字段的配置。这意味着您可以为同一模型创建多个基于不同源字符串和不同配置选项的缩略名。

public function sluggable(): array
{
    return [
        'title-slug' => [
            'source' => 'title'
        ],
        'author-slug' => [
            'source' => ['author.lastname', 'author.firstname'],
            'separator' => '_'
        ],
    ];
}

这是构建缩略名的字段或字段数组。每个$model->field都通过空格分隔连接起来,以构建可缩略字符串。这些可以是模型属性(即数据库中的字段)、关系属性或自定义获取器。

要引用相关模型中的字段,请使用点表示法。例如,以下书籍的缩略名将来自其作者的名字和书籍的标题。

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()的值作为生成缩略名的源。

方法

定义将可缩略字符串转换为缩略名的方法。此配置有三个可能的选项。

  1. method为null(默认设置)时,该包使用默认的缩略引擎-- cocur/slugify --来创建缩略名。

  2. method是一个可调用函数时,则使用该函数或类方法。函数/方法应期望两个参数:要处理的字符串和分隔字符串。例如,要使用Laravel的Str::slug,您可以这样做:

'method' => ['Illuminate\\Support\\Str', 'slug'],
  1. 您还可以将method定义为闭包(再次,期望两个参数)
'method' => static function(string $string, string $separator): string {
    return strtolower(preg_replace('/[^a-z]+/i', $separator, $string));
},

method的任何其他值都会抛出异常。

对于更复杂的缩略需求,请参阅下面的扩展Sluggable

onUpdate

默认情况下,更新模型不会尝试生成新的缩略名值。假设一旦生成缩略名,您就不希望它改变(如果您使用缩略名作为URL,并且不想破坏SEO效果,这一点尤为重要)。

如果您想重新生成一个或多个模型缩略字段,在更新之前将这些字段设置为null或空字符串。

$post->slug = null;
$post->update(['title' => 'My New Title']);

如果您希望在每次更新模型时都执行此行为,则将onUpdate选项设置为true。

分隔符

这定义了构建缩略名时使用的分隔符,并将其传递给上面定义的method。默认值是连字符。

唯一

这是一个布尔值,用于定义给定类型的所有模型中是否存在唯一的slug。例如,如果您有两篇博客文章,并且它们的标题都是“我的博客文章”,那么如果unique为false,它们都会转换为“my-blog-post”。这可能会成为一个问题,例如,如果您在URL中使用slug。

unique设置为true后,第二个Post模型将转换为“my-blog-post-1”。如果存在标题相同的第三篇文章,它将转换为“my-blog-post-2”,依此类推。每个后续模型将在slug的末尾附加一个增量值,以确保唯一性。

唯一后缀

如果您想使用不同的唯一性识别方式(而不仅仅是自增整数),可以将uniqueSuffix配置设置为函数或可调用对象,以为您生成“唯一”值。

该函数应接受四个参数

  1. 基本slug(即非唯一slug)
  2. 分隔字符串
  3. 所有以相同slug开头的其他slug的\Illuminate\Support\Collection
  4. 要使用的第一个后缀(对于第一个需要变为唯一的slug)您可以根据需要创建一个新的后缀,该后缀未被集合中的任何slug使用。例如,如果您想使用字母而不是数字作为后缀,这是一种实现方法
'uniqueSuffix' => static function(string $slug, string $separator, Collection $list, $firstSuffix): string
    {
      $size = count($list);

      return chr($size + 96);
    }

第一个唯一后缀

添加唯一后缀时,我们从“2”开始计数,因此生成的slug列表看起来可能如下所示

  • my-unique-slug
  • my-unique-slug-2
  • my-unique-slug-3
  • 等等。

如果您想从不同的数字开始(或将不同的值传递到上面的自定义uniqueSuffix函数中),则可以在这里定义。

注意:该软件包的早期版本从唯一后缀1开始。在8.0.5版本中,这被更改为2,因为它是附加到第二个slug的更“直观”的后缀值。

包括已删除

将此设置为true还会在尝试强制唯一性时检查已删除的模型。这仅影响使用软删除功能的Eloquent模型。默认值为false,因此软删除的模型在检查唯一性时不计入。

保留

一个不允许作为slug的值的数组,例如,以防止与现有路由或控制器方法冲突等。这可以是一个数组,或者是一个返回数组的闭包。默认值为null:没有保留的slug名称。

最大长度

将此设置为正整数将确保生成的slug被限制在最大长度内(例如,以确保它们适合您的数据库字段)。默认值为null,不强制执行限制。

注意:如果启用了unique(默认情况下是启用的),并且您预计会有几个具有相同slug的模型,那么您应该将此值设置为比您的数据库字段长度少几个字符。原因在于,该类将向后续模型附加“-2”,“-3”,“-4”等,以保持唯一性。这些增量扩展不计入maxLength计算的一部分。

保留单词最大长度

如果您正在使用maxLength设置截断slug,那么您可能想确保您的slug不会在单词中间被截断。例如,如果您的源字符串是“我的第一篇文章”,并且您的maxLength是10,生成的slug将变成“my-first-p”,这不是理想的情况。

默认情况下,maxLengthKeepWords值设置为true,这将从slug的末尾截断部分单词,得到“my-first”而不是“my-first-p”。

如果您想保留部分单词,则将此配置设置为false。

短标题引擎选项

method为null(默认设置)时,该包使用默认的slugify引擎 -- cocur/slugify -- 来创建slug。如果您想在实例化引擎时向Slugify构造函数传递一组自定义选项,请在这里定义。有关这些选项的详细信息,请参阅Slugify文档。此外,请查看customizeSlugEngine以了解其他自定义Slugify进行slug的方法。

简短配置

如果您真的很懒,该包支持非常简短的配置语法。

public function sluggable(): array
{
    return ['slug'];
}

这将使用来自config/sluggable.php的所有默认选项,使用模型的__toString()方法作为源,并将slug存储在slug字段中。

扩展 Sluggable

有时配置选项不足以满足复杂需求(例如,可能需要考虑其他属性的唯一性测试)。

在这些情况下,该包提供了对slugify工作流程的钩子,您可以在其中使用自己的函数,无论是在每个模型的基础上,还是在扩展包的特质的自定义特质中。

注意:如果您将这些方法放入自己的特质中,您需要在模型中指明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;
        }

    // ...
}

定制短标题引擎

/**
 * @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引擎可以在slug发生之前进行自定义。这可能是在此更改使用的字符映射或修改语言文件等。

您可以在每个模型和每个属性的基础上自定义引擎(也许您的模型有两个slug字段,其中一个需要自定义)。

请查看tests/Models/PostWithCustomEngine.php以获取示例。

此外,请查看slugEngineOptions配置以了解其他自定义Slugify的方法。

带有唯一短标题约束的作用域

/**
 * @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
{
    // ...
}

此方法应用于确定给定slug是否唯一的查询。传递给作用域的参数包括

  • $model -- 正在slug的对象
  • $attribute -- 正在生成的slug字段
  • $config -- 给定模型和属性的配置数组
  • $slug -- "基本" slug(在应用任何唯一后缀之前)

您可以在查询作用域中自由使用这些值。例如,请查看tests/Models/PostWithUniqueSlugConstraints.php,其中slug是从标题生成的,但slug的作用域是作者。因此,Bob可以有一篇与Pam的帖子相同的标题的帖子,但两者将具有相同的slug。

查找相似短标题的作用域

/**
 * 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
{
    // ...
}

这是查找模型的"相似"slugs的默认作用域。基本上,该包查找与$slug参数相同的现有slugs,或者以$slug加上分隔符字符串开头的slugs。结果是传递给uniqueSuffix处理器的集合。

通常,此查询作用域(在Sluggable特质中定义)应保持不变。然而,您可以在模型中重载它。

SluggableScopeHelpers 特性

将可选的SluggableScopeHelpers特质添加到您的模型中允许您使用模型及其slugs。例如

$post = Post::whereSlug($slugString)->get();

$post = Post::findBySlug($slugString);

$post = Post::findBySlugOrFail($slugString);

因为模型可以有多于一个的slug,所以这需要更多的配置。有关所有详细信息,请参阅SCOPE-HELPERS.md

路由模型绑定

有关详细信息,请参阅ROUTE-MODEL-BINDING.md

错误、建议、贡献和支持

感谢所有为这个项目做出贡献的

请使用 GitHub 报告错误,以及发表评论或建议。

有关如何贡献更改,请参阅 CONTRIBUTING.md

版权和许可证

eloquent-sluggableColin Viebrock 编写,并遵循 MIT 许可协议 发布。

版权所有 (c) 2013 Colin Viebrock