Review code Laravel đẹp, chuẩn

Qua một thời gian dài làm việc, coding với Laravel, review code của các thành viên khác cũng như tham khảo từ nhiều cao thủ Laravel khác tôi xin tổng hợp lại một số code smell trong laravel và cách khắc phục. Hi vọng sau khi đọc bài này các bạn sẽ có một bộ souce Laravel đẹp, đúng chuẩn dễ dàng bảo trì và tối ưu nhất. Trong phần này tôi sẽ đề cập đến những vấn đề cơ bản nhất mà một lập trình viên từ  junior đến senior đều phải nắm vì vậy tôi cũng chỉ giải thích ngắn gọn nhất có thể. Các vấn đề về nguyên lý SOLID, các patterns... tôi sẽ tập trung trong một bài viết khác.



Lưu ý:
Tôi thương xuyên update tài liệu về review source code Laravel tại đây: https://github.com/minhbangchu/laravel-best-practices

1. Mỗi Class, Mỗi Method chỉ nên có 1 trách nhiệm duy nhất.
Bad:
public function getFullNameAttribute()
{
    if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
        return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
    } else {
        return $this->first_name[0] . '. ' . $this->last_name;
    }
}
Good:
public function getFullNameAttribute()
{
    return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}

public function isVerifiedClient()
{
    return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}

public function getFullNameLong()
{
    return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

public function getFullNameShort()
{
    return $this->first_name[0] . '. ' . $this->last_name;
}
(Bạn có thể tham khảo thêm ở nguyên lý số 1: "Single responsibility principle" của SOLID )

2. Hãy để Model béo tốt còn Controller thì đẹp dáng ^^ 
Đặt tất cả các xử lý logic liên quan đến DB vào các Model vì đó mới là chức năng của Model và Controller chỉ gọi đến phương thức logic đó trong Model thôi,

Bad:
public function index()
{
    $clients = Client::verified()
        ->with(['orders' => function ($q) {
            $q->where('created_at', '>', Carbon::today()->subWeek());
        }])
        ->get();

    return view('index', ['clients' => $clients]);
}
Good:
public function index()
{
    return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

class Client extends Model
{
    public function getWithNewOrders()
    {
        return $this->verified()
            ->with(['orders' => function ($q) {
                $q->where('created_at', '>', Carbon::today()->subWeek());
            }])
            ->get();
    }
}


3. Hãy thực hiện Validation trong lớp Request.
Chức năng của Request là nơi kiểm soát tính hợp lệ của dữ liệu đầu vào. Sau khi một request được gửi lên từ phía client thì request sẽ được validate dữ liệu bởi class form request trước khi được controller sử dụng. Vì vậy xin hãy tiếp tục giữ dáng đẹp cho Controller.

Bad:
public function store(Request $request)
{
    $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'publish_at' => 'nullable|date',
    ]);

    ....
}
Good:
public function store(PostRequest $request)
{    
    ....
}

class PostRequest extends Request
{
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
            'publish_at' => 'nullable|date',
        ];
    }
}


4. Các xử lý Business logic nên nằm trong lớp Service
Controller không nên kiêm nhiệm thêm các xử lý Logic vì vậy hãy tách chúng ra vào lớp Service. Việc này sẽ giảm tải cho controller, dễ dàng làm Unit test, dễ dàng sử dụng lại các business logic đó ở nơi khác.

Bad:
public function store(Request $request)
{
    if ($request->hasFile('image')) {
        $request->file('image')->move(public_path('images') . 'temp');
    }
    
    ....
}
Good:
public function store(Request $request)
{
    $this->articleService->handleUploadedImage($request->file('image'));

    ....
}

class ArticleService
{
    public function handleUploadedImage($image)
    {
        if (!is_null($image)) {
            $image->move(public_path('images') . 'temp');
        }
    }
}

5. Đừng lặp lại code. Hãy sử dụng lại khi có thể.
Điều này cũng áp dụng cho Blade templates và Eloquent scopes

Bad:
public function getActive()
{
    return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->where('verified', 1)->whereNotNull('deleted_at');
        })->get();
}
Good:
public function scopeActive($q)
{
    return $q->where('verified', 1)->whereNotNull('deleted_at');
}

public function getActive()
{
    return $this->active()->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->active();
        })->get();
}

6. Ưu tiên Eloquent hơn là sử dụng Query Builder và raw SQL queries. Ưu tiên collections hơn là sử dụng arrays
Eloquent giúp cho bạn viết SQL đẹp, dễ đọc và bảo trì code hơn. Eloquent cũng cung cấp sẵn cho bạn nhiều phương thức tuyệt vời như: Soft deletes, Events, Scopes ... Tương tự Collections là một đối tượng hỗ trợ nhiều phương thức linh động và hay sử dụng hơn là Array.

Bad:
SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
              FROM `users`
              WHERE `articles`.`user_id` = `users`.`id`
              AND EXISTS (SELECT *
                          FROM `profiles`
                          WHERE `profiles`.`user_id` = `users`.`id`) 
              AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC
Good:
Article::has('user.profile')->verified()->latest()->get();

7. Tự động gán các tham số của một HTTP request vào các biến hoặc đối tượng (Mass assignment)
Bad:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();
Good:
$category->article()->create($request->all());

8. Không chạy các câu Query trong Blade template. Xử lý Vấn đề N + 1 bằng Eager Loading
Bad:
@foreach (User::all() as $user)
    {{ $user->profile->name }}
@endforeach
Good:
// Eager Loading
$users = User::with('profile')->get();
...

@foreach ($users as $user)
    {{ $user->profile->name }}
@endforeach

Đoạn code Bad phía trên có 2 vấn đề:
- Không nên viết các truy vấn dữ liệu trong Blade template.
- Nếu có 100 User, sẽ có 101 câu Query sẽ được chạy.
Nếu sử dụng đoạn code Good bên dưới, chúng ra sẽ sử dụng công cụ Eager Loading của Laravel. Với việc sử dụng hàm with() hay load(), ta có thể load một lúc ra tất cả các User cùng với profile name chỉ bằng một câu truy vấn SQL. Và 101 câu query vào DB ở trên sẽ được tối ưu lại còn 2 câu Query.

Laravel cung cấp cho chúng ta một hệ thống Eager Loading rất hoàn hảo và mạnh mẽ. Bạn có thể tham khảo thêm ở đây eager-loading

9. Hãy comment cho code của bạn, nhưng nếu có thể hãy sử dụng tên phương thức và biến dễ hiểu để thay thế cho việc Comment.
Bad:
if (count((array) $builder->getQuery()->joins) > 0)
Better:
// Determine if there are any joins.
if (count((array) $builder->getQuery()->joins) > 0)
Good:
if ($this->hasJoins())

10. Không nên đặt JS và CSS trong Blade template và không đặt HTML nào trong các lớp PHP
Bad:
let article = `{{ json_encode($article) }}`;
Good:
<input id="article" type="hidden" value="{{ json_encode($article) }}">

Or

<button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button>
Code JS được đặt trong một file Javascript
let article = $('#article').val();

11. Sử dụng file config, language, constant thay thế cho Text trong code.
Bad:
public function isNormal()
{
    return $article->type === 'normal';
}

return back()->with('message', 'Your article has been added!');
Good:
public function isNormal()
{
    return $article->type === Article::TYPE_NORMAL;
}

return back()->with('message', __('app.article_added'));

12. Sử dụng cú pháp ngắn hơn và dễ đọc hơn nếu có thể
Bad:
$request->session()->get('cart');
$request->input('name');
Good:
session('cart');
$request->name;
Một số ví dụ khác về cú pháp ngắn hơn trong Laravel các bạn có thể tham khảo:
Common syntaxShorter and more readable syntax
Session::get('cart')session('cart')
$request->session()->get('cart')session('cart')
Session::put('cart', $data)session(['cart' => $data])
$request->input('name'), Request::get('name')$request->name, request('name')
return Redirect::back()return back()
is_null($object->relation) ? $object->relation->id : null }optional($object->relation)->id
return view('index')->with('title', $title)->with('client', $client)return view('index', compact('title', 'client'))
$request->has('value') ? $request->value : 'default';$request->get('value', 'default')
Carbon::now(), Carbon::today()now(), today()
App::make('Class')app('Class')
->where('column', '=', 1)->where('column', 1)
->orderBy('created_at', 'desc')->latest()
->orderBy('age', 'desc')->latest('age')
->orderBy('created_at', 'asc')->oldest()
->select('id', 'name')->get()->get(['id', 'name'])
->first()->name->value('name')
13. Sử dụng IoC container hoặc facades thay vì tạo Class mới
Việc khởi tạo một Class mới sẽ tạo ra sự liên kết phực tạp giữa các lớp, đồng thời sẽ phức tạp hơn trong quá trình testing. Vì vậy hãy sử dụng IoC Contrainer. Điều này giúp tuân thủ nguyên lý "Dependency Inversion Principle" của SOLID.
Bad:
$user = new User;
$user->create($request->all());
Good:
public function __construct(User $user)
{
    $this->user = $user;
}

....

$this->user->create($request->all());

14. Không lấy trực tiếp dữ liệu từ file .env
File .env sẽ không được sử dụng trong môi trường Production vì vậy hãy đữa dữ liệu và file config sau đó sử dụng helper config() helper để sử dụng dữ liệu đó.
Bad:
$apiKey = env('API_KEY');
Good:
// config/api.php
'key' => env('API_KEY'),

// Use the data
$apiKey = config('api.key');

15. Lưu trữ ngày theo định dạng chuẩn. Sử dụng Accessors và Mutators để sửa đổi định dạng ngày.
Accessors và Mutators cho phép bạn format các attributes của Eloquent khi lấy ra từ một model hoặc cũng có thể set giá trị cho chúng. Tại sao phải làm vậy? Vì trong project của bạn, 1 thuộc tính có thể sẽ được sử dụng ở rất nhiều nơi, chẳng lẽ mỗi lần sử dụng bạn lại phải format nó.
Bad:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Good
// Model
protected $dates = ['ordered_at', 'created_at', 'updated_at']
public function getSomeDateAttribute($date)
{
    return $date->format('m-d');
}

// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
Bạn tham khảo thêm về Accessor và Mutator tại đây nhé: https://laravel.com/docs/5.6/eloquent-mutators#accessors-and-mutators

16. Hãy tuân theo Quy ước đặt tên của Laravel
Laravel sử dụng PSR standards. Bạn có thể tham khảo tại https://www.php-fig.org/psr/psr-2/
Ngoài ra nên sử dụng các quy tắc sau vì đang được áp dụng rộng rãi trong cộng đồng Laravel:
WhatHowGoodBad
ControllersingularArticleControllerArticlesController
Routepluralarticles/1article/1
Named routesnake_case with dot notationusers.show_activeusers.show-active, show-active-users
ModelsingularUserUsers
hasOne or belongsTo relationshipsingulararticleCommentarticleComments, article_comment
All other relationshipspluralarticleCommentsarticleComment, article_comments
Tablepluralarticle_commentsarticle_comment, articleComments
Pivot tablesingular model names in alphabetical orderarticle_useruser_article, articles_users
Table columnsnake_case without model namemeta_titleMetaTitle; article_meta_title
Model propertysnake_case$model->created_at$model->createdAt
Foreign keysingular model name with _id suffixarticle_idArticleId, id_article, articles_id
Primary key-idcustom_id
Migration-2017_01_01_000000_create_articles_table2017_01_01_000000_articles
MethodcamelCasegetAllget_all
Method in resource controllertablestoresaveArticle
Method in test classcamelCasetestGuestCannotSeeArticletest_guest_cannot_see_article
VariablecamelCase$articlesWithAuthor$articles_with_author
Collectiondescriptive, plural$activeUsers = User::active()->get()$active, $data
Objectdescriptive, singular$activeUser = User::active()->first()$users, $obj
Config and language files indexsnake_casearticles_enabledArticlesEnabled; articles-enabled
Viewsnake_caseshow_filtered.blade.phpshowFiltered.blade.php, show-filtered.blade.php
Configsnake_casegoogle_calendar.phpgoogleCalendar.php, google-calendar.php
Contract (interface)adjective or nounAuthenticatableAuthenticationInterface, IAuthentication
TraitadjectiveNotifiableNotificationTrait
(Sẽ tiếp tục update)
--------
Tham khảo:
- https://laravel.com/
- https://www.php-fig.org/psr/psr-2/
- https://github.com/alexeymezenin/laravel-best-practices#contents

1 nhận xét:

Rất mong các ý kiến của các bạn khi đọc bài viết này !