0

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

Tiếp sau phần 8 thì phần 9 này tôi sẽ trình bày nốt về những nội dung cuối cùng trong chuỗi bài về Laravel cơ bản này. Bao gồm các nội dung sau :

  • Middleware
  • Relationships
  • Route Model Binding

!

Middleware

Trong phần trước chúng ta đã có thể login vào nhưng vẫn chưa có cơ chế điều khiển để những ai chưa login hệ thống sẽ không tạo thêm, hay sửa xoá gì các bài viết được. Bắt đầu từ Laravel 5 thì việc filter này sẽ tiến hành trong Middleware. Nó được đặt ở thư mục như bên dưới, khi bạn tạo project Laravel thì sẽ có 3 files được tạo ra :

app/Http/Middleware/
├── Authenticate.php
├── RedirectIfAuthenticated.php
└── VerifyCsrfToken.php

Hãy vào xem nội dung của file Authenticate.php :

<?php // app/Http/Middleware/Authenticate.php
namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Contracts\Auth\Guard;
 
class Authenticate {
 
    protected $auth;
 
    public function __construct(Guard $auth) {
        $this->auth = $auth;
    }
 
    public function handle($request, Closure $next) {
        if ($this->auth->guest()) {
            // Khi mà chưa login
            if ($request->ajax()) {
                return response('Unauthorized.', 401);
            } else {
                return redirect()->guest('auth/login');
            }
        }
 
        // Khi đã login
        return $next($request);
    }
}

Điểm chính ở đây như bạn thấy đó chính là phương thức handle(), nó sẽ được gọi đến trước khi mà phương thức trong Controller được gọi từ Route. Bên trong nó chứa xử lý phán đoán xem user có tiếp tục được dùng hệ thống hay không? Nếu user mà chưa login thì sẽ bị chuyển hướng về trang login còn ngược lại thì để tiếp tục nó sẽ gọi callback là $next.

Vậy đăng kí Middleware như nào? Việc đó sẽ làm ở trong app/Html/Kernel.php :

<?php // app/Http/Kernel.php
namespace App\Http;
 
use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
class Kernel extends HttpKernel {
    protected $middleware = [
        'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
        'Illuminate\Cookie\Middleware\EncryptCookies',
        'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
        'Illuminate\Session\Middleware\StartSession',
        'Illuminate\View\Middleware\ShareErrorsFromSession',
        'App\Http\Middleware\VerifyCsrfToken',
    ];
 
    protected $routeMiddleware = [
        'auth' => 'App\Http\Middleware\Authenticate',
        'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
        'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
    ];
 
}

Điều mà bạn cần nhớ ở những dòng này sẽ có 2 cái sau :

  • Trường hợp bạn muốn thực hiện Middleware đối với tất HTTP request của hệ thống thì cho vào $middleware
  • Còn chỉ muốn thực hiện Middleware đối với Route đặc định nào đó thì đăng kí vào $routeMiddleware.

Giờ hãy thử dùng Middleware trong Controller :

<?php // app/Http/Controllers/ArticlesControllers.php
namespace App\Http\Controllers;
...
class ArticlesController extends Controller {
    public function __construct()
    {
        $this->middleware('auth', ['except' => ['index', 'show']]);
    }
    ...
}

Như bạn thấy tôi đã thêm vào constructor với nội dung bên trong nó là sử dụng đến Middleware. Tôi truyền vào tham số là auth - chính là key khi mà code đăng kí Middleware ở trên thuộc tính $routeMiddleware của Kernel.php khi thực hiện phương thức middleware(). Bạn để ý là tôi có sử dụng cả lựa chọn optional là except có chỉ định mảng đối tượng không bị ảnh hưởng của Middleware đã chỉ định là indexshow. Đến đây hãy thử xác nhận lại Ruote bằng artisan :

php artisan route:list

+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+
| Domain | Method   | URI                      | Name             | Action                                                 | Middleware |
+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+
|        | GET|HEAD | articles                 | articles.index   | App\Http\Controllers\ArticlesController@index          |            |
|        | GET|HEAD | articles/create          | articles.create  | App\Http\Controllers\ArticlesController@create         | auth       |
|        | POST     | articles                 | articles.store   | App\Http\Controllers\ArticlesController@store          | auth       |
|        | GET|HEAD | articles/{articles}      | articles.show    | App\Http\Controllers\ArticlesController@show           |            |
|        | GET|HEAD | articles/{articles}/edit | articles.edit    | App\Http\Controllers\ArticlesController@edit           | auth       |
|        | PUT      | articles/{articles}      | articles.update  | App\Http\Controllers\ArticlesController@update         | auth       |
|        | PATCH    | articles/{articles}      |                  | App\Http\Controllers\ArticlesController@update         | auth       |
|        | DELETE   | articles/{articles}      | articles.destroy | App\Http\Controllers\ArticlesController@destroy        | auth       |
+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+

Như bạn thấy rằng cột Middleware ngoài cùng bên tay phải cho thấy Middleware auth đang được áp dụng cho những Route nào ngoại trừ 2 ông đã được thiết lập ngoài vùng phủ sóng index, show. Vậy là đã hoàn thành việc sử dụng Middleware, bạn có thể vào tạo hay sửa xoá bài viết sẽ thấy được Middleware sẽ check xem bạn đã login chưa, nếu chưa nó sẽ chuyển hướng bạn về lại trang login.

Tiếp đến do chúng ta đã thêm Middelware vào ArticlesController.php nên phía View cũng nên tiến hành điều khiển hiển thị của button theo trạng thái login :

{{-- resources/views/articles/index.blade.php --}}
@extends('layout')
 
@section('content')
    <h1>Articles</h1>
    <hr/> 
    {{-- chỉ hiển thị nút Create ở list các bài viết khi user đang login --}}
    @if (Auth::check())
        {!! link_to('articles/create', 'Create', ['class' => 'btn btn-primary']) !!}
    @endif
 
    @foreach($articles as $article)
        ...
    @endforeach
@stop
{{-- resources/views/articles/show.blade.php --}}
@extends('layout')
 
@section('content')
    <h1>{{ $article->title }}</h1>
    <hr/>
    <article>
        <div class="body">{{ $article->body }}</div>
    </article>
    {{-- Và lúc show bài viết cũng tương tự với nút Edit và Delete --}}
    @if (Auth::check())
        <br/>
        {!! link_to(route('articles.edit', [$article->id]), 'Edit', ['class' => 'btn btn-primary']) !!}
        <br/>
        <br/>
        {!! delete_form(['articles', $article->id]) !!}
    @endif
@stop

Xong ! Còn việc tạo ra Middleware cũng rất đơn giản, bạn sẽ dùng lệnh artisan để làm việc đó :

php artisan make:middleware MyMiddleware
// app/Http/Middleware/MyMiddleware.php sẽ được tạo
<?php namespace App\Http\Middleware; 
use Closure;
 
class MyMiddleware {
    public function handle($request, Closure $next)
    {
        // Những xử lý của bạn đặt ở đây
 
        return $next($request);
    }
}

Relationships

Kế đến tôi muốn chuyển sang đến relationship một - nhiều trên Model, để mà một User có được nhiều bài Article thì cần gắn quan hệ giữa User model với Article model.

// app/User.php
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
    ...
    public function articles() 
    {
        return $this->hasMany('App\Article');
    }
}

Tôi tạo ra phương thức articles() với mục đích tạo quan hệ một User nhiều bài Article bằng cách dùng phương thức hasMany(). Lúc này thì ta đã có thể lấy ra nhiều Article có quan hệ với User như code bên dưới :

$articles = User::find(1)->articles();
// app/Article.php 
class Article extends Model
{
    ...
    public function user() 
    {
        return $this->belongsTo('App\User');
    }
}

Ngược lại thì tôi tạo ra phương thức user() để mapping với AppUser bằng phương thức belongsTo(). Tương tự bạn có thể lấy được một User có mapping với Article như dưới :

$user = Article::find(1)->user();

Rồi tôi thêm vào khoá ngoại đến bảng Users vào bảng Articles bằng cách sửa trực tiếp Migration đã tạo ở lần trước, thực hiện rollback tất cả DB và tái xây dựng lại từ đầu.

<?php
// database/migrations/YYYY_MM_DD_TTTTTT_create_articles_table.php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateArticlesTable extends Migration
{
    public function up()
    {
        Schema::create('articles', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('user_id')->unsigned();   // Thêm vào
            $table->string('title');
            $table->text('body');
            $table->timestamps();
 
            // Thêm khoá ngoại user_id và có chỉ định onDelete(‘cascade’) để nếu dữ liệu Users có bị xoá thì tất cả bài viết của hắn cũng bị xoá theo
            $table->foreign('user_id')
                        ->references('id')
                        ->on('users')
                        ->onDelete('cascade');
        });
    }
 
    public function down()
    {
        Schema::drop('articles');
    }
}

Nếu đã chắc chắn hãy chạy command dưới để rollback tất cả DB bằng artisan để thực hiện lại toàn bộ Migration.

php artisan migrate:refresh

Do đã có sự thay đổi trong bảng nên ta cũng cần đi sửa cả Seed nữa :

<?php
 
use App\User;
use App\Article;
use Carbon\Carbon;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
 
class DatabaseSeeder extends Seeder
{
 
    public function run()
    {
        Model::unguard();
 
        $this->call('UsersTableSeeder');  // Thêm vào
        $this->call('ArticlesTableSeeder');
 
            Model::reguard();
    }
 
}
 
// Thêm vào
class UsersTableSeeder extends Seeder
{
 
    public function run()
    {
        DB::table('users')->delete();
 
        User::create([
            'name' => 'root',
            'email' => 'root@sample.com',
            'password' => bcrypt('password')
        ]);
    }
}
 
class ArticlesTableSeeder extends Seeder
{
 
    public function run()
    {
        DB::table('articles')->delete();
 
        $user = User::all()->first();
        $faker = Faker::create('en_US');
 
        for ($i = 0; $i &amp;lt; 10; $i++) {
            // Article::create([
            //  'title' => $faker->sentence(),
            //  'body' => $faker->paragraph(),
            //  'published_at' => Carbon::today(),
            // ]);
 
            // Thay đổi đoạn trên thành như dưới
            $article = new Article([
                'title' => $faker->sentence(),
                'body' => $faker->paragraph(),
                'published_at' => Carbon::now(),
            ]);
            $user->articles()->save($article);  // Mapping với $user
        }
    }
}

Tôi đã thêm vào dữ liệu của một user và thay đổi để mapping dữ liệu bài viết với user rồi mới thực hiện lưu. Hãy chạy lệnh artisan để thực hiện seed :

php artisan db:seed

Và khi mà lưu bài viết mới thì cũng cần sửa phương thức store() trong Controller để mapping nó với user đang login :

// app/Http/Controllers/ArticlesController.php 
class ArticlesController extends Controller {
    ...
    public function store(ArticleRequest $request) {
        // Article::create($request->all());
        \Auth::user()->articles()->create($request->all());
 
        \Session::flash('flash_message', 'Add article successfully.');
 
        return redirect()->route('articles.index');
    }
    ...
}

Bạn hãy thực hiện login rồi dùng tinker để xác nhận dữ liệu :

$ php artisan tinker
>>>
>>> $user = App\User::where("name", "who")->first();
>>>
>>> $user->articles->count();
=> 2
>>>
>>> $user->articles->toArray();
=> [
       [
           "id"           => "11",
           "user_id"      => "5",
           "title"        => "WHO's article",
           "body"         => "Someone wrote me",
           "created_at"   => "2017-08-25 11:56:17",
           "updated_at"   => "2017-08-25 11:56:17",
           "published_at" => "2017-08-25 00:00:00"
       ],
       [
           "id"           => "12",
           "user_id"      => "5",
           "title"        => "WHO's article 2",
           "body"         => "Somebody wrote me too",
           "created_at"   => "2017-08-25 12:05:31",
           "updated_at"   => "2017-08-25 12:05:31",
           "published_at" => "2017-08-25 00:00:00"
       ]
   ]
>>>
>>>
>>> $article = App\Article::find(11);
>>>
>>> $article->user->toArray();
=> [
       "id"         => "5",
       "name"       => "who",
       "email"      => "who@sample.com",
       "created_at" => "2017-08-25 11:55:55",
       "updated_at" => "2017-08-25 11:55:55"
   ]
>>> 

Vậy là ta đã có thể bằng $user->articles để access vào bài viết từ User model và ngược lại với $article->user thì có thể access vào User từ Article model !

Route Model Binding

Cuối cùng tôi sẽ sử dụng chức năng Route Model Binding để refactor lại Controller. Route Model Binding là một chức năng rất hữu ích để thay vì dùng id ở những chỗ đang nhận id của model thì sẽ nhận được cả instance của model đối với id đó.

+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+
| Domain | Method   | URI                      | Name             | Action                                                 | Middleware |
+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+
|        | GET|HEAD | articles                 | articles.index   | App\Http\Controllers\ArticlesController@index          |            |
|        | GET|HEAD | articles/create          | articles.create  | App\Http\Controllers\ArticlesController@create         | auth       |
|        | POST     | articles                 | articles.store   | App\Http\Controllers\ArticlesController@store          | auth       |
|        | GET|HEAD | articles/{articles}      | articles.show    | App\Http\Controllers\ArticlesController@show           |            |
|        | GET|HEAD | articles/{articles}/edit | articles.edit    | App\Http\Controllers\ArticlesController@edit           | auth       |
|        | PUT      | articles/{articles}      | articles.update  | App\Http\Controllers\ArticlesController@update         | auth       |
|        | PATCH    | articles/{articles}      |                  | App\Http\Controllers\ArticlesController@update         | auth       |
|        | DELETE   | articles/{articles}      | articles.destroy | App\Http\Controllers\ArticlesController@destroy        | auth       |
+--------+----------+--------------------------+------------------+--------------------------------------------------------+------------+

Kiểm tra lại route thì ta thấy có 4 actions đang nhận tham số {articles} và chúng đều có xử lý tìm kiếm bài viết bằng id ở trong Controller.

$article = Article::findOrFail($id);
// app/Http/Controllers/ArticlesController.php 
...
class ArticlesController extends Controller {
    ...
    public function show($id) {
        $article = Article::findOrFail($id);  // Tìm kiếm bài viết bẳng $id
 
        return view('articles.show', compact('article'));
    }
    ...
    public function edit($id) {
        $article = Article::findOrFail($id);  // Tìm kiếm bài viết bẳng $id
 
        return view('articles.edit', compact('article'));
    }
 
    public function update($id, ArticleRequest $request) {
        $article = Article::findOrFail($id);  // Tìm kiếm bài viết bẳng $id
        $article->update($request->all());
        \Session::flash('flash_message', 'Updated article successfully.');
 
        return redirect()->route('articles.show', [$article->id]);
    }
 
    public function destroy($id) {
        $article = Article::findOrFail($id);  // Tìm kiếm bài viết bẳng $id
        $article->delete();
        \Session::flash('flash_message', 'Deleted article successfully.');
 
        return redirect()->route('articles.index');
    }
}

Lợi ích của Route Model Binding nếu dùng ở đây sẽ là không cần thực hiện xử lý tìm kiếm bằng $id nữa. Tôi sẽ tiến hành thiết lập như sau trong RouteServiceProvider.php :

// app/Providers/RouteServiceProvider.php
...
class RouteServiceProvider extends ServiceProvider {
    protected $namespace = 'App\Http\Controllers';
 
    public function boot(Router $router)
    {
        parent::boot($router);
 
        $router->model('articles', 'App\Article');  // Thêm vào
    }
    ...
}

Bằng cách thêm vào thực hiện $router->mode() trong phương thức boot() tồi tiến hành mapping model đối với tham số. Tham số thứ nhất chính là tham số được ghi trong URI của route bên trên. Còn tham số thứ hai là namespace của model cần binding. Ok, tiếp tục refactor lại Controller :

// app/Http/Controllers/ArticlesController.php 
...
class ArticlesController extends Controller {
    ...
    public function show(Article $article) {  // Thay đổi từ $id thành $article
        return view('articles.show', compact('article'));
    }
    ...
    public function edit(Article $article) {  // Thay đổi từ $id thành $article
        return view('articles.edit', compact('article'));
    }
 
    public function update(Article $article, ArticleRequest $request) {  // Thay đổi từ $id thành $article
        $article->update($request->all());
        \Session::flash('flash_message', 'Updated article successfully.');
 
        return redirect()->route('articles.show', [$article->id]);
    }
 
    public function destroy(Article $article) {  // Thay đổi từ $id thành $article
        $article->delete();
        \Session::flash('flash_message', 'Deleted article successfully.');
 
        return redirect()->route('articles.index');
    }
}

Bạn thấy rằng code thực hiện tìm kiếm $article = Article::findOrFail($id); đã không còn cần thiết nữa, do đó Controller của chúng ta cũng trở lên rất là đơn giản, sáng sủa hơn !

Trong phần 10 tôi sẽ viết nốt nội dung chủ đề cuối cùng nói về quan hệ nhiều - nhiều trong Laravel.


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í