PHP Laravel Event & Listener

林芷蕾Alicia
11 min readMar 20, 2023

--

今年因為工作的關係開始用PHP+Laravel,以前都是用Nodejs,可能因為Nodejs有所謂的非同步概念,平常在開發上很少用到Event,如果要用的話都會搭配個幫忙管理queue的套件像是RabbitMQ,之前在nodejs需要用到event & queue時是在在面對高併發、耗時任務、micro-services之間的非同步溝通的時候。

但php應用這個概念算是蠻廣泛的,實作上也容易,需求量不大的話其實用mysql + supervisor就好。

laravel給我的感覺是把很多東西abstract了,挺好的,維護性我認為比刻Nodejs好太多,如果要手刻寫到維護性像lararvel一樣乾淨好懂,肯定要花很多心力,但laravel讓他變得簡單。Nodejs要做到類似的感覺的話可能要用Nestjs吧。

用Event的好處

抽象化邏輯,把一條api的side effect塞到事件當中,之前我是把它們幫成不同的function,舉例:

學校圖書預約系統,預約更新路由:

  • input驗證->使用Form Validation
  • 更新預約資料 (主要任務)
  • 連動更新其他預約 (ex: 如果這筆預約被接受,其他預約狀態要改成佔用中)
  • 寄發Email通知預約被接受

之前我是直接把它做成3個func:

updateReservation, SyncReservation, SendEmail

但其實仔細看會發現

(1)連動更新其他預約跟

(2)寄發Email通知預約被接受都是side effect,我們不需要等代這些動作都完成才response,此時可以利用event+queue的力量來優化響應時間,也讓代碼更整潔。

What is Event & listenr

Event: 某事件發生了,像是訂單完成、使用者註冊。Hey, sth just happen, anyone who wants to do sth, you are free to do it now(handle by listener).

Listener:當某事件發生時要做的事,像是當使用者註冊事件發生,要做寄發認證信。一個Listener可以監聽多個事件。

1:1的寫法

自己手動綁定event&listenr:

在EventServiceProvider中的listen變數維護:

/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],
'事件' => ['監聽器A', '監聽器B']
];

利用type hint + 自動偵測的方式來做:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;

/**
* Handle the event.
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(OrderShipped $event)
{
if (true) {
$this->release(30);
}
}
}

在EventServiceProvider加上:

/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return true;
}

它就會自動將function handle (EventClass $event)跟這個listenr class綁在一起了。

監聽多個event的寫法:(待補)

防雷提醒:如果已經啟用shouldDiscoverEvents,要小心event重複發送兩次。因為在subscribe的時候就會幫我們做好綁定,此時我們要把原本的type hint拿掉避免自動監測又在綁定一次。

將event變成非同步! Queue worker job基本概念

事前設定

這邊用的drvier用database,queue跟job的維護會用mysql,你也可以換成用redis。

設定queue (queue設定檔跟env)

// .env
QUEUE_CONNECTION=database

Migrate

php artisan queue:table

php artisan migrate

變成非同步,放到queue之後在處理

在listener class加上implements ShouldQueue,就代表綁定到的事件發生時,要做的這個處理會先被放到queue中,之後worker會處理這些job。

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
//
}

代表我這次的監聽器的handle會變job被放到queue。

為了要讓這些任務能夠被處理,我們需要啟動worker!這邊我用supervisor來管理,算是一種優化吧!如下。

優化:自動化啟動與重啟woker

因為worker不會知道新的code,所以需要重啟worker才能處理新寫的event, listener

有時worker可能自己掛掉也需要重啟

這樣太手動, 可以利用supervisor幫我們管理worker

安裝

首先需要安裝supervisor,mac可以用homebrew,linux根據官方文件走就可以很簡單。

啟動supervisor (Mac)

brew services start supervisor

寫設定檔

在supervisor檔可以知道要把設定檔放哪:

[include]
files = /usr/local/etc/supervisor.d/*.ini

嗯嗯,看來需要在supervisor.d資料夾下,新增xx專案.ini設定檔。

這邊直接附上最終的設定檔,下面紀錄我踩雷的過程。

[program:goodlink-laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/opt/php@7.2/bin/php /Users/maomao/reservation-api/artisan queue:work database --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=8
redirect_stderr=true
stdout_logfile=~/reservation-api/storage/worker.log
stopwaitsecs=3600

踩雷過程

踩雷:conf檔有錯,會跳出

error: <class 'FileNotFoundError'>, [Errno 2] No such file or directory: file: /usr/local/Cellar/supervisor/4.2.5/libexec/lib/python3.11/site-packages/supervisor/xmlrpc.py line: 557

仔細看發現是我在設定檔中把檔案路徑多打一個\,所以這是一個supervisory在運行時遇到檔案無法讀取的問題。

Debug: 檢查supervisor狀態

sudo brew services list

回傳結果:

Name Status User File
php none
php@7.2 none root
redis none
supervisor none root

看起來有成功啟動服務,Supervisor 已經被安裝並且正在運行,但是沒有管理任何程序。

這才得知設定檔出現了問題!

修正路徑typo後遇到的其他問題:

sudo supervisorctl reread
// No config updates to processes
sudo supervisorctl update
//
sudo supervisorctl start all
// README.md: ERROR (no such process)
// app: ERROR (no such process)
// artisan: ERROR (no such process)
// bootstrap: ERROR (no such process)
// composer.json: ERROR (no such process)
// composer.lock: ERROR (no such process)
// config: ERROR (no such process)
// creds: ERROR (no such process)
// database: ERROR (no such process)
// package.json: ERROR (no such process)
// phpunit.xml: ERROR (no such process)
// public: ERROR (no such process)
// resources: ERROR (no such process)
// routes: ERROR (no such process)
// server.php: ERROR (no such process)
// storage: ERROR (no such process)
// tests: ERROR (no such process)
// vendor: ERROR (no such process)
// webpack.mix.js: ERROR (no such process)

根據錯誤,Supervisor 似乎無法找到要運行的程序。可能配置文件有誤或者你的程序不存在。

此時去看stdout_logfile(worker有錯誤時會寫的log檔):

supervisor: couldn't setuid to 0: Can't drop privilege as nonroot user
supervisor: child process was not spawned

因為我的conf檔中寫了user=root,但是在運行時就不是root了,所以會錯,這時我把user=這一整行拿掉就解決了。

修正完之後重新運行:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all

就沒有再報錯了

檢查是否啟動成功:

sudo supervisorctl status

這個命令會顯示所有supervisor管理的進程的狀態,如果某個進程是RUNNING狀態,則表示它已經成功啟動。

可以看到成功了,而且運行8個(在設定檔中寫的numprocs=8)

看似一切就緒,實際運行又出一個問題:

踩雷:處理event的時候,發現一直有fail job,問題:

(1)沒有用queue來處理event的話,可以正確操作到firestore,不會跳出需要啟用gRPC的extension。

(2)沒有用queue來處理event的話,能正確讀取到listener的方法,反之跳說找不到方法

可以看出是兩個問題,解決方式為:

(1) php.ini讀取錯誤=>明確指出要用哪一個php,因為我電腦有不同的版本跟不同版本的php.ini檔

(2) 無法正確讀取到listener的方法=>重啟supervisor

解法:強制指定要用哪一份php運行

因為我有多個不同版本的php,只有7.2版本的php.ini有啟用grpc extension。修改我的設定檔:

把php 改成 /usr/local/opt/php@7.2/bin/php

[program:goodlink-laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/opt/php@7.2/bin/php /Users/maomao/reservation-api/artisan queue:work database - sleep=3 - tries=3

可以透過運行which php跟php -v我可以知道要用哪一個,要再確認的話就是用php — ini來看預設的php ini是用哪一個。

改完一樣要運行:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all

--

--

林芷蕾Alicia

Junior Backend developer in IOT company , love to share.