0

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

Ở phần trước tôi đã đi đến nội dung tạo Form. Trong bài này tôi sẽ đi tiếp đến các phần nội dung sau :

  • Form validation
  • Formrequest
  • Error message
  • Scope
  • View Partial

Form validation

Controller

Tôi sẽ đi bước đầu tiên với controller để validate dữ liệu được nhập vào Form đã tạo. Đó là phải chỉnh sửa phương thức store() như sau :

<?php namespace AppHttpControllers;
// app/Http/Controllers/ArticlesController.php
use App\Article;
use App\Http\Controllers\Controller;
 
use Illuminate\Http\Request;  // ①'
class ArticlesController extends Controller {
    // ...
    public function store(Request $request) {  // ①
        $rules = [    // ②
            'title' => 'required|min:3',
            'body' => 'required',
            'published_at' => 'required|date',
        ];
        $this->validate($request, $rules);  // ③
 
        Article::create($request->all());
 
        return redirect('articles');
    }
}

Phần ① tôi đã thay đổi cách lấy request. Lần trước tôi sử dụng facade \Request để access vào request nhưng ở đây đã đổi thành lấy ra instance của lớp Illuminate\Http\Request từ đối số của phương thức store(). Đây là cách hoạt động rất hay của Laravel controller, khi mà ta chỉ cần ghi vào tên lớp vào đối số của phương thức thì tự động instance của lớp sẽ được sinh ra.

Phần ② là chỗ thiết lập những rule validate dữ liệu. Chúng ta có thể lập được rất nhiều rule nhưng trong bài này tôi chỉ đưa vào vài ví dụ như đoạn code trên. Chi tiết hơn bạn có thể xem tại : http://laravel.com/docs/5.1/validation#available-validation-rules

Phần ③ tôi đang thực hiện phương thức validate() của Controller. Ở đây nếu như có lỗi thì Laravel có cơ chế tự động giúp ta được redirect trở về màn hình trước đó.

View

Xong phần xử lý bên Controller thì việc còn lại sẽ là hiển thị những lỗi bắt được lên View. Nếu như có nhiều lỗi thì ta sẽ nhận được mảng chứa chúng nên tôi sẽ dùng foreach để duyệt rồi hiển thị chúng ra màn hình.

// resources/views/articles/create.blade.php
@extends('layout')
@section('content')
    <h1>Write a New Article</h1>
    <hr/>
    {{-- Tôi thêm phần hiển thị lỗi --}}
    @if ($errors->any())
        <div class="alert alert-danger">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif
    {!! Form::open() !!}
        ...
    {!

Việc thiết lập đã hoàn tất, bạn có thể truy cập vào http://localhost:8000/articles/create nhưng không nhập gì cả mà ấn nút Add Article, nếu error message hiển thị là xong.

Formrequest

Bước trên tôi đã thiết lập xong validate Form, giờ tôi sẽ dùng 1 chức năng là Formrequest để refactor phần đó.

Tạo Formrequest

Tôi sử dụng artisan để tạo ra FormRequest dùng cho Article.

php artisan make:request ArticleRequest

Và file app/Http/Requests/ArticleRequest.php sẽ được tạo ra với nội dung như sau :

<?php // app/Http/Requests/ArticleRequest.php
namespace App\Http\Requests;
use App\Http\Requests\Request;
 
class ArticleRequest extends Request {
    public function authorize()
    {
        return false;
    }
 
    public function rules()
    {
        return [
            //
        ];
    }
}

Sửa ARTICLEREQUEST.PHP

Tôi tiến hành sửa nội dung thành :

<?php namespace App\Http\Requests;
use App\Http\Requests\Request;
 
class ArticleRequest extends Request {
    public function authorize()
    {
        return true;  // ①
    }
 
    public function rules()
    {
        return [  // ②
            'title' => 'required|min:3',
            'body' => 'required',
            'published_at' => 'required|date',
        ];
    }
}

Phần ① tôi sẽ thiết lập quyền hạn đối với request bằng phương thức authorize(). Ví dụ nhưng user đang login hiện tại mà ko có quyền thì sẽ trả về false. Nhưng bây giờ thì ai cũng có thể tạo được dữ liệu Article nên sẽ trả về true.

Phần ② trả về validation rule bằng phương thức rules().

Controller

Tiếp theo tôi sẽ đi sửa để trên phương thức store() của ArticlesController sẽ sử dụng ArticleRequest.

<?php namespace AppHttpControllers;
// app/Http/Controllers/ArticlesController.php
 
use App\Article;
use App\Http\Controllers\Controller;
use App\Http\Requests\ArticleRequest;  // ①'
 
class ArticlesController extends Controller {
    // ...
    public function store(ArticleRequest $request) {  // ①
        // đoạn validate ở đây giờ ko cần nữa
 
        Article::create($request->all());
        return redirect('articles');
    }
}

Phần ① như bạn thấy là tôi đã đổi từ Illuminate\Http\Request sang dùng App\Http\Requests\ArticleRequest. Chỉ cần thiết lập như vậy là bạn đã có thể không cần đến validate đã thiết lập ở bước đầu tiên nữa nhưng kết quả vẫn sẽ tương tự. Controller lúc này đã trở lên sạch sẽ hơn rồi 😃

Đổi error message sang ngôn ngữ khác

Mặc định ngôn ngữ của message lỗi là tiếng Anh nên nếu thấy cần thiết, ví dụ như khách hàng của tôi là người nhật thì việc chuyển đổi sang tiếng Nhật là hoàn toàn cần làm. Tôi sẽ chuyển config qua ngôn ngữ Nhật bằng cách thiết lập locale thành ja như sau :

// config/app.php
'locale' => 'ja',

Sau đó là cần thiết lập file lỗi cho tiếng Nhật tương ứng. Những file này được lưu tại folder lang :

resources
└── lang
     ├── en
     │   └── validation.php
     └── ja
          └── validation.php

Bạn có thể copy bên thư mục en sang ja sau đó thì chuyển nhưng message có nội dung tiếng Anh sang tiếng Nhật.

Scope

Trước khi đi vào chức năng Scope của Eloquent chúng ta hãy nhìn lại xem phần danh sách hiển thị ra có vấn đề gì không. Tôi nghĩ là có 2. Thứ nhất là những bài mới được tạo ra lại bị chìm xuống dưới. Thứ hai là những bài mà có ngày published_at là ngày của tương lai cũng được hiển thị ra. Vậy hãy sửa lại chức năng này cho hợp lý hơn.

Sắp xếp

Tôi sẽ sửa lại ArticlesController.php như sau vì dùng all() thì nó sẽ chỉ đơn thuần lấy ra tất cả các bài viết nên tôi đổi sang dùng lastest()->get() để lấy ra những cái mới nhất rồi sắp xếp theo published_at giảm dần :

<?php namespace App\Http\Controllers;
// app/Http/Controllers/ArticlesController.php
 
// ...
class ArticlesController extends Controller {
 
    public function index() {
        // $articles = Article::all();  Trước tôi chỉ dùng code này
        $articles = Article::latest('published_at')->get(); // Thay đổi thành như này
        /* Ngoài ra có thể dùng orderBy()->get() cũng cho kết quả tương tự
        // $articles = Article::orderBy('published_at', 'desc')->get();  
        */
 
        return view('articles.index', compact('articles'));
    }
}

Tiếp theo tôi sẽ sửa để lấy ra chỉ những bài viết có ngày publish trước thời điểm hiện tại dùng where() :

$articles = Article::latest('published_at')->where('published_at', '<=', Carbon::now())->get();

Scope

Nếu như trang của bạn chỉ cần dùng điều kiện mà chỉ lấy bài viết có published_at trước thời điểm hiện tại thì như vậy ok. Nhưng trong trường hợp điều kiện này được sử dụng lặp lại nhiều lần thì việc khai báo ở nhiều chỗ sẽ bị dư thừa và giảm sự mạch lạc của code. Lúc này khái niệm scope sẽ hữu ích cho bạn. Đây là chức năng mà Eloquent của Laravel cung cấp. Nó cho phép định nghĩa phạm vi - scope để sử dụng nhiều lần được.

Tôi sẽ thêm phương thức scope() vào Model, và để định nghĩa bạn cần gắn cho nó tiền tố scope như sau :

<?php namespace App;
// app/Article.php 
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
 
class Article extends Model {
    // ... 
    //  Tôi định nghĩa published scope để dùng nhiều chỗ
    public function scopePublished($query) {
        $query->where('published_at', '<=', Carbon::now());
    } 
    // ...
}

Sau đó sẽ thay đổi index() trong Controller từ dùng where sang dùng `scope.

public function index() {
        $articles = Article::latest('published_at')->published()->get();
 
        return view('articles.index', compact('articles'));
    }

Code đoạn này đã trở lên ngắn gọn dễ đọc hơn và ta cũng có thể dùng lại scope published này ở những chỗ khác. Và tương tự bạn hoàn toàn có thể tạo ra được nhiều scope khác nhau phục vụ cho từng nhu cầu riêng rẽ.

View Partial

Trong Laravel tồn tại một khái niệm là View Partial, bạn có thể hiểu nó cho phép bạn common hoá Form để dùng chung được ở các View khác nhau. Ví dụ như trong phạm vị loạt bài này, tôi đã tạo ra list và Form tạo mới, nhưng còn thiếu phần chỉnh sửa. Mà phần này sẽ gần như tương tự lúc tạo mới nên bạn có thể dùng View Partial.

Routing

Tôi sẽ thêm hai route mới vào là editupdate. Bạn chú ý rằng update không phải phương thức get mà là path :

// app/Http/routes.php
Route::get('articles', 'ArticlesController@index');
Route::get('articles/create', 'ArticlesController@create');
Route::get('articles/{id}', 'ArticlesController@show');
Route::post('articles', 'ArticlesController@store');
Route::get('articles/{id}/edit', 'ArticlesController@edit');  // new
Route::patch('articles/{id}', 'ArticlesController@update');  // new

Controller

Do đã thêm route nên ta cần thêm vào hai phương thức tương ứng trong Controller là editupdate:

// app/Http/Controllers/ArticlesController.php
    public function edit($id) {
        $article = Article::findOrFail($id);
 
        return view('articles.edit', compact('article'));
    }
 
    public function update($id, ArticleRequest $request) {
        $article = Article::findOrFail($id);
 
        $article->update($request->all());
 
        return redirect(url('articles', [$article->id]));
    }
}

Về phương thức edit() thì hoàn toàn không có kiến thức gì mới lạ nhưng phần update() thì nó đang nhận vào tham số $id$request. Tham số $request này đang dùng lớp ArticleRequest kế thừa từ FormRequest mà ở trên đã làm. Bằng việc sử dụng FormRequest thì khi mà có lỗi lúc check dữ liệu nhập vào Form thì sẽ tự động redirect về màn hình trước đó. Trong phương thức update() này sẽ lấy ra thông tin của bài viết đối với $id chỉ định, rồi cập nhật bài viết với nội dung nhập vào Form và redirect về trang hiển thị của nó.

View

Tôi sẽ đi tạo ra edit.blade.php bằng cách copy và chỉnh sửa từ phần tạo mới create.blade.php :

// resources/views/articles/edit.blade.php
@extends('layout')
@section('content')
    <h1>Edit: {{ $article->title }}</h1>
    <hr/>
    @if ($errors->any())
        <ul class="alert alert-danger">
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    @endif
 
    {!! Form::model($article, ['method' => 'PATCH', 'url' => ['articles', $article->id]]) !!}
    <div class="form-group">
        {!! Form::label('title', 'Title:') !!}
        {!! Form::text('title', null, ['class' => 'form-control']) !!}
    </div>
    <div class="form-group">
        {!! Form::label('body', 'Body:') !!}
        {!! Form::textarea('body', null, ['class' => 'form-control']) !!}
    </div>
    <div class="form-group">
        {!! Form::label('published_at', 'Publish On:') !!}
        {!! Form::input('date', 'published_at', $article->published_at->format('Y-m-d'), ['class' => 'form-control']) !!}
    </div>
    <div class="form-group">
        {!! Form::submit('Edit Article', ['class' => 'btn btn-primary form-control']) !!}
    </div>
    {!! Form::close() !!}
 
@stop

Bạn có thể thấy có 4 điểm được sửa ở đây gồm :

  1. Hiển thị tiêu đề bài viết trên phần tiêu đề của trang
  2. Thay đổi từ Form::open thành Form::model. Cái này dùng để mapping giá trị của $article với Form. Rồi chỉ định METHOD là PATHid của $article đến url.
  3. Thay đổi giá trị khởi tạo của mục nhập published_at từ date('Y-m-d') thành giá trị của $article->published_at. Vì giá trị này đang chỉ định Date Mutator ở lớp Article nên sẽ có giá trị instance của lớp Carbon được trả về. Sau đó tiến hành biến đối nó thành chuỗi sử dụng phương thức format() làm giá trị khởi tạo lúc edit.
  4. Thay đổi label của nút submit thành Edit Article

Và nếu bạn dùng trình duyệt để view source thì nó sẽ có dạng :

...
<form method="POST" action="http://localhost:8000/articles/1" accept-charset="UTF-8">
    <input name="_method" type="hidden" value="PATCH">
...

View Partial

Việc tiếp theo là sử dụng VIEW PARTIAL để refactor code do phần tạo mới và sửa bài viết có những code trùng lặp nhau. Đầu tiên là phải partial hoá hiển thị lỗi - tạo form_error_blade.php :

// resources/views/errors/form_errors.blade.php
 
@if ($errors->any())
    <ul class="alert alert-danger">
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
@endif

Để sử dụng file trên thì cần sửa lại file edit.blade.php bằng cách dùng @include để tham chiếu đến nó. Và do file đó được tạo dưới thư mục resources/views/errors nên cần chỉ định errors.form_errors - đây chính là khái niệm View Partial, tức chia View ra từng phần :

// resources/views/articles/edit.blade.php
@extends('layout')
@section('content')
    <h1>Edit: {!! $article->title !!}</h1>
    <hr/>
    @include('errors.form_errors')
 
    {!! Form::model($article, ['method' => 'PATCH', 'url' => ['articles', $article->id]]) !!}
        ...
        ...
    {!! Form::close() !!}
 
@stop

Và cũng cần phải Partial hoá cả Form rồi trả về giá trị đến Partial. Tôi sẽ tạo file form.blade.php :

// resources/views/articles/form.blade.php
 
<div class="form-group">
    {!! Form::label('title', 'Title:') !!}
    {!! Form::text('title', null, ['class' => 'form-control']) !!}
</div>
<div class="form-group">
    {!! Form::label('body', 'Body:') !!}
    {!! Form::textarea('body', null, ['class' => 'form-control']) !!}
</div>
<div class="form-group">
    {!! Form::label('published_at', 'Publish On:') !!}
    {!! Form::input('date', 'published_at', $published_at, ['class' => 'form-control']) !!}
</div>
<div class="form-group">
    {!! Form::submit($submitButton, ['class' => 'btn btn-primary form-control']) !!}
</div>

Bạn thấy rằng gía trị khởi tạo ban đầu của published_at sẽ được trả về bởi biến $published_at. Và label hiển thị của nút submit cũng sẽ được trả về bằng biến $submitButton. Để sử dụng nó tôi cũng cần sửa thêm file edit.blade.php :

// resources/views/articles/edit.blade.php
@extends('layout')
@section('content')
    <h1>Edit: {!! $article->title !!}</h1>
    <hr/>
    @include('errors.form_errors')
 
    {!! Form::model($article, ['method' => 'PATCH', 'url' => ['articles', $article->id]]) !!}
        @include('articles.form', ['published_at' => $article->published_at->format('Y-m-d'), 'submitButton' => 'Edit Article'])
    {!! Form::close() !!}
 
@stop

Tương tự như ở trên tôi cũng dùng @include để tham chiếu đến form.blade.php. Và đưa giá trị của published_atsubmitButton. Cuối cùng tôi chỉ cần thêm vào nút Edit là hoàn thiện :

// resources/views/articles/show.blade.php
@extends('layout')
@section('content')
    <h1>{{ $article->title }}</h1>
    <hr/>
    <article>
        <div class="body">{{ $article->body }}</div>
    </article>
    <br/>
    {!! link_to(action('ArticlesController@edit', [$article->id]), 'Edit', ['class' => 'btn btn-primary']) !!}
@stop

Hy vọng bài này đã cung cấp được thêm kiến thức mới cho những newbie với Laravel như tôi.

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í