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 ...
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.
- Schedule
- Event
- Job
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=luongr3@gmail.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="awidth=device-awidth, 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' => 'luongr3@gmail.com', 'name' => 'luongr3', ],
Pass data cho view. ta sẽ đặt 1 filed là public $$nvoice để 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, $$nvoice 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 = AppModelsInvoice::findOrFail(1); Mail::to(AppModelsUser::find(6)->email)->send(new AppMailInvoiceShipping($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: <fb3840c143d001c41279bd62590b5809@local-pets.com> Date: Tue, 13 Dec 2016 04:31:15 +0000 Subject: Invoice Shipping From: luongr3@gmail.com To: luongr3@hotmail.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="awidth=device-awidth, 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 AppConsole; use IlluminateConsoleSchedulingSchedule; use IlluminateFoundationConsoleKernel as ConsoleKernel; class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * * @var array */ protected $commands = [ CommandsMatchStartAlert::class, ]; /** * Define the application's command schedule. * * @param IlluminateConsoleSchedulingSchedule $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 $$chedule->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 AppConsoleCommands; use AppRepositoriesMessageMessageRepositoryInterface; use IlluminateConsoleCommand; 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 $$ignature = "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 AppEvents; use IlluminateQueueSerializesModels; 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 AppListeners; use AppEventsUserCreated; use AppRepositoriesMessageMessageRepositoryInterface; 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 $$isten, trong file EventServiceProvider.php
protected $listen = [ 'AppEventsUserCreated' => [ 'AppListenersUserCreatedListener', ], ];
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 AppJobs; use AppMailInvoiceShipping; use AppModelsInvoice; use AppModelsUser; use IlluminateBusQueueable; use IlluminateQueueSerializesModels; use IlluminateQueueInteractsWithQueue; use IlluminateContractsQueueShouldQueue; 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 $$ser này
Route::get('email/send', function () { $invoice = AppModelsInvoice::findOrFail(1); $users = AppModelsUser::all(); foreach ($users as $user) { dispatch(new AppJobsSendEmail($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