Một vòng laravel (Part 4)
Bài đăng này đã không được cập nhật trong 8 năm
- Inversion Of Control
- Service Provider
- Contracts
- Facade 4 khái niệm core trong laravel
IoC (Inversion of Control)
IoC là một design pattern mà đi ngược lại với các design của lập trình truyền thống. Nếu với cách thức lập trình truyền thống, ta sẽ (khởi tạo và) gọi các dependency khi cần, thì với IoC, tất cả những thứ ta cần đều sẽ được truyền vào từ trước và ban sẽ gọi khi dùng đến. Ví dụ thế này, trong class Product, tôi cần dùng đến cả Category. Thì sẽ có sự khác nhau giữa lập trình có áp dụng IoC và không.
// not apply IoC
class Product
{
public function addProductCategory($product)
{
$category = new Category();
$product->category = $category->find($product->category_id);
return $product;
}
}
// apply IoC
class Product
{
public $category;
public function __construct(Category $category)
{
$this->category = $category;
}
public function addProductCategory($product)
{
$product->category = $this->category;
return $product;
}
}
Một trong những áp dụng tốt nhất của IoC là Dependency Injection. Và laravel thì áp dụng Dependency Injection (hay DI) một cách khá triệt để =)). (chính là service container)
Với DI (cũng có thể coi là 1 design pattern): ta vẫn áp dụng theo IoC, các dependency sẽ được inject vào constructor hoặc setter của class sử dụng nó. Nhưng các module (class) sẽ không giao tiếp tực tiếp với nhau, mà thông qua 1 interface.
Vẫn với ví dụ trên, ta sẽ không inject Category, mà sẽ là 1 CategoryInterface hoặc ICategory, còn việc Inject inteface nhưng vẫn có thể sử dụng được instance của Category, how and why thì do DI container xử lý. (tức là sẽ do các framework xử lý, ta ko cần phải lo).
// apply IoC
class Product
{
public $category;
public function __construct(CategoryInterface $category)
{
$this->category = $category;
}
public function addProductCategory($product)
{
$product->category = $this->category;
return $product;
}
}
Với riêng trong laravel, ta sẽ cần phải bind CategoryInterface với Category, mục đích của việc này là để laravel biết rằng khi cần sử dụng tới CategoryInterface, nó sẽ inject instance của Category. Ta tiến hành bind trong AppServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind('CategoryInterface', 'Category');
}
}
Giờ thì ta sẽ chỉ việc khởi tạo 1 $product = new Product()
, những thành phần còn lại ($category) sẽ được tự động inject vào bởi laravel. (Lưu ý là Category sẽ phải implement CategoryInterface)
Việc sử dụng DI mạng lại rất nhiều lợi ích:
- Giảm sự phụ thuộc giữa các module. Giả sử hệ thống của ta đang sử dụng eloquent, và một ngày đẹp trời nào đó, khách hàng nói rằng, họ thấy query builder nhanh hơn, và muốn chuyển sang. Nếu sự phụ thuộc giữa các module với eloquent lớn, đó sẽ là 1 big problem. Nhưng vấn đề sẽ dễ giải quyết hơn nếu tất cả chỉ là 1 cái bind =)) . Ta sẽ tạo 1 class mới sử dụng query builder, và bind interface cũ với module này, và vấn đề được giải quyết awesome ha .
- Code dễ maintain, dễ thay thế (thực chất vẫn từ ý trên).
- Tuân thủ SOLID, Repository Pattern. Controller sẽ ko phụ thuộc trực tiếp model nữa.
- Dễ nhận biết các dependency hơn khi tập hợp tất cả vào 1 chỗ (constructor hoặc setter)
Nhưng cũng kèm theo kha khá bất lợi:
- Luồng khó hiểu, tất nhiền rồi, không phải ai cũng hiểu được dependency injection (mặc dù triển khai khá dễ dàng).
- khó hiểu, rắc rối hơn, dẫn tới khó debug hơn.
Service Provider
Trong phần trước về IoC, DI, ta có nhắc đến 1 khái niệm là Service Provider, đây được coi là "central place" của các phần khác trong laravel. Service provider có nhiệm vụ khởi động các thành phần cần thiết cho hệ thống và cho application của bạn. Nếu còn nhớ thì không chỉ phần về IoC, mà cả phần về event, tôi cũng đã phải động tới Service Provider, ta sẽ phải set Event và Listener trong property $listen của EventServiceProvider. Rồi khi làm về Gates và Policies, cả 2 đều phải được register trong function boot của AuthServiceProvider. Bạn có thể đăng kí nhiều thứ với Servicde Provider như: service container binding, event listener, middleware, event route.
Ngoài ra nếu để ý khi install các package dành cho laravel như laravel debugbar, ide helper, socialite, ... ta đều có 1 thao tác là thêm service provider class của package đấy vào providers
. Điều này sẽ giúp laravel biết những provider nào cần phải load cùng với application, nhưng tất nhiên, đa phần những provider này là deferred provider, chúng sẽ được load chỉ khi nào được dùng tới.
Ở trong folder provider, laravel có cung cấp cho ta một vài ServiceProvider mặc định
- AuthServiceProvider: phục vụ mapping policy với model và register các service provider về authentication và authorization (như Gates chẳng hạn)
- EventServiceProvider: mapping event với listener, đăng kí các event.
- RouteServiceProvider: define các thành phần và xử lý cần thiết cho router.
- AppServiceProvider: dành cho những application còn lại. Mình thì ít dùng cái này, khi cần thì mình sẽ register hẳn một service provider.
Tạo Service Provider Ở đây tôi sẽ hướng dẫn các bạn tạo RepositoryServiceProvider, phục vụ cho việc bind các Repository và interface.
php artisan make:provider RepositoryServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App;;
use App\Repositories\User\UserRepository;
use App\Repositories\User\UserRepositoryInterface;
class RepositoryServiceProvider extends ServiceProvider
{
protected $defer = true;
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
view()->share('layout', 'admin.layout.layout');
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
App::bind(UserRepositoryInterface::class, UserRepository::class);
}
public function provides()
{
return [UserRepositoryInterface::class];
}
}
Lưu ý:
- boot(): ở đây sẽ khai báo các thành phần mà bạn muốn share giữa các view. Giả sử tôi muốn share số lượng user đang online trên site, dọc qua tất cả các page chẳng hạn, thì tôi sẽ làm như sau
view()->share('onlineUser', $numberOfOnlineUser);
- register(): ở đây ta sẽ đặt bind, dùng Facade App hoặc app()->bind().
- property $defer và function provides() sẽ được nhắc tới sau, nhưng ko có cũng ko sao.
Đăng kí Provider (config/app.php)
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\RepositoryServiceProvider::class,
Deferred Provider ở trên mình có nhắc tới deferred provider, đó là những provider sẽ chỉ được load khi nào nó được dùng tới, ví dụ như mình vừa thiết lập 1 binding giữa UserRepository với UserRepositoryInterface, chỉ khi nào UserController được khởi tạo, UserRepositoryInterface được dùng tới thì laravel mới inject instance của UserRepository vào. Cách thực hiện: trong RepositoryServiceProvider, thêm $defer = true và set thêm 1 function là provides, function này sẽ trả lại service provider binding mà được register bởi provider (UserServiceProviderInterface).
Contracts
Thực tế thì chẳng cần hiểu về contract thì hầu hết các công việc với laravel đều diễn ra thuận lợi, và tôi tin là bạn cũng ít khi động đến nó, trừ khi là viết package hoặc contribute cho laravel.
Contract ở đây chính là interface mà thôi. vâng, interface, interface vs abstract class ấy. Bạn có thể vào vendor/laravel/framework/src/illuminate/contracts để xem một loạt các interface mà laravel đã quy định.
Dùng contract (hay interface) để làm gì. Hãy nhìn lại 2 concept ở trên về Service provider và service container. Để Dependency Injection, thì chắc chắn là phải dùng tới interface, vâng, đó chính là contract. Cần phải có 1 sự rằng buộc nào đó đảm bảo 2 module, 1 high module và 1 low module có thể làm việc với nhau mà không có rằng buộc quá lớn =)) . contract sẽ đảm bảo điều đó.
Lấy lại ví dụ về eloquent và query builder. Ta có EloquentUserRepository sử dụng eloquent để thao tác với model, giờ khách hàng thích query builder hơn, đâp đi xây lại 1 cái QueryBuilderUserRepository, nhưng xây lại những cái gì để đảm bảo controller vẫn hoạt động tốt. Nếu có contract thì mọi việc sẽ đơn giản hơn nhiều, dù là EloquenUserRepository hay QueryBuilderRepository thì khi đã implement 1 UserRepositoryInterface đều sẽ phải có các method đảm bảo cho controller hoạt động trơn tru.
Facade
Phần cuối cùng trong series về laravel của mình, Facade, thành phần được dùng rất nhiều trong laravel, nhưng thực tế thì ... làm rất ít.
Bản chất của Facade chỉ là một cây cầu nối giữa bạn và service container. Nếu còn nhớ về khái niệm service container, thì đó là nói quản lý các class dependence và thực hiện Dependency Injection. Service đã tạo và bind rất nhiều thứ cho ta dùng, và việc của bạn chỉ là inject nó vào nơi bạn cần hoặc resolve nó ra mà thôi.
Nhưng Facade thì không nghĩ vậy, 1 dòng và 1 statement thôi. Hãy đến với ví dụ nhau:
Ta bind instance của Category với container Category
$this->app->bind(‘Category’, function() {
return new Category();
});
// IoC
$category = app(‘Category’);
$checkPorduct = $category->checkProduct($productId);
// Facade
$checkProduct = Category::checkProduct($productId);
Rõ ràng Facade nhanh gọn hơn, và lại không cần phải hiểu về IoC, về DI. Nhưng chắc chẳng IDE nào hiểu được cách mà Facade làm việc (bởi nó xư lý qua magic function __callstatic) và khó trace khi debug.
Nhưng bất chấp các việc đó, mình vẫn khuyên các bạn nên sử dụng Facade, đã từng có 1 phiên bản laravel loại bỏ hoàn toàn Facade bởi một vài điều không tốt của nó, nhưng cho đến giờ thì nó vẫn sống khỏe tới laravel 5.3.
Ngoài ra, nếu bạn cần thêm 1 lựa chọn để tận dụng tài nguyên to lớn của service container thì nên sử dụng các helper function, cách thức hoạt động chả khác gì Facade, nhưng lại chính chủ =))
All rights reserved