Laravel - Tối ưu URL với Eloquent Sluggable

c998f361a19f7e051cdb3a2183f24bd620c74975.jpg

1. Tại sao nên tối ưu cấu trúc đường dẫn?

Việc tối ưu cấu trúc đường dẫn sẽ giúp bạn tạo ra các đường dẫn thân thiện (Friendly URLs) để các công cụ tìm kiếm dễ dàng đọc, lập chỉ mục, dễ nhớ và ngắn gọn ngoài ra còn có thể bao gồm từ khóa có trong nội dung của đường dẫn để tốt nhất cho việc SEO.

Ví dụ về đường dẫn thân thiện và không thân thiện:

Mặc định Laravel cũng đã giúp bạn để các đường dẫn trở nên dễ đọc và dễ nhìn như http://localhost.dev/post/10, http://localhost.dev/post/10/comments

Tuy nhiên đường dẫn như vậy thường sẽ không cung cấp nhiều thông tin cho cả người đọc và các công cụ tìm kiếm. Chẳng hạn con số 10 ở đây có ý nghĩa gì đối với người dùng? Các bạn đều biết đó là giá trị integer đại diện cho primary key của bản ghi được tìm thấy trong bảng “post” tuy nhiên nó sẽ có ý nghĩa hơn rất nhiều nếu sử dụng đường dẫn như sau: http://localhost.dev/post/duong-dan-than-thien-trong-laravel-5.

Các bạn có thể thấy được tác dụng của việc tối ưu cấu trúc đường dẫn, trong phần sau của ìa viết mình sẽ hướng dẫn các bạn cách tạo ra đường dẫn thân thiện trong Laravel 5.

2. Tối ưu URL với Eloquent Sluggable

Việc tạo đường dẫn thân thiện trong Laravel tương đối đơn giản dưới sự hỗ trợ của package eloquent-sluggable.

2.1. Cài đặt

CHÚ Ý: Tùy thuộc vào phiên bản laravel mà chúng ta cài đặt package với các version khác nhau

Laravel Version Sluggable Version
4.x 2.x
5.1, 5.2 4.0
5.1*, 5.2*, 5.3 4.1

Chúng ta có thể cài đặt package thông qua Composer

$ composer require cviebrock/eloquent-sluggable:^4.1

Sau đó, cập nhật config config/app.php

'providers' => [
    // ...
    Cviebrock\EloquentSluggable\ServiceProvider::class,
];

Sau đó, bạn sẽ cập nhật việc cấu hình bằng cách chạy lệnh php artisan vendor:publish trong console:

php artisan vendor:publish --provider="Cviebrock\EloquentSluggable\ServiceProvider"

2.2. Cập nhật Eloquent Models

Để sử dụng slug thì trong model cũng như database của bạn cần phải có collumn chứa nó, chúng ta có thể thêm vào bằng migration. Tiếp theo chúng ta cập nhật lại các models cần sử dụng slug. Nhữn models đó cần phải sử dụng Sluggable trait đồng thời phải khai báo phương thức sluggable(), phương thức này trả về 1 mảng chứa các config liên quan tới slug của model hiện tại. Các bạn xem thêm tại: Configuration

use Cviebrock\EloquentSluggable\Sluggable;

class Post extends Model
{
    use Sluggable;

    /**
     * Return the sluggable configuration array for this model.
     *
     * @return array
     */
    public function sluggable()
    {
        return [
            'slug' => [
                'source' => 'title'
            ]
        ];
    }

}

2.3. Sử dụng

Sau các bước trên, slug sẽ được tự động tạo mỗi khi chúng ta thực hiện phương thức save() của model:

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

$post->save();

Đê lấy slug:

echo $post->slug;

Nếu chúng ta thực hiện sao chép model với phương thức replicate() của Eloquent thì package sẽ tự động tạo lại slug cho giá trị được sao chép để đảm bảo slugunique.

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

$post->save();
// $post->slug is "my-awesome-blog-post"

$newPost = $post->replicate();
// $newPost->slug is "my-awesome-blog-post-1"

2.4. Class SlugService

Tất cả các xử lý logic về việc tạo slug đều được quản lý bởi class \Cviebrock\EloquentSluggable\Services\SlugService

Bạn có thể tạo ra slug mà không thực hiện việc tạo và lưu vào model bằng phương thức createSlug:

use \Cviebrock\EloquentSluggable\Services\SlugService;

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

Ngoài 3 tham số cơ bản trên, phương thức createSlug còn có thêm tham số thứ 4 là một array chứa config, ví dụ:

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

Với việc đặt unique = false, slug được tạo ra từ ví dụ trên sẽ ko là unique.

2.5. Events

Các model Sluggable sẽ thực thi 2 Eloquent events "slugging" and "slugged".

Event "slugging" được thực hiện trước khi slugs được tạo ra. Nếu hàm callback từ event này trả về là false thì slug sẽ không được tạo ra.

Event slugged được thực thi ngay sau slug được tạo ra. Nó sẽ không được gọi nếu model không cần slugging (bạn cần khai báo việc cần slugging hay không với phương thức needsSlugging()).

Post::registerModelEvent('slugging', function($post) {
    if ($post->someCondition()) {
        // the model won't be slugged
        return false;
    }
});

Post::registerModelEvent('slugged', function($post) {
    Log::info('Post slugged: ' . $post->getSlug());
});

2.6. Configuration

Configuration được thiết kế để trở lên linh hoạt nhất có thể. Bạn có thể cài đặt mặc định cho tất cả các models và thực hiện việc setting riêng cho từng models sau đó.

Mặc định, configuration tổng quát cho các models được lưu trong file app/config/sluggable.php. Nếu bạn chưa publish file sluggable.php thì package sẽ lấy dữ liệu từ vendor/cviebrock/eloquent-sluggable/resources/config/sluggable.php như là dữ liệu mặc định.

return [
    'source'          => null,
    'maxLength'       => null,
    'method'          => null,
    'separator'       => '-',
    'unique'          => true,
    'uniqueSuffix'    => null,
    'includeTrashed'  => false,
    'reserved'        => null,
];

Cho từng models, configuration được quản lý với phương thức sluggable(). Phương thức này sẽ trả về một danh sách chứa config cho model:

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

Chúng ta có thể tạo nhiều slugs cho cùng model, dựa trên các source khác nhau cùng với các options khác nhau, ví dụ:

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

2.7. SluggableScopeHelpers Trait

Bằng việc thêm SluggableScopeHelpers trait vào model của bạn, bạn có thể thực hiện một số phương thức như:

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

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

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

Xem thêm tại: SCOPE-HELPERS.md.

2.8. Route Model Binding

Như các bạn đã biết, kể từ laravel 5.2, Laravel đã bắt đầu hỗ trợ Model Binding trong Route.

2.8.1. Implicit Binding

Để thay đổi việc Implicit binding bằng id sang slug, chúng ta chỉ cần thêm phương thức getRouteKeyName()vào model và trả về tên của slug:

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

class Post extends Model
{
    use Sluggable;

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

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName()
    {
        return 'slug';
    }

}

Tiếp theo chúng ta set up routes tương tự như việc sử dụng id cho binding trong Eloquent documentation:

Route::get('api/posts/{post}', function (App\Post $post) {
    return $post->title;
});

Nếu bạn sử dụng SluggableScopeHelpers trait, bạn có thể bind route bằng slug mặc định:

public function getRouteKeyName()
{
    return $this->getSlugKeyName();
}

2.8.2 Explicit Binding

Bạn cũng có thể sử dụng phương thức RouteServiceProvider::boot như được miêu tả trong Laravel Documentation để quản lý explicit route model binding.

public function boot()
{
    parent::boot();

    Route::model('user', App\User::class);
}