Một vòng Laravel (Part 3)

Tiếp nối loạt bài về Laravel, hôm nay chúng ta sẽ đi tiếp những chủ đề còn lại.

  • Mail
  • Schedule
  • Event
  • Job

Mail

Một tính năng nữa mà Laravel đã đơn giản hóa khá nhiều đi cho lập trình viên. Khởi đầu, hãy chắn chắn là có package guzzle trong project của bạn

composer require guzzlehttp/guzzle

Tiếp theo, ta chọn các mail driver để setup, tôi sẽ vẫn để mặc định là stp. hoặc nếu không thì các bạn có thể chọn mailgun, cách đăng kí cũng khá đơn giản. các bạn vào đây để đăng kí account: https://mailgun.com/signup Đăng kí xong bạn sẽ nhận được một api secret key và domain. config 2 field trong .env và thay đổi field MAIL_DRIVER thành mailgun

MAIL_DRIVER=mailgun
MAILGUN_DOMAIN=key-6b74bb12e49647024bcb6e799100065f
MAILGUN_SECRET=sandbox7738f6064bd239c58399b6511fbf6e32.mailgun.org

Tiếp đó, vẫn trong file .env, bạn cần config tài khoản đăng nhập vào host gửi mail.

MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=[email protected].com
MAIL_PASSWORD=youneverknow
MAIL_ENCRYPTION=tls
MAIL_NAME=pets

Tạo class xây dựng content cho mail

php artisan make:mail InvoiceShipping

Trong method build của InvoiceShipping.php, return view mà ta muốn gửi cho user

        return $this->view('emails.invoice');

Tạo file view emails/invoice.blade.php

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Invoice {{ $invoice->id }}</div>

                <div class="panel-body">
                    Thanks GOD! you received this email!
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

Config sender cho email. (config/mail.com)

'from' => [
    'address' => '[email protected]',
    'name' => 'luongr3',
],

Pass data cho view. ta sẽ đặt 1 filed là public $invoice để truyền dữ liệu vào view invoice.blade.php (laravel sẽ tự động truyền cho ta). (InvoiceShipping.php)


    public $invoice;
    /**
     * Create a new message instance.
     * @param Invoice $invoice
     *
     * @return void
     */
    public function __construct(Invoice $invoice)
    {
        $this->invoice = $invoice;
    }

ở trên, $invoice sẽ được inject vào method construct của InvoiceShipping, điều thường thấy trong các class của laravel.

Send email. Cú pháp send email rất đơn giản. Ta chỉ việc to cho 1 user hoặc nhiều user, nội dung email mà ta đã tạo. Mail::to(User::find(5))->send(new InvoiceShipping($invoice)); ở đây trong routes/web.php, tôi tạo 1 link để send email.

Route::get('email/send', function () {
    $invoice = \App\Models\Invoice::findOrFail(1);

    Mail::to(\App\Models\User::find(6)->email)->send(new \App\Mail\InvoiceShipping($invoice));
    echo "Send successfully";
});

Sau một loạt config trên, giờ ta sẽ đến phần test. Trước khi test thì tôi khuyên bạn nên làm 1 việc, đó là thay đổi MAIL_DRIVER thành log, lý do cho thay đổi này là để laravel log bắt tất cả email mà ta gửi, giúp việc test dễ dàng hơn, và đương nhiên, fix bug dễ dàng hơn. Vào link www.your-project.com/email/send, nếu show ra "Send successfully" và trong file log có nội dung như sau thì bạn đã thành công.

[2016-12-13 04:31:15] local.DEBUG: Message-ID: <[email protected]-pets.com>
Date: Tue, 13 Dec 2016 04:31:15 +0000
Subject: Invoice Shipping
From: [email protected].com
To: [email protected].com
MIME-Version: 1.0
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Invoice 1</div>

                <div class="panel-body">
                    Thanks GOD! you received this email!
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

Schedule

Có lẽ nói tới Cron Job thì nhiều người sẽ quen hơn. Múc đích là để ta tạo các task, chạy tự động trên server. Vẫn với ví dụ gửi mail ở trên, nếu ta muốn cứ hàng tuần, khách hàng nhận 1 email quảng cáo các sản phẩm mới của công ty, thì ta sẽ tạo 1 Schedule task chạy tự động trên server, cứ cách 1 tuần là task này chạy và làm ... gì thì làm.

OK, bắt đầu thôi. Ta sẽ tạo 1 cron entry trên ubuntu.

sudo nano /etc/crontab

Thêm * * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1 vào cuối file, mục đích của việc này là để server call Laravel Command Scheduler mỗi phút, còn laravel scheduler call các task của bạn theo lịch thế nào thì do bạn thiết lập.

Tạo Schedule Task Các bạn vào app/console/commands/Kernel.php, trong method schedule chính là cách schedule task mà ta sẽ khai báo. Ở đây tôi định làm 1 trang đá bóng, mà hệ thống sẽ tự động thông báo cho người dùng nếu sắp có một trạn bóng, tự động kiểm tra 5 phút 1 lần.

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
   /**
    * The Artisan commands provided by your application.
    *
    * @var array
    */
   protected $commands = [
       Commands\MatchStartAlert::class,
   ];

   /**
    * Define the application's command schedule.
    *
    * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
    * @return void
    */
   protected function schedule(Schedule $schedule)
   {
       // $schedule->command('inspire')
       //          ->hourly();
       $schedule->command('alert:match_start')->everyFiveMinutes();
   }
}

Chi tiết về các Frequency của scheduler, bạn có thể tham khảo tại đây https://laravel.com/docs/5.3/scheduling Các bạn để ý sau $schedule->command là mã của scheduler task alert:match_start. Sẽ phải có 1 class để handle việc làm gì mỗi 5 phút, tương ứng mới mã này. Ta sẽ tạo class đó như sau:

php artisan make:console MatchStartAlert --command=alert:match_start

Lệnh này sẽ taọ 1 file MatchStartAlert.php trong folder app/console/commands/.

<?php
namespace App\Console\Commands;

use App\Repositories\Message\MessageRepositoryInterface;
use Illuminate\Console\Command;

class MatchStartAlert extends Command
{
   /**
    * The name and signature of the console command.
    *
    * @var string
    */
   protected $signature = 'alert:match_start';

   /**
    * The console command description.
    *
    * @var string
    */
   protected $description = 'Command description';
   protected $messageRepository;

   /**
    * Create a new command instance.
    *
    * @return void
    */
   public function __construct(MessageRepositoryInterface $messageRepository)
   {
       $this->description = trans('message.alert_user_about_match_start');
       $this->messageRepository = $messageRepository;
       parent::__construct();
   }

   /**
    * Execute the console command.
    *
    * @return mixed
    */
   public function handle()
   {
       \Log::info('aaa');
       $this->messageRepository->alertMatchStart();
   }
}

Có 2 chỗ mà bạn cần để ý, thứ nhất là property $signature = "alert:match_start" chính là mã để nhận diện schedule task này, cái thứ 2 là method handle. Đây chính là nơi mỗi 5 phút, ta "làm gì đó". Ở đây trong handle, tôi đặt 1 cái log, như vậy mỗi 5 phút, trong laravel.log sẽ phải có 1 string là 'aaa', còn nếu không, tức là cron job này vẫn chưa hoạt động. ok, giờ ngồi đợi thôi. =)) just kidding. Bạn có thể chỉnh lại everyFiveMinutes thành second hay gì đó nhanh hơn giúp bạn check. Còn đây là thành quả ngồi đợi của tôi. (laravel.log)

[2016-12-13 14:00:02] local.INFO: aaa
[2016-12-13 14:05:02] local.INFO: aaa
[2016-12-13 14:10:02] local.INFO: aaa
[2016-12-13 14:15:01] local.INFO: aaa

Bạn cũng có thể dùng lệnh sau để check xem đã đặt Schedule task nào chưa bằng

php artisan list

Tôi thu được như sau

  up                   Bring the application out of maintenance mode
 alert
  alert:match_start    Alert user about match is starting
 app
  app:name             Set the application namespace

Event

Event là một chức năng laravel thêm vào để hỗ trợ cho việc xử lý hướng sự kiện. Khi mà từ 1 điều kiện, ta có thể trigger 1 hoặc nhiều event khác nhau. Giả sử ta muốn gửi cho user 1 message "Chào mừng tới Website của chúng tôi" ngay sau khi user đăng kí tài khoản, ta làm như sau.

Tạo Event

php artisan make:event UserCreated

Trong file UserCreated.php, ta tạo 1 property public $user và type-hint User để truyền dữ liệu (mà listener cần để xử lý) vào property này, sau này thì $user sẽ được truyền sang listener.

<?php
namespace App\Events;

use Illuminate\Queue\SerializesModels;

class UserCreated extends Event
{
   use SerializesModels;

   public $user;

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

Tạo Listener

php artisan make:listener UserCreatedListener --event=UserCreated

Trong file UserCreatedListener có method handle chính là nơi ta xử lý dữ liệu từ Event ném tới, ở đây thì laravel đã inject sẵn cho ta UserCreated $event. Nêú muốn lấy dữ liệu $user mà tôi nói ở trên để xử lý, ta làm như sau:

<?php
namespace App\Listeners;

use App\Events\UserCreated;
use App\Repositories\Message\MessageRepositoryInterface;

class UserCreatedListener
{
   private $messageRepository;

   /**
    * Create the event listener.
    *
    * @return void
    */
   public function __construct(
       MessageRepositoryInterface $messageRepository
   ) {
       $this->messageRepository = $messageRepository;
   }

   /**
    * Handle the event.
    *
    * @param  CreateBet  $event
    * @return void
    */
   public function handle(UserCreated $event)
   {
       $user = $event->user;
       $data = [
           'user_id' => $user['id'],
           'content' => trans('message.hello_user'),
           'type' => config('common.message.type.user_event'),
           'target' => $user['id'],
        ];
        User::store($data);
   }
}

Và việc cuối cùng cần phải làm, bind Event với Listener. Thực ra ngược lại với đúng, Với 1 event, ta sẽ có thể trigger nhiều listener khác nhau tùy thuộc mục đích. Ta sẽ làm việc này ở property $listen, trong file EventServiceProvider.php

protected $listen = [
   'App\Events\UserCreated' => [
       'App\Listeners\UserCreatedListener',
   ],
];

Job

Job & Queue, thực chất job được tạo ra để phục vụ cho queue. Haỹ nhớ tới các event và cron job mà ta vừa tạo ở trên. Giả sử trên hệ thống của ta có 10.000 user, và ta check sắp có một trận đá bóng sắp tới và hệ thống sẽ gửi message cho 10k người này ... cùng 1 lúc, và sẽ còn tệ hơn nếu thay vì gửi mesage, ta gửi ... mail. Lúc này sẽ cần tới Queue, mà muốn dùng queue thì cần phải hiểu 2 khái niệm nữa: connection và job.

Connection chính là ... connection, dùng để connect tới database (lưu trữ các job) và các backend service như sqs, redis, beanstalkd. Một connection thì có thể gồm nhiều Queue, và trogn các backend service thì đã thiết lập 1 queue mặc định, các job sẽ được dispatch tới queue này trong trường hợp không chỉ rõ queue nào. Connection là nơi vận hành queue, giống kiểu 1 ga điện ngầm vây, mỗi con tàu trong ga là queue, còn job là các hành khách, được gửi tới các queue rồi chuyển đi.

Ok, giờ ta sẽ triển khai Queue cho việc gửi email tới cho user. Đầu tiên, tạo table cho các job

php artisan queue:table
php artisan migrate

Tùy vào công việc mà bạn có thể dùng tới các driver còn lại hay không, nhưng mình khuyên là nên cài ít nhất là predis. Thêm các packages sau vào composer.json rồi chạy composer update

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

ok, việc tiếp theo là tạo các job và dùng thôi. Tôi sẽ tạo 1 job là SendEmail, khi tạo ra thì job đã implements sẵn 1 ShouldQueue, tức là bạn sẽ không phải lo về queue nào queue nào nữa, đây là một asynchronous queue. Class có method handle(), chính là nơi xử lý send email, và ta sẽ type-hint $user cần gửi mail và $invoice vào.

php artisan make:job SendEmail
<?php

namespace App\Jobs;

use App\Mail\InvoiceShipping;
use App\Models\Invoice;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mail;

class SendEmail implements ShouldQueue
{
   use InteractsWithQueue, Queueable, SerializesModels;

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

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

   }
}

Giờ khi ta cần gửi mail (truy cập vào link /email/send). Ta sẽ tìm tất cả các $users, với mỗi $user, ta sẽ tạo ra các job tương ứng để gửi mail tới $user này

Route::get('email/send', function () {
   $invoice = \App\Models\Invoice::findOrFail(1);
   $users = \App\Models\User::all();
   foreach ($users as $user) {
       dispatch(new \App\Jobs\SendEmail($user, $invoice));
   }
});

Ngoài ra, ta có thể specify queue hay connection bằng cách:

//specify queue
$job =  (new SendEmail($user, $invoice))->onQueue(‘what-ever-you-want’);
dispatch($job);
//specify connection
$job = (new SendEmail($user, $invoice))->onConnection(‘predis’);
dispatch($job);

Queue worker monitor: Supervisor Queue worker được laravel include vào để xử lý các job khi được push vào queue. Còn supervisor là bộ giám sát tiến trình cho hệ thống linux, kết hợp 2 thành phần này lại giúp quản lý các job tốt hơn, ngoài ra còn hõ trợ cả việc handle error, exception. Chi tiết cách config supervisor bạn có thể tham khảo tại đây: https://laravel.com/docs/5.3/queues#supervisor-configuration


All Rights Reserved