返回

Laravel 5.8 中生成唯一 Slug 的多种方法及最佳实践

mysql

Laravel 5.8 中处理重复标题并生成唯一 Slug 的方法

遇到过标题相同,但需要生成唯一 Slug 的情况吗? 这篇博客就来聊聊在 Laravel 5.8 中如何解决这个问题。

问题

直接看问题:如果直接使用 Str::slug($postTitle, '-') ,标题一样的话,Slug 也会一样。我们想要的效果是,即使标题相同,Slug 也要是唯一的,比如:

原因分析

根本原因是Str::slug()函数只会根据标题生成一个基本的 Slug。 如果存在相同的标题, 那么就会有相同的 URL, 这在数据库层面(如果 slug 字段设置了唯一索引) 和 SEO 角度来看,都是有问题的。

解决方案

有好几种办法可以解决这个问题,咱们一个个来看。

1. 使用循环 + 计数器

这是最直接也最容易理解的方法。基本思路是:

  1. 先用 Str::slug() 生成一个基础的 Slug。
  2. 检查数据库里有没有一样的 Slug。
  3. 如果有,就在 Slug 后面加上一个计数器(-1,-2,-3 这种)。
  4. 循环这个过程,直到找到一个唯一的 Slug。

原理: 通过递增计数器来保证 Slug 的唯一性。

代码示例:

use Illuminate\Support\Str;
use App\Post; // 假设你的 Post 模型是 App\Post

public function addPostDetails(Request $request)
{
    // ... 其他代码 ...

    $postTitle = $request->postTitle;
    $posturl = Str::slug($postTitle, '-');

    $count = 1;
    $originalSlug = $posturl; //保存原始slug

    while (Post::where('post_url', $posturl)->exists()) {
        $posturl = $originalSlug . '-' . $count; // 拼接计数
        $count++;
    }

    // ... 其他代码 ...

    $savepost = new Post;
    // ... 其他代码 ...
    $savepost->post_url = $posturl;
    $savepost->save();

    // ... 其他代码 ...
}

说明:

  • Post::where('post_url', $posturl)->exists() 这个用来检查数据库里是不是已经存在这个 Slug。
  • 用了一个 while 循环, 保证了最终生成的 $posturl 是唯一值。

2. 使用 UUID 或 随机字符串

另一种方法是在 Slug 中加入 UUID(Universally Unique Identifier)或者一个随机字符串。

原理: UUID 和随机字符串几乎不可能重复,所以能保证 Slug 的唯一性。

代码示例 (UUID):

use Illuminate\Support\Str;
use App\Post;

public function addPostDetails(Request $request)
{
    // ... 其他代码 ...

    $postTitle = $request->postTitle;
        $posturl = Str::slug($postTitle, '-') . '-' . Str::uuid();

    // ... 其他代码 ...
        $savepost = new Post;
    // ... 其他代码 ...

    $savepost->post_url = $posturl;
    $savepost->save();
    // ... 其他代码 ...
}

代码示例 (随机字符串):

use Illuminate\Support\Str;
use App\Post;

public function addPostDetails(Request $request)
{
    // ... 其他代码 ...

    $postTitle = $request->postTitle;
    $posturl = Str::slug($postTitle, '-') . '-' . Str::random(8); // 8 个字符的随机字符串

    // ... 其他代码 ...
    $savepost = new Post;
    // ... 其他代码 ...
    $savepost->post_url = $posturl;

    $savepost->save();
    // ... 其他代码 ...

}

说明:

  • Str::uuid() 生成一个 UUID。
  • Str::random(8) 生成一个包含 8 个随机字符的字符串。你可以根据需要调整长度。
  • 这种做法能有效保证slug唯一, 但可读性比较差, 用户或者搜索引擎不一定喜欢这样的url。

3. 循环 + 时间戳

第三种方案是把循环和时间戳结合。

原理: 时间戳是唯一的,把它加到 Slug 里能起到很好的区分作用。

代码示例:

use Illuminate\Support\Str;
use App\Post;
use Carbon\Carbon;

public function addPostDetails(Request $request)
{
    // ... 其他代码 ...

    $postTitle = $request->postTitle;
    $posturl = Str::slug($postTitle, '-');
     $originalSlug = $posturl;
    while (Post::where('post_url', $posturl)->exists()) {
           $posturl = $originalSlug. '-' . Carbon::now()->timestamp;
    }

    // ... 其他代码 ...
    $savepost = new Post;
      // ... 其他代码 ...
    $savepost->post_url = $posturl;
     $savepost->save();
     // ... 其他代码 ...
}

说明 :

  • 如果Str::slug()生成的 slug 不唯一, 会拼接上当前时间戳。
  • 虽然理论上在同一毫秒内提交相同的标题仍会导致冲突,但是发生的概率极低。如果确实发生了,可以通过修改数据库该字段的唯一索引来实现。

4. 使用第三方包 (推荐)

如果不想自己写太多代码,可以用一些现成的第三方包,比如 cviebrock/eloquent-sluggable

原理: 这个包提供了很多方便的功能,可以自动生成唯一的 Slug,而且支持自定义。

安装:

composer require cviebrock/eloquent-sluggable

使用:

  1. 在你的 Post 模型里引入 Sluggable trait:
// app/Post.php

use Illuminate\Database\Eloquent\Model;
use Cviebrock\EloquentSluggable\Sluggable;

class Post extends Model
{
    use Sluggable;

    public function sluggable(): array
    {
        return [
            'post_url' => [
                'source' => 'post_title'
            ]
        ];
    }

     // ...其他代码...
}
  1. 在你的 config/app.php 文件里(通常不需要,但如果遇到问题,可以尝试):

    // ...
    'providers' => [
    	// ...
     Cviebrock\EloquentSluggable\ServiceProvider::class,
     ],
    // ...
    
  2. 在数据库迁移文件里, 确保 post_url 字段是唯一的:

    // 假设你的表名是 posts
    Schema::table('posts', function (Blueprint $table) {
          $table->string('post_url')->unique();
    });
    
  3. 在控制器里, 不用手动生成 Slug, 只需要保存 post_title 就行:


public function addPostDetails(Request $request)
{
    // ... 其他代码 ...

    $savepost = new Post;
    $savepost->post_global_id = $postglobid;
    $savepost->post_title = $postTitle;
    $savepost->post_desc = $postDesc;
    $savepost->post_img = $postimgnm;
    $savepost->post_img_gal = json_encode($galImg);
    // $savepost->post_url = $posturl;  // 这行不需要了

    $savepost->save();
     // ... 其他代码 ...
}

说明:

  • 'source' => 'post_title' 告诉这个包用 post_title 字段来生成 Slug。
  • 这个包会自动处理重复的情况,生成唯一的 Slug。
  • eloquent-sluggable 还支持很多高级功能,比如自定义 Slug 生成规则,处理更新 Slug 等等,具体可以看它的文档。

进阶使用技巧(eloquent-sluggable):

  • 自定义 Slug 生成规则:

    // app/Post.php
        public function sluggable(): array
        {
            return [
                'post_url' => [
                    'source' => 'post_title',
                    'onUpdate' => true, //允许 title 更新时, 更新 slug. 默认是 false.
                      'unique' => true, //强制唯一.
                      'separator' => '-', // 更改分隔符
                     'maxLength'=> 200,  //最大长度
                     'method' => function ($string, $separator) {  // 完全自定义 slug 的生成
                         return 'prefix-' . Str::slug($string, $separator);
                        }
                ]
            ];
        }
    

    详细可以参考官方文档。

  • 手动触发slug生成 :

     $post = Post::find(1);
     $post->resluggify();
     $post->save();
    

安全建议:
无特殊安全建议,注意使用参数化查询或ORM以预防SQL注入即可。

总结

生成唯一 Slug 的方法很多,要根据自己的实际情况选择合适的方法. 如果图省事, 就直接上第三方包, 又快又好。 如果对URL美观度有要求,建议选择计数器或时间戳方式.