0

Tìm hiểu Laravel từ số 0 (P10)

Để kết thúc chuỗi bài giới thiệu cơ bản về Laravel 5 thì trong phần 10 này tôi sẽ trình bày nốt về quan hệ nhiều - nhiều trong Laravel trên Model và UI.

Ví dụ cho lần này sẽ là chức năng gán tag vào các bài viết để hiểu về quan hệ nhiều - nhiều cụ thể như thế nào. Nếu bạn từng viết blog chắc hẳn cũng biết được tag nó hoạt động ra sao, không đi đâu xa thì với Viblo này khi viết bài tôi cũng có thể đính tag cho bài của mình :

  • Thường thì trong một bài bạn hoàn toàn có thể gán nhiều tag một lúc ( 1 bài viết nhiều tag)
  • Ngược lại thì tag sẽ dẫn đến nhiều bài viết có gán nó vào nội dung ( 1 tag nhiều bài viết)

Tôi sẽ tạo Model cho Tag, chạy :

php artisan make:model Tag

Và file app/Tag.php sẽ được tạo ra. Đối với Model thì tôi sẽ implement quan hệ nhiều - nhiều bằng cách sử dụng belongsToMany() :

ARTICLE

<?php // app/Article.php
namespace App; 
...
class Article extends Model
{
    ...
    public function user()
    {
        return $this->belongsTo('App\User');
    }
    // Thêm đoạn này
    public function tags()
    {
        return $this->belongsToMany('App\Tag')->withTimestamps();
    }
}

TAG

<?php // app/Tag.php
namespace App;
use Illuminate\Database\Eloquent\Model;
 
class Tag extends Model
{
    protected $fillable = ['name'];
 
    public function articles()
    {
        return $this->belongsToMany('App\Article')->withTimestamps();;
    }
}

Trong đối số thứ nhất của belongsToMany() tôi cần truyền vào tên Model liên quan, còn đối thứ hai sẽ là tên Model trung gian nhiều - nhiều. Như trên tôi đã lược đi đối số thứ hai, trog trường hợp này thì theo quy tắc tên Model trung gian sẽ là tên Model được sắp xếp theo thứ tự alphabet. Kiểu như bên dưới thì hai cách chỉ định và không chỉ định đối thứ hai đều cho một kết quả. Nếu muốn chỉ định khác đi thì bạn sẽ cần dùng đến đối thứ hai.

return $this->belongsToMany('App\Article');
return $this->belongsToMany('App\Article', 'article_tag');

Còn thứ 3 và 4 sẽ là chỉ định khoá ngoại của bảng trung gian. Khi giản lược đi thì nó sẽ là tên Model_id. Do đó, hai dòng code dưới sẽ có ý nghĩa như nhau :

return $this->belongsToMany('App\Article', 'article_tag');
return $this->belongsToMany('App\Article', 'article_tag', 'article_id', 'tag_id');

Ngoài ra, để mà cập nhật timestamp của bảng trung gian thì tôi cần dùng đến sự giúp đỡ của hàm withTimestamps() như ở trên.

MIGRATION

Tôi sẽ tạo ra Migration tạo ra bảng tags và article_tag.

php artisan make:migration create_tags_table --create=tags

Tôi sẽ đi chỉnh sửa bảng file database/migrations/YYYY_MM_DD_XXXX_create_tags_table.php giống như bên dưới :

<?php // database/migrations/YYYY_MM_DD_XXXX_create_tags_table.php
 
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateTagsTable extends Migration {
    public function up()
    {
        // Bảng tags
        Schema::create('tags', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
 
        // Bảng trung gian của bài viết và tags
        Schema::create('article_tag', function(Blueprint $table)
        {
            $table->integer('article_id')->unsigned()->index();
            $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade');
 
            $table->integer('tag_id')->unsigned()->index();
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
 
            $table->timestamps();
        });
    }
 
    public function down()
    {
        Schema::drop('tags');
        Schema::drop('article_tag');
    }
}

Như bạn thấy tôi đã không dùng các đối số 2, 3, 4 của hàm belongsToMany() nên tôi cần phải tạo bảng tags và bảng trung gian cho đúng quy tắc của Laravel là article_tag và chỉ định khoá ngoại chuẩn là tag_id và article_id. Ngoài ra tôi chỉ định thêm quy tắc onDelete nhằm đảm bảo sự toàn vẹn, thống nhất dữ liệu khi mà bài viết hoặc tag liên quan bị xoá thì bảng trung gian cũng sẽ ko còn bản ghi tương ứng chứa quan hệ đó nữa. Hãy chạy migrate :

php artisan migrate

Tôi tiến hành check bằng tinker, tạo ra 4 tags :

$ php artisan tinker
>>> $tag_diary = App\Tag::create(['name' => 'diary']);
>>> $tag_work = App\Tag::create(['name' => 'work']);
>>> $tag_hobby = App\Tag::create(['name' => 'hobby']);
>>> $tag_family = App\Tag::create(['name' => 'family']);
>>> App\Tag::all()->toArray();
=> [
       [
           "id"         => "1",
           "name"       => "diary",
           "created_at" => "2017-09-24 18:11:40",
           "updated_at" => "2017-09-24 18:11:40"
       ],
       [
           "id"         => "2",
           "name"       => "work",
           "created_at" => "2017-09-24 18:11:57",
           "updated_at" => "2017-09-24 18:11:57"
       ],
       [
           "id"         => "3",
           "name"       => "hobby",
           "created_at" => "2017-09-24 18:13:18",
           "updated_at" => "2017-09-24 18:13:18"
       ],
       [
           "id"         => "4",
           "name"       => "family",
           "created_at" => "2017-09-24 18:13:45",
           "updated_at" => "2017-09-24 18:13:45"
       ]
   ]
>>> App\Tag::lists('name');
=> [
       "diary",
       "work",
       "hobby",
       "family"
   ]
>>>

rồi thực hiện gán tags cho bài viết :

>>> $article = App\Article::first();
>>> $article->tags()->attach($tag_diary->id);  // ①
>>> 
>>> DB::select('select * from article_tag');
=> [
        {
           article_id: "1",
           tag_id: "1",
           created_at: "2017-09-24 21:07:56",
           updated_at: "2017-09-24 21:07:56"
       }
   ]
>>> 
>>> $article = App\Article::first();
>>> $article->tags->toArray();
=> [
       [
           "id"         => "1",
           "name"       => "diary",
           "created_at" => "2017-09-24 18:11:40",
           "updated_at" => "2017-09-24 18:11:40",
           "pivot"      => [
               "article_id" => "1",
               "tag_id"     => "1",
               "created_at" => "2017-09-24 21:07:56",
               "updated_at" => "2017-09-24 21:07:56"
           ]
       ]
   ]
>>> 

Để mà đưa vào Model nhiều - nhiều thì tôi cần dùng phương thức ①attach(). Kế đến, từ phía tag tôi sẽ thử tham chiếu đến bài viết :

>>> $tag = App\Tag::first();
>>> $tag->articles->toArray();
=> [
       [
           "id"           => "1",
           "user_id"      => "3",
           "title"        => "OMNIS ARCHITECTO ODIO REPELLAT ET VOLUPTATEM BEATAE.",
           "body"         => "Minus magni est dignissimos est excepturi incidunt. Eligendi et consequatur sunt adipisci laborum corrupti repudiandae vero. Dolor eum perspiciatis enim non reiciendis.",
           "created_at"   => "2017-09-22 17:24:56",
           "updated_at"   => "2017-09-22 19:28:45",
           "published_at" => "2017-09-22 00:00:00",
           "pivot"        => [
               "tag_id"     => "1",
               "article_id" => "1",
               "created_at" => "2017-09-24 21:07:56",
               "updated_at" => "2017-09-24 21:07:56"
           ]
       ]
   ]
>>> 

Phần nhiều - nhiều trên Model đến đây như vậy là đã thành công. Tiếp theo sẽ là quan hệ nhiều - nhiều trên màn hình. Và tôi sẽ sử dụng quan hệ đó từ màn hình tạo mới và sửa bài viết. Đầu tiên phải sửa lại Article.php để có thể lấy được mảng id của Tag :

<?php // app/Article.php
namespace App;
...
class Article extends Model {
    ...
 
    public function tags() {
        return $this->belongsToMany('App\Tag')->withTimestamps();
    }
 
    // Tôi thêm vào accesser chỗ này để lấy ra mảng id của tag 
    public function getTagListAttribute() {
        return $this->tags->lists('id')->all();
    }
}

Rồi cần sửa tiếp cả phần Controller trong ArticlesController.php :

<?php // app/Http/Controllers/ArticlesController.php
namespace App\Http\Controllers;
 
use App\Article;
use App\Tag;
use App\Http\Requests\ArticleRequest;
 
class ArticlesController extends Controller {
    ...
    public function create() {
        $tags = Tag::lists('name', 'id');  // ①
 
        return view('articles.create', compact('tags'));
    }
 
    public function store(ArticleRequest $request) {
        $article = \Auth::user()->articles()->create($request->all());
        $article->tags()->attach($request->input('tag_list'));  // ②
 
        \Session::flash('flash_message', 'Added article successfully.');
 
        return redirect()->route('articles.index');
    }
 
    public function edit(Article $article) {
        $tags = Tag::lists('name', 'id');  // ③
 
        return view('articles.edit', compact('article', 'tags'));
    }
 
    public function update(Article $article, ArticleRequest $request) {
        $article->update($request->all());
        $article->tags()->sync($request->input('tag_list', []));  // ④
 
        \Session::flash('flash_message', 'Updated article successfully.');
 
        return redirect()->route('articles.show', [$article->id]);
    }
    ...
}

Trong 2 phương thức ①create() và ③edit() tôi đã sửa để truyền list tên tag và id tới View. Còn trong ②store() đã thêm tag_list được truyền từ request vào relation của Tag bằng phương thức attach(). Với ④update() đã đồng bộ tag_list được truyền từ request vào relation của Tag bằng phương thức sync().

Trên View tôi cũng phải cần đi sửa form của việc tạo mới và sửa bài viết :

// resources/views/articles/form.blade.php 
...
<div class="form-group">
    {!! Form::label('published_at', 'Publish On:') !!}
    {!! Form::input('date', 'published_at', $published_at, ['class' => 'form-control']) !!}
</div>
 
{{-- Thêm vào --}}
<div class="form-group">
    {!! Form::label('tag_list', 'Tags:') !!}
    {!! Form::select('tag_list[]', $tags, null, ['class' => 'form-control', 'multiple']) !!}
</div>
 
<div class="form-group">
    {!! Form::submit($submitButton, ['class' => 'btn btn-primary form-control']) !!}
</div>

Như bạn thấy tôi đã thêm vào thẻ select. Và tôi có thể binding $article với Form bằng thuộc tính tag_list do đã có getTagListAttribute() trong Article Model. Do tag có thể chọn nhiều nên tôi đã gắn thêm [] cho binding name để thành tag_list[] và thêm cả lựa chọn multiple. Kế tiếp sẽ cần sửa View lúc hiển thị ra bài viết nếu mà có tag được gắn vào thì tag list cũng sẽ được hiển thị ra :

// resources/views/articles/show.blade.php 
@extends('layout')
@section('content')
    <h1>{{ $article->title }}</h1>
    <hr/>
    <article>
        <div class="body">{{ $article->body }}</div>
    </article>
 
    {{-- thêm vào --}}
    @unless ($article->tags->isEmpty())
        <h5>Tags:</h5>
        <ul>
            @foreach($article->tags as $tag)
                <li>{{ $tag->name }}</li>
            @endforeach
        </ul>
    @endunless
 
    @if (Auth::check())
        {!! link_to(route('articles.edit', [$article->id]), 'Edit', ['class' => 'btn btn-primary']) !!}
        {!! delete_form(['articles', $article->id]) !!}
    @endif
@stop

Bạn hãy chạy thử xem ! Vậy là hoàn thiện cả Model và UI cho quan hệ nhiều - nhiều và cũng là kết thúc cả chuỗi bài về Laravel cơ bản này !

The end & Thanks !


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí