Laravel và những điều cần biết - phần 3

Mục lục

Tiếp theo loạt bài viết về Laravel. Hôm nay chúng ta sẽ đi tìm hiểu:

  • Event
  • Job
  • Inversion Of Control
  • Service Provider
  • Contracts
  • Facade

1. Event

Giới thiệu

Event là một chức năng của Laravel để hỗ trợ xử lý hướng sự kiện. Khi một sự kiện xảy ra thì ta có thể thực hiện một hành động nào đó. Ví dụ như khi một thành viên đăng kí vào hệ thống thì gửi thông báo chào mừng hoặc các điều khoản cần tuân thủ.

Để hiểu về event của laravel ta phải hiểu 2 khái niệm:

  • Event: là sự kiện cần lắng nghe. Các lớp tổ chức sự kiện được đặt ở thư mục app/Events
  • Listener: là các hành động được thực hiện khi sự kiện xảy ra. Các lớp Listener được đặt ở thư mục app/Listeners

Và tất nhiên ta cần kết nối sự kiện với hành động để khi sự kiện xảy ra thì hành động sẽ được thực hiện. Việc kết nối này sẽ được khai báo trong mảng $listen ở file app/Providers/EventServiceProvider.php.

Trong mảng này Event sẽ được thể hiện ở key, value là một mảng các class Listener.

protected $listen = [
    'App\Events\SomeEvent' => [
        'App\Listeners\EventListener',
    ],
];

Ngoài cách đăng kí Event - Listener trong mảng $listen bạn cũng có thể đăng kí trong phương thức boot() trong EventServiceProvider.

public function boot()
{
    parent::boot();
    Event::listen('event.name', function ($foo, $bar) {
        //
    });
}

Tạo Event, Listener

Nếu bạn đã tạo liên kết giữa Event với Listener trong EventServiceProvider thì chỉ cần chạy lệnh sau:

php artisan event:generate

Tất cả các class EventListener đã khai báo trong mảng $listen sẽ được tạo mới. Và tất nhiên những class đã được tạo rồi sẽ giữ nguyên.

Đây là cách nhanh nhất để tạo nhiều Event, Listener cùng lúc.

Bạn cũng có thể tạo thủ công Event như sau:

php artisan make:event EventName

Ví dụ: Tạo Event UserCreated để bắt sự kiện khi người dùng tạo tài khoản.

php artisan make:event UserCreated

Một file UserCreated.php sẽ được tạo trong thư mục app/Events với 1 class UserCreated tương ứng.

Tiếp theo ta tạo Listener tương ứng với Event UserCreated bên trên.

php artisan make:listener UserCreatedListener --event=UserCreated

Và có một câu hỏi đặt ra là Listener này đã được kết nối với Event bên trên chưa?

Câu trả lời là chưa. Chúng ta vẫn phải khai báo kết nối cặp Event với Listener này trong EventServiceProvider.

Cách này rất "thủ công", dài dòng và phải thực hiện nhiều thao tác đúng như tên gọi của nó. Mình vẫn thích dùng cách đầu hơn vì nó "công nghiệp" hơn. 😃

Điều tiếp theo chúng ta cần quan tâm là làm sao có thể truyền biến trong Event vào Listener.

Rất đơn giản trong class Event bạn chỉ cần khai báo biến public và khởi tạo nó trong hàm __construct laravel sẽ tự động truyền biến này vào Listener cho ta.

Ví dụ: Ta muốn truyền $user vào Listener

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class UserCreated
{
    use InteractsWithSockets, SerializesModels;
    public $user;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($user)
    {
         $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

Và trong Listener sẽ cứ tự nhiên mà sử dụng thôi.

Ví dụ: Gửi mail xác nhận khi người dùng đăng kí thành công.

namespace App\Listeners;

use App\Events\UserCreated;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mail;
use App\Mail\MailVerify;

class UserCreatedListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  UserCreated  $event
     * @return void
     */
    public function handle(UserCreated $event)
    {
        $user = $event->user;
        Mail::to($user->email)->send(new MailVerify($user))
    }
}

Kích hoạt sự kiện

Để kích hoạt 1 sự kiện ta dùng helper event()

event(new App\Events\UserCreated($user));

Hoặc dùng facade Event

Event::fire(new App\Events\UserCreated('1234', 'abcd'));

Hàng đợi cho Listeners

Nếu một công việc cần nhiều thời gian để thực hiện, không thể thực hiện ngay được. Ví dụ như có nhiều người cùng đăng kí 1 lúc và việc gửi mail xác nhận cần khá nhiều thời gian. Lúc này chúng ta cần phải cho vào hàng đợi để công việc thực hiện tuần tự. Không làm quá tải server hay tắc nghẽn hệ thống. Để làm được điều này Listeners chỉ cần implements ShouldQueue là vấn đề sẽ được giải quyết.

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class UserCreatedListener implements ShouldQueue
{
    //
}

Như vậy 1 Event có thể có nhiều Listener. Có thể dùng 1 Listener cho nhiều Event không?

Laravel giải quyết việc này bằng cách tạo ra Event Subscribers.

Event Subscribers

Tạo Listeners Subscribers như sau:

namespace App\Listeners;

class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function onUserLogin($event) {}

    /**
     * Handle user logout events.
     */
    public function onUserLogout($event) {}

    /**
     * Register the listeners for the subscriber.
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen(
            'Illuminate\Auth\Events\Login',
            'App\Listeners\[email protected]'
        );

        $events->listen(
            'Illuminate\Auth\Events\Logout',
            'App\Listeners\[email protected]'
        );
    }

}

Trong class UserEventSubscriber trên ta kết nối sự kiện với các phương thức Listeners trong subscribe() bằng phương thức $events->listen(). Biến đầu tiên là tên class sự kiện. Biến tiếp theo là hàm cần thực hiện.

Để đăng kí Event Subscribers ta thêm vào mảng $subscribe trong EventServiceProvider.

protected $subscribe = [
    'App\Listeners\UserEventSubscriber',
];

2. Job

Gỉa sử có một sản phẩm mới và bạn muốn thực hiện công việc gửi mail tới tất cả các thành viên. Việc gửi mail tốn rất nhiều thời gian và tài nguyên không thể làm đồng thời cùng 1 lúc. Do vậy laravel đã thiết kế ra Job và Queue. Job là một công việc cụ thể. Queue giúp bạn trì hoãn việc xử lý một công việc nào đó, hoặc thực hiện nhiều công việc 1 cách tuần tự.

Trước khi bắt đầu với Queue, ta cần tìm hiểu thêm một khái niệm nữa là connections . Connections dùng để kết nối đến cơ sở dữ liệu lưu trữ các Job hoặc các dịch vụ khác như Amazon SQS, Beanstalk, Redis. Một connection thì có thể gồm nhiều Queue, và trong mỗi connection đã thiết lập 1 queue mặc định, các job sẽ được cho vào queue này trong trường hợp không chỉ rõ queue nào.

Bây giờ bắt đầu triển khai một hàng đợi cho việc gửi mail.

Tạo bảng cho các Job

php artisan queue:table
php artisan migrate

Cài các gói cần thiết

Tùy công việc cụ thể mà bạn có thể sử dụng các gói cần thiết.

Thêm vào file composer các dòng sau rồi chạy composer update:

"aws/aws-sdk-php": "~3.0",
"pda/pheanstalk": "~3.0",
"predis/predis": "~1.0"

Tạo Job

Tất cả các Job được lưu trong thư mục app/Jobs. Bạn có thể tạo Job mới bằng cách sử dụng câu lệnh Artisan:

php artisan make:job SendEmail

Class job được tạo ra đã implements Illuminate\Contracts\Queue\ShouldQueue interface. Điều này có nghĩa là công việc sẽ được đẩy vào hàng đợi không đồng bộ.

Mỗi Job class sẽ có một phương thức handle(). Đây chính là phương thức xử lý công việc của Job. Cụ thể trong trường hợp này là việc gửi mail.

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Models\Product;
use App\Models\User;
use Mail;
use App\Mail\MailHotProduct;

class SendReminderEmail implements ShouldQueue
{

    use InteractsWithQueue,
        Queueable,
        SerializesModels;

    protected $user;
    protected $product;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(User $user, Product $product)
    {
        $this->user = $user;
        $this->product = $product;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Mail::to($this->user->email)->send(new MailHotProduct($this->product));
    }

}

Thực hiện Job

Khi đã tạo xong Job, ta có thể thực hiện nó bằng helper dispatch().

 $product = \App\Models\Product::where('is_hot', true);
 $users = \App\Models\User::all();
 foreach ($users as $user) {
     dispatch(new \App\Jobs\SendEmail($user, $product));
 }

Công việc sẽ được thêm vào hàng đợi mặc định và thực hiện. Bạn có thể delay thời gian thực hiện bằng cách dùng phương thức delay():

dispatch(new \App\Jobs\SendEmail($user, $product)->->delay(Carbon::now()->addMinutes(10)));

Chỉ định queue và connection:

//specify queue
$job =  (new SendEmail($user, $product))->onQueue(‘processing’);
dispatch($job);

//specify connection
$job = (new SendEmail($user, $product))->onConnection(‘predis’);
dispatch($job);

3. Inversion Of Control

Giới thiệu

Inversion of Control (IoC) là một design pattern được tạo ra để code có thể tuân thủ nguyên lý Dependency Inversion - chữ D trong nguyên lý SOLID . IoC được hiểu đơn giản là cung cấp cho một object những object nó phụ thuộc (dependencies) từ bên ngoài truyền vào mà không phải khởi tạo từ trong class.

Xét ví dụ sau:

class StandardLogger {

    public function info($message)
    {
      printf("[INFO] %s \n", $message);
    }
}

class MyLog {
    public $logger;

    public function __construct() {
        $this->logger = new StandardLogger();
    }

    public function info($string)
    {
        return $this->logger->info($string);
    }
}

//Main application, somewhere else
$myLog = new MyLog();
$myLog->info('This object depend on another object');

Ví dụ trên đã chỉ ra class MyLog bị phụ thuộc vào class StandardLogger. Như vậy không tuân thủ theo nguyên lý Dependency Inversion. Khi sử dụng IoC thì đoạn code trên sẽ được viết lại như sau:

class StandardLogger {

    public function info($message)
    {
      printf("[INFO] %s \n", $message);
    }
}

class MyLog {
    public $logger;

    public function __construct(StandardLogger $logger) {
        $this->logger = $logger;
    }

    public function info($string)
    {
        return $this->logger->info($string);
    }
}

//Main application, somewhere else
$myLog = new MyLog( new StandardLogger());
$myLog->info('This object depend on another object');

Hiện tại thì khi chúng ta muốn chuyển sang một loại logger khác (ví dụ như FileLogger hay MongoDBLogger) chúng ta phải sửa lại __construct của class MyLog. Việc này rất tốt thời gian và công sức nếu ta sử dụng logger ở rất nhiều class.

Để giải quyết vấn đề này, ý tưởng đặt ra là tạo một interface có thể đại diện cho tất cả các loại logger. Khi cần dùng loại logger nào thì chỉ cần truyền vào từ bên ngoài. Việc này được gọi là Dependency Injection (DI). Đây là một trong những áp dụng tốt nhất của IoC.

interface LoggerInterface 
{
    function info($message);
}

class StandardLogger implements LoggerInterface
{

    public function info($message)
    {
        printf("[INFO] %s \n", $message);
    }
}

class FileLogger implements LoggerInterface 
{

    public function info($message) 
    {
        file_put_contents('app.log', sprintf("[INFO] %s \n", $message), FILE_APPEND);
    }
}

class MyLog 
{
    public $logger;

    public function __construct(LoggerInterface $logger) 
    {
        $this->logger = $logger;
    }

    public function info($string)
    {
        return $this->logger->info($string);
    }
}
// Print to standard input/output device
$myLog = new MyLog(new StandardLogger);
$myLog->info('This object depend on another object');
// Write to file
$myFileLog = new MyLog(new FileLogger);
$myFileLog->info('This object depend on another object'); 

Các dạng DI

Có 3 dạng Dependency Injection:

  • Constructor Injection: Các dependency sẽ được container truyền vào (inject vào) 1 class thông qua constructor của class đó. Đây là cách thông dụng nhất.
  • Setter Injection: Các dependency sẽ được truyền vào 1 class thông qua các hàm Setter.
  • Interface Injection: Class cần inject sẽ implement 1 interface. Interface này chứa 1 hàm tên Inject. Container sẽ injection dependency vào 1 class thông qua việc gọi hàm Inject của interface đó. Đây là cách rườm rà và ít được sử dụng nhất.

Ưu & nhược điểm của DI

Ưu điểm

  • Giảm sự kết dính giữa các module
  • Code dễ bảo trì, dễ thay thế module
  • Rất dễ test và viết Unit Test
  • Dễ dàng thấy quan hệ giữa các module (Vì các dependecy đều được inject vào constructor)

Nhược điểm

  • Khái niệm DI khá “khó tiêu”, các developer mới sẽ gặp khó khăn khi học
  • Sử dụng interface nên đôi khi sẽ khó debug, do không biết chính xác module nào được gọi
  • Các object được khởi tạo toàn bộ ngay từ đầu, có thể làm giảm performance
  • Làm tăng độ phức tạp của code

Dependency Injection trong laravel - Service container

Khi áp dụng kỹ thuật DI này, thì một vấn đề khác lại nảy sinh, làm thế nào chúng ta biết được lớp MyLog này phụ thuộc vào những lớp nào để khởi tạo nó? Việc tạo ra một instance của class MyLog rất đơn giản nếu như nó chỉ phụ thuộc trực tiếp vào một class khác. Tuy nhiên có khả năng xảy ra trường hợp phụ thuộc lồng nhau, ví dụ như MyLog--> DBLogger --> DatabaseAccess. Và nó gây rất nhiều khó khăn cho việc khởi tạo một object mà chúng ta cần, bởi vì danh sách các lớp phụ thuộc lồng nhau rất sâu.

Để giải quyết điều này, người ta nghĩ ra Dependency Injection Container hay còn gọi là Inversion of Control Container (IoC container). Thuật ngữ Inversion of Control mang tính tổng quát hơn Dependency Injection. Về bản chất thì IoC Conainter là một tấm bản đồ, hay một dịch vụ tổng đài cuộc gọi. Nó cho ta biết một lớp phụ thuộc vào những lớp class nào khác và phân giải được những class đó bằng kỹ thuật Reflection, hoặc từ danh sách đã được developer đăng ký trước.

Trong Laravel, từ phiên bản 5.0 trở đi gọi IoC container là Service container.

Sử dụng cơ bản

Thử lấy một ví dụ đơn giản về 2 tầng phụ thuộc của một class.

class Car {
    public $enigne;
    public function __construct(Engine $enigne) {
        $this->enigne = $enigne;
    }
}
class Engine {
    public $piston;
    public function __construct(Piston $piston) {
        $this->piston = $piston;
    }
}
class Piston {}

Khi ta muốn khởi tạo một đối tượng $car = new Car(); thì php sẽ báo lỗi:

Argument 1 passed to Car::__construct() must be an instance of Engine, none given,...

Cũng dễ hiểu vì class Car phụ thuộc vào class Engine mà class này lại phụ thuộc vào class Piston. Trong Laravel, nếu chúng ta dùng App::make thì Service container trong Laravel sẽ tự động phân giải dependencies của class Car và giúp chúng ta khởi tạo đối tượng $car một cách đúng đắn.

$car = App::make('Car');

dd($car);

//Ouput
Car {#212 ▼
  +enigne: Engine {#216 ▼
    +piston: Piston {#218}
  }
}

Trở lại với ví dụ về MyLog ở trên. Nếu như ta khởi tạo object MyLog bằng App::make('MyLog') Laravel sẽ báo lỗi như sau:

Target [LoggerInterface] is not instantiable.

Hiển nhiên vì class MyLog của chúng ta nhận tham số từ __construct là một interface chứ không phải một class nên Laravel không thể khởi tạo interface đó và cho vào class MyLog được. Cho nên chúng ta phải bind LoggerInterface với một class cụ thể implement interface đó.

App::bind('LoggerInterface', 'StandardLogger');

$myLog = App::make('MyLog');

dd($myLog);
//Ouput
MyLog {#212 ▼
  +logger: StandardLogger {#214}
}

4. Service Provider

Giới thiệu

Trong laravel Service Provider được coi là trung tâm của ứng dụng. Ứng dụng và tất cả nhưng dịch vụ cốt lõi của laravel đều được khởi động qua Service Provider. Bạn có thể đăng kí nhiều thứ với Servicde Provider như: service container binding, event listener, middleware, event route. Service Provider cũng là nơi trung tâm để cấu hình ứng dụng.

Ngoài ra nếu để ý khi cài đặt 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 trong file config/app.php. Điều này sẽ giúp laravel biết những provider nào cần phải load cùng với ứng dụng. 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 thư mục app/Provider, laravel có cung cấp cho ta một vài ServiceProvider mặc định:

  • AuthServiceProvider: phục vụ liên kết policy với model và đăng ký 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.

Tạo Service Provider

Để tạo một Service Provider ta dùng câu lệnh make:provider:

php artisan make:provider RepositoryServiceProvider

Một Service Provider với tên RepositoryServiceProvider được tạo ra trong thư mục app/Provider có nội dung như sau:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Giải thích các phương thức

  • boot(): Phương thức này sẽ được chạy khi tất cả các Service Provider được đăng kí.
  • register(): ở đây ta sẽ đặt bind, dùng Facade App hoặc app()->bind().

Đăng kí Service Provider

Để đăng kí Service Provider ta chỉ cần thêm tên class vào mảng providers trong file config/app

'providers' => [
    //...,
    App\Providers\RepositoryServiceProvider::class,
    //...,
],

5. Contracts

Giới thiệu

Bản chất Contract chính là interface.

Như phần 3 mình đã giới thiệu về Inversion Of Control, ta sử dụng interface thay class để truyền vào __construct của class MyLog. Làm như vậy giúp ta dễ dàng thay đổi implementation mà không làm ảnh hưởng gì đến tính đúng đắn của chương trình.

Contracts trong laravel

Trong laravel ta thường Service Container để bind một interface với một implementation của nó.

Xét ví dụ sau:

// Dependency Injection Style
class Something
{
    protected $mailer;

    public function __constructor(Illuminate\Contracts\Mail\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sendMail()
    {
        // Sending mail
        $this->mail->send($view, $data);
    }
}

Như ví dụ trên đã sử dụng Constract Mailer. Service Container vẫn có thể sinh ra được một instance $mailer cho chúng ta mà không bị lỗi. Đó là do Contracts Mailer đã được bind với một implementation của nó.

Việc sử dụng như vậy sẽ giúp chúng ta có thể dễ dàng sử dụng Service khác thay cho Service mặc định. Bằng cách bind Constract Mailer với Service đó. Chỉ với điều kiện Service đó phải implement và thỏa mãn Constract Mailer mà thôi.

6. Facade

Facade cho phép bạn truy cập đến các hàm bên trong các service được khai báo trong Service Container bằng cách gọi các hàm static. Sử dụng Facade giúp lập trình viên viết code được đễ dàng hơn.

Nếu để ý thì trong file config/app.php có một mảng tên là aliases. Mảng được khai báo dưới dạng key là tên viết tắt, value là tên đầy đủ của class đó. Laravel sẽ sử dụng hàm class_alias() để gọi đến class tương ứng khi ta gọi tên viết tắt của nó. Ví dụ sử dụng class Auth bên trong project của mình là ta đang gọi đến class Illuminate\Support\Facades\Auth.

Illuminate\Support\Facades\Auth chính là một Facade.

Như vậy, việc gọi các hàm Route::get(), hay Auth::user() đều là đang sử dụng Facade.

Facade mặc định của Laravel đều nằm trong thư mục vendor\laravel\framework\src\Illuminate\Supports.

Tham khảo

All Rights Reserved