+146

Laravel Beauty: Recipes & Best Practices

laravel-1.png

Index

  1. Laravel Beauty: Recipes & Best Practices
  2. Laravel Beauty: Tìm hiểu về Service Container
  3. Laravel Beauty: Tìm hiểu về Service Provider
  4. Laravel Beauty: Tìm hiểu về Facade
  5. Laravel Beauty: Tìm hiểu về Contract

About "Laravel Beauty" series

Laravel là một Open-source PHP Web Application Framework, được tạo ra bởi Taylor Otwell. Mặc dù ra đời khá là muộn màng khi mới chỉ bắt đầu từ năm 2011, tuy nhiên cho đến nay thì Laravel đã trở thành PHP Framework phổ biến nhất (và tuyệt vời nhất, theo quan điểm của mình =))). Cộng đồng Laravel rất active, số lượng package cho Laravel cũng cực kỳ nhiều và phong phú. Laravel bỏ xa hoàn toàn các framework khác về số lượng stars hay số lượng forks trên Github!

Một trong những điểm mình yêu thích nữa ở Laravel đó là nó thật sự là một Modern Full Stack Framework, giúp cho việc phát triển web trở nên cực kỳ nhanh chóng và đơn giản. Để thực hiện việc đó thì Laravel mang trong mình rất nhiều rất nhiều những tư tưởng mới mẻ, với những khái niệm hay Design Pattern thú vị.

Mình cũng rất thích điểm Laravel thường xuyên yêu cầu phiên bản PHP rất cao, như từ bản 4.2 đã yêu cầu PHP 5.4 trở lên, hay phiên bản 5.1 ra mắt tháng 6 vừa qua đã yêu cầu PHP từ 5.5.9 (trong khi có những framework PHP khác giờ vẫn còn dậm chân support PHP 5.3 (facepalm)). Việc yêu cầu những phiên bản PHP cao như vậy giúp cho người lập trình viên dễ dàng tiếp cận với "phong cách" code hiện đại, và với những tính năng mới hơn.

Trong loạt bài viết Laravel Beauty này, mình sẽ cố gắng gửi đến các bạn những hiểu biết của mình về những điều tuyệt vời của Laravel, hay những kinh nghiệm hữu ích khi làm việc với một project Laravel. Hy vọng các bạn có thể thêm hiểu và thêm yêu thích Framework tuyệt vời này 😄

Và cũng do tích chất có phần hơi lý thuyết của những bài viết như vậy thế nên nếu bạn là một người mới bắt đầu học Laravel, chưa từng làm một project nào về Laravel thì bạn nên xem qua về Document của Laravel trước. Nó thật sự là khá đầy đủ và hữu ích đấy 😄

Before start

Mội vài package bạn nên có trong mỗi Project của mình.

Laravel Debugbar

Laravel Debugbar sẽ giúp bạn theo giõi hoạt động của project của mình được tốt hơn. Như việc có những request nào, đi qua những route nào, sử dụng những view nào, hay thực hiện những câu truy vấn SQL nào ... Việc kiểm tra được số lượng cũng như nội dung các câu truy vấn SQL sẽ giúp bạn biết lúc nào code của mình hoạt động tốt, lúc nào cần phải tối ưu code.

Thế nên Laravel Debugbar thực sự là một công cụ không thể thiếu giúp cho việc phát triển với Laravel trở nên dễ dàng hơn.

Laravel Log Viewer

Ngoài ra bạn cũng nên sử dụng thêm một công cụ là Laravel Log Viewer, để có thể dễ dàng check nội dung từ những file logs mà Laravel sinh ra bên trong thư mục storage/logs từ trang web của mình. Nên chú ý là đừng có để quyền truy cập vào trang view logs đó cho tất cả mọi users nhé 😃

Laravel IDE Helper

Nếu bạn đang sử dụng những IDE trong việc phát triển với Laravel thì Laravel IDE Helper sẽ là một công cụ rất hữu ích dành cho bạn.

Laravel IDE Helper đem lại khả năng Auto Complete cho rất nhiều class của Laravel, đặc biệt trong đó có Facade (thứ mà không một IDE nào hiểu được là nó hoạt động như thế nào =))).

Ngoài ra Laravel IDE Helper còn có khả năng auto comment cho Model theo chuẩn của PHPDoc. Nếu project của bạn yêu cầu phải viết comment đầy đủ và chặt chẽ, thì hãy để Laravel IDE Helper giúp đỡ một phần nào cho bạn.

Recipes & Best Practices

Vấn đề N + 1 và Eager Loading

Hãy giả sử bạn gặp một vấn đề như thế này:

  • Bạn có một danh sách các bài viết trong bảng posts, với Model Eloquent tên là Post
  • Mỗi Post thuộc về một User. Post có quan hệ belongsTo với User, và User có quan hệ hasMany với Post.
  • Bạn có một trang Post list, cần hiển thị ra một danh sách các Post, dĩ nhiên với tên tác giả của post đó. Đây là một tình huống rất hay thường gặp trong các project, với nhiều biến thể khác nhau. Và dưới đây là một cách giải quyết thường gặp.
// Post Model
public function user()
{
    return $this->belongsTo(User::class);
}
// In Controller or Somewhere else
$posts = Post::all();
// View
foreach ($posts as $post) {
    {{ $post->title }}
    {{ $post->user->name }}
}
  • Như vậy để in ra tên của chủ nhân của bài viết, ta đã sử dụng đến quan hệ user mà mình đã định nghĩa, tức nó sẽ lấy ra User chủ nhân của bài Post. Nếu bạn cài Laravel Debugbar thì bạn sẽ nhận ra những vấn đề của phương pháp này:
    • Với mỗi một bài Post ta phải thực hiện một câu truy vấn SQL để lấy ra User. Nếu số lượng bài post của bạn là lớn thì số lượng câu truy vấn SQL phải thực cũng sẽ là lớn.
    • Có khả năng bị thực hiện những câu truy vấn trùng lặp nhau. Chẳng hạn bài post số 1, 2, 3 đều thuộc user có id là 1. Trong trường hợp đó ta sẽ phải thực hiện 3 câu truy vấn SQL cùng với mục đích là lấy ra user có id bằng 1 (để gán vào cho từng bài post).

Nên biết truy vấn SQL thường là một trong những tác vụ tốn nhiều thời gian nhất trong một xử lý request. Rõ ràng việc phải sử dụng nhiều câu truy vấn như vậy là một vấn đề ta cần khắc phục. Ta gọi vấn đề đó là N + 1 problem, bởi ngoài việc dùng 1 câu truy vấn để select ra N bài post, bạn sẽ phải sử dụng tiếp N câu truy vấn để lấy ra N user cho N bài post đó.

Laravel cung cấp một công cụ để giải quyết vấn đề này, đó là Eager Loading.

// Eager Loading
$posts = Post::with('user')->all();

// Lazy Eager Loading
$posts = Post::all();
$posts->load('user');

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 thuộc về tất cả các Post chỉ bằng một câu truy vấn SQL. Điều này sẽ giúp ta giảm số lượng câu truy vấn đi một cách đáng kể.

Hãy luôn cố gắng dùng Eager Loading những lúc có thể nhé. 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ạnh việc load một quan hệ như trên, ta còn có thể load một lúc nhiều quan hệ (Multiple Relationships Eager Loading), load quan hệ chồng nhau (Nested Eager Loading), hay thực hiện Eager Loading có kèm điều kiện ...

Thông tin chi tiết về cách sử dụng Eager Loading các bạn có thể xem tại Official Document của Laravel

Method hay Property ?

Tiếp tục một vấn đề liên quan đến quan hệ của Eloquent. Giả sử như bạn gặp một vấn đề như sau:

  • Mỗi Category có nhiều Post. Mỗi Post thuộc về một Category.
  • Bạn có một trang Category view, và trong trang hiện thông tin chi tiết về category đó thì bạn cần hiện ra số lượng bài viết có trong category. Để giải quyết được vấn đề lấy số lượng bài post trong một category ta có cách như sau.
// Category Model
public function posts()
{
    return $this->hasMany(Post::class);
}
// View. We can use
$category->posts->count();
// Or
$category->posts()->count();

Như mình viết ở trên thì ta có 2 cách viết là $category->posts->count();$category->posts()->count();. Bạn có biết nó khác nhau thế nào không ?

Đầu tiên ta cần biết rằng $category->posts() là lời gọi hàm posts() từ instance $category, trong khi đó posts là lời gọi property.

Cách gọi $category->posts() thì như định nghĩa ở trên là return $this->hasMany(Post::class);, ta có thể thấy nó sẽ trả về instance của Category. Tuy nhiên trong Model Category không hề có property nào là posts cả, vậy cách gọi $category->posts sẽ trả về cái gì ?

Câu trả lời là Laravel sử dụng các Magic Method của PHP để biến việc gọi property posts trở nên hợp lệ. Và nó sẽ trả về kết quả của quan hệ posts. Tức $category->posts ở đây sẽ tương đương với $category->posts()->get(). Một điều đặc biệt của phép gọi qua property là Laravel sẽ chỉ query DB ở lần đầu tiên, nhận về kết quả và sẽ lưu kết quả đó vào property mới tạo ra.

Ở trên ta có Category quan hệ hasMany với Post, do đó $category->posts sẽ trả về một Collection (Collection là một Class trong Laravel giúp quản lý Array được dễ dàng và thuận tiện hơn).

Như vậy ta có thể thấy cách làm việc của 2 câu lệnh nêu trên sẽ là như sau:

  • $category->posts->count(): Đầu tiên $category->posts sẽ query DB để lấy ra tất cả các posts thuộc về category hiện tại. Kết quả là một Collection. Sau đó hàm count() được gọi trên Collection này để đếm số lượng phần tử của Collection. Nó đơn giản cũng giống như hàm count() đếm số lượng phần tử của mảng vậy. Tức với câu lệnh này ta sẽ lấy hết posts thuộc về category trong DB ra, và dùng xử lý PHP để đếm số lượng posts đó. Bạn gọi câu lệnh này bao nhiêu lần đi chăng nữa thì cũng chỉ có một lần duy nhất câu query DB được thực hiện, đó là ở lần gọi đầu tiên.
  • $category->posts()->count(): Đầu tiền $category->posts() sẽ trả về instance $category với điều kiện query là quan hệ posts. Sau đó hàm count() được sử dụng là hàm của Eloquent, với nhiệm vụ build ra câu query count trong MySql chẳng hạn. Tức với câu lệnh này, ta sẽ chạy duy nhất một câu query DB là đếm số lượng bản ghi posts thuộc về category. Mỗi lần gọi câu lệnh này là một lần bạn phải query DB.

Khi đã hiểu được bản chất của 2 cách gọi kia thì bạn sẽ có thể tự xem xét xem mình nên sử dụng cách viết nào trong trường hợp nào. Chẳng hạn như bạn có một trang Category view, ở đó chỉ cần hiện ra số lượng bài viết trong Category mà không cần quan tâm đến nội dung các bài post thì rõ ràng cách gọi $category->posts()->count() là tối ưu hơn nhiều. Tuy nhiên nếu cần phải in ra cả các bài posts nữa thì cách gọi $category->posts->count() lại tốt hơn, bởi vì dù gì thì bạn cũng cần gọi $category->posts để lấy ra các bài posts, sau đó gọi thêm hàm count từ đó sẽ giúp bạn không cần phải query thêm DB một lần nữa.

Dependency Injection

Dependency Injection là một design pattern được sử dụng phổ biến trong các Framework hiện đại. Hiểu đơn giản có nghĩa là nếu một Class A hoạt động phụ thuộc vào một vài Class khác, thì thay vì khởi tạo các instance của các Class kia bên trong Class A, ta sẽ inject những instance đó vào thông qua constructor hay setter. Những instance của các Class mà Class A cần để hoạt động đó được gọi là dependency.

// Non Dependency Injection
Class A
{
    protected $classB;
    public function __construct()
    {
        $this->classB = new ClassB();
    }
}

// Dependency Injection
Class A
{
    protected $classB;
    public function __construct(ClassB $classB)
    {
        $this->classB = $classB;
    }
}

Trong khuôn khổ của bài viết này mình sẽ không đi sâu phân tích vào khái niệm Dependency Injection, đây là một khái niệm quan trọng các bạn nên tìm hiểu và nắm vững. Thay vào đó, ta hãy cùng xem qua một cách sử dụng Dependency Injection hiệu quả trong Laravel nhé.

Laravel cung cấp cho chúng ta một công cụ cực mạnh tên là Service Container. Đó là nơi quản lý Class Dependency, tức xem một Class có những Class phụ thuộc nào. Ngoài ra nó cũng giúp chúng ta thực hiện Dependency Injection một cách hiệu quả thông qua class constructor. Service Container của Laravel có thể giúp bạn inject dependency vào rất nhiều loại Class, từ Controller, Event listeners, cho đến Queue Jobs, Middleware, Console Command ...

Chẳng hạn như ta có thể inject class Mailer vào bên trong Controller như sau (ví dụ thôi nhé, còn bạn không nên thực hiện logic gửi mail bên trong Controller làm gì =)))

<?php

class UserController extends Controller
{
    /**
     * The mailer implementation.
     */
    protected $mailer;

    /**
     * Create a new instance.
     *
     * @param  Mailer  $mailer
     * @return void
     */
    public function __construct(\Illuminate\Contracts\Mail\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
}

Đơn giản như vậy đó, bạn không cần phải biết cách khởi tạo một instance Mailer như thế nào, nó cần những dependencies gì. Bạn chỉ việc type-hint tên class hay interface, và Service Container làm hết cho bạn phần còn lại.

Thậm chí bên trong Controller bạn còn có thể inject được vào bên trong từng method như sau:

class UserController extends Controller
{
    public function update(\Illuminate\Http\Request $request, $id)
    {
        $all = $request->all();
    }
}

Đương nhiên ở trên chỉ là một ví dụ về Dependency Injection trong Laravel, để tiếp cận với request, bạn có thể có nhiều cách khác nhau:

class UserController extends Controller
{
    public function update(\Illuminate\Http\Request $request, $id)
    {
        // Thông qua method injection
        $all = $request->all();
        // Thông qua helper
        $all = request()->all();
        // Thông qua Facade
        $all = \Request::all();
    }
}

Cả ba cách thức trên sẽ đều trả ra kết quả giống nhau mà thôi, và sử dụng cách thức nào thì có thể là do các thành viên trong team quyết định 😄

Có thể trong các bài viết sau này, mình sẽ đi sâu vào phân tích bản chất của các hàm helper cũng như cách hoạt động của Facade, khi đó thì các bạn sẽ hiểu rõ hơn 3 cách gọi ở trên về bản chất là có gì khác nhau hay không?

Repository Design Pattern

Repository Design Pattern hiện là một phương pháp làm việc với project Laravel rất phổ biến. Hiểu một cách đơn giản đó là bạn sẽ tạo ra một tầng Repository ở giữa ControllerModel (ORM Layer), với nhiệm vụ là thực hiện các business logic xử lý DB, từ đó tránh được việc viết Business Logic ở cả Controller lẫn Model, tạo ra những hàm có thể được sử dụng lại ở nhiều nơi khác nhau.

Với Repository Pattern, Controller sẽ không còn làm việc trực tiếp với Model nữa, nó cần xử lý liên quan đến DB, nó sẽ làm việc với Repository.

Các bạn có thể tìm hiểu thêm về Repository Design Pattern thông qua mốt số bài viết sau:

Ngoài ra bạn có thể implement Repository Pattern trong project Laravel của mình một cách dễ dàng với package Laravel 5 Repositories

Một ví dụ nhỏ, sử dụng Repository Pattern, kết hợp với Dependency Injection.

class CategoryController extends BaseController
{
    private $categoryRepository;

    // Sử dụng Dependency Injection để inject instance của CategoryRepository
    public function __construct(CategoryRepository $categoryRepository)
    {
        $this->categoryRepository = $categoryRepository;
    }

    public function show($id)
    {
        // Làm việc với DB thông qua Repository, không dùng Model nữa
        $category = $this->categoryRepository->find($id);
    }
}

Lời kết

Trên đây mình đã giới thiệu một số vấn đề đơn giản mà mình hay gặp phải trong công việc cùng những cách giải quyết mình hay sử dụng.

Hy vọng nó sẽ giúp ích được ít nhiều cho các bạn.

Từ phần tiếp theo của loạt bài viết này, mình sẽ tập trung đào sâu về những phần core của Laravel, những Design Pattern được implement bên trong Laravel, cách thức hoạt động cũng như cách sử dụng những khái niệm rất mới mẻ nhưng thú vị như Inversion Of Control, Service Provider, Contract, Facade, Middleware, Job, Event ...

Happy Coding with 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í