Laravel Event的分析和使用

Laravel Event的分析和使用

第一部分 概念解釋 請自行查看觀察者模式php

第二部分 源碼分析 (邏輯較長,不喜歡追代碼能夠直接看使用部分)laravel

第三部分 使用redis

第一部分 解釋

當一個用戶閱讀了一篇文章,可能須要給文章增長點擊量,給閱讀的用戶增長積分,給文章做者發送通知等功能。對於以上操做,數組

咱們可使用laravel提供的事件機制進行良好的解耦。以上的用戶閱讀一篇文章,就是laravel中的一個事件,用戶閱讀文章後觸緩存

發的一系列操做就是此事件的監聽者,他們會被逐個執行。實際上laravel的事件服務是觀察者模式的一個實現,閉包

觸發了一個事件,就好象推倒了多米諾骨牌的地一塊,剩下的操做就驕傲給提早擺好的陣型自行完成了。不一樣的是現實中咱們很難讓骨牌app

中止倒塌, 但在laravel中咱們能夠很方便的中止事件的傳播,即終止監聽者的調用鏈。框架

第二部分 追源碼

事件服務的註冊
# laravel中每一個服務,須要先註冊再啓動,其中註冊是必定的,啓動過程能夠沒有。事件服務也不例外。但事件服務的註冊位置較爲特殊,
# 位於Application.php
protected function registerBaseServiceProviders()
{	
    # 事件服務就是在此註冊的
    # 注意application的register方法實際上調用了服務提供者的register方法
    $this->register(new EventServiceProvider($this));
    $this->register(new LogServiceProvider($this));
    $this->register(new RoutingServiceProvider($this));
}

# 事件服務提供者 Illuminate\Events\EventServiceProvider
public function register()
{
    # 注意此處的singleton綁定 後面會使用到
    $this->app->singleton('events', function ($app) {
        // 綁定的是一個disaptcher實例 併爲事件服務設置了隊列解析器
       	// 注意此閉包是在咱們嘗試從容器中解析事件服務的時候纔會執行
        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
            return $app->make(QueueFactoryContract::class);
        });
    });
}

# 看Illuminate\Events\Dispatcher類
# 簡單的構造方法
public function __construct(ContainerContract $container = null)
{
    $this->container = $container ?: new Container;
}

# setQueueResolver方法 一個簡單的set
public function setQueueResolver(callable $resolver)
{
    $this->queueResolver = $resolver;

    return $this;
}

# 能夠看到事件服務的註冊其實是向容器中註冊了一個事件的分發器
事件服務的啓動一(獲取全部的事件和監聽者)
# 框架啓動的過程當中會調用app/Providers下全部服務提供者的boot方法,事件服務也不例外。
App\Providers\EventServiceProvider文件
class EventServiceProvider extends ServiceProvider
{
	# 此數組鍵爲事件名,值爲事件的監聽者
    # 事件服務的啓動階段會讀取此配置,將全部的事件和事件監聽者對應起來並掛載到事件分發器Dispatcher上
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
	
    # 事件服務啓動真正調用的方法 能夠看到調用了父類的boot方法
    # 也能夠在boot方法中向事件分發器中自行綁定事件和監聽者
    public function boot()
    {
        parent::boot();

        //
    }
}

# EventServiceProvider的父類
# 註冊事件監聽器
public function boot()
{	
    // getEvents方法 獲取事件和監聽器
    $events = $this->getEvents();
	
    foreach ($events as $event => $listeners) {
        foreach (array_unique($listeners) as $listener) {
            // 此處Event facade對應的是Dispatcher的listen方法
            // facade的原理和使用以前介紹過
            Event::listen($event, $listener);
        }
    }

    foreach ($this->subscribe as $subscriber) {
        // 調用的是Dispatcher的subscribe方法
        Event::subscribe($subscriber);
    }
}

# getEvents方法
public function getEvents()
{
    if ($this->app->eventsAreCached()) {
        $cache = require $this->app->getCachedEventsPath();

        return $cache[get_class($this)] ?? [];
    } else {
        return array_merge_recursive(
            // 若是事件很是多,也能夠設置事件和監聽者的目錄,讓框架自行幫助查找
            // 若是須要開啓discoveredEvents功能,須要在App\Providers\EventServiceProvider中
           	// 重寫shouldDiscoverEvents方法 並返回true 表明開啓事件自動發現
           	// 若是須要指定事件和監聽者的目錄,須要重寫discoverEventsWithin方法,其中返回目錄數組
           	// 固然你也能夠所有寫在listen屬性中
            // 當重寫了以上兩個方法的時候 返回的數組和$listen屬性的格式是徹底一致的 以事件名稱爲key 監聽者爲value
            $this->discoveredEvents(),
            
            // 返回的就是App\Providers\EventServiceProvider下的listen數組
            $this->listens()
        );
    }
}

# discoveredEvents方法 此方法觸發的前提是重寫了shouldDiscoverEvents方法
public function discoverEvents()
{	
    // 使用了laravel提供的collect輔助函數 文檔有詳細章節介紹
    // collect函數返回collection集合實例方便咱們鏈式操做
    // reject方法的做用是 回調函數返回 true 就會把對應的集合項從集合中移除
    // reduce方法的做用是 將每次迭代的結果傳遞給下一次迭代直到集合減小爲單個值
    return collect($this->discoverEventsWithin())
        // discoverEventsWithin方法返回查找事件監聽者的目錄數組
        // 默認返回 (array) $this->app->path('Listeners')
        // 咱們天然能夠重寫discoverEventsWithin方法,返回咱們指定的監聽者目錄
        ->reject(function ($directory) {
            // 移除集合中不是目錄的元素
            return ! is_dir($directory);
        })
        ->reduce(function ($discovered, $directory) {
            return array_merge_recursive(
                $discovered,
                // 使用Symfony的Finder組件查找Listener文件
                DiscoverEvents::within($directory, base_path())
            );
        }, []);
}

# Illuminate\Foundation\Events\DiscoverEvents::within方法
# 提取給定目錄中的所有監聽者
public static function within($listenerPath, $basePath)
{
    return collect(static::getListenerEvents(
        (new Finder)->files()->in($listenerPath), $basePath
    ))->mapToDictionary(function ($event, $listener) {
        return [$event => $listener];
    })->all();
}

protected static function getListenerEvents($listeners, $basePath)
{
    $listenerEvents = [];
	// $listeners是Finder組件返回指定目錄下的迭代器,遍歷能夠拿到目錄下的全部文件
    foreach ($listeners as $listener) {
        try {
            $listener = new ReflectionClass(
                // 將絕對路徑轉換爲類名
                static::classFromFile($listener, $basePath)
            );
        } catch (ReflectionException $e) {
            continue;
        }

        if (! $listener->isInstantiable()) {
            continue;
        }

        // dump($listener->getMethods(ReflectionMethod::IS_PUBLIC));

        foreach ($listener->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            // 表明着一個監聽者類中 能夠設置多個監聽器
            if (! Str::is('handle*', $method->name) ||
                ! isset($method->getParameters()[0])) {
                continue;
            }
			
            $listenerEvents[$listener->name.'@'.$method->name] =
                // 能夠認爲此處返回的是事件名
                // 寫在handle*方法中的參數 我建議必定要加上類型提示,而且將類型名參數做爲第一個參數傳入
                Reflector::getParameterClassName($method->getParameters()[0]);
        }
    }
	
    // 過濾事件參數名爲空的監聽器並返回
    return array_filter($listenerEvents);
}
事件服務的啓動二(註冊事件監聽者)
# 上面獲取了所有的事件監聽者 下面就要註冊這些事件監聽者了
# 繼續看EventServiceProvider::boot方法
# 使用php artisan event:list 能夠查看框架中已經註冊的事件
# php artisan event:cache php artisan event:clear
public function boot()
{	
    # 拿到了$listen屬性和要求自動發現的全部事件(若是開啓了自動發現的話)
    $events = $this->getEvents();
 	// dump($events);
    
    foreach ($events as $event => $listeners) {
        foreach (array_unique($listeners) as $listener) {
            // 調用dispatcher的listen方法
            // 事件名爲key 事件監聽者爲value 進行事件的註冊監聽
            Event::listen($event, $listener);
        }
    }

    foreach ($this->subscribe as $subscriber) {
        // subscribe方法請自行查看
        Event::subscribe($subscriber);
    }
}

# Dispatcher::listen方法
# 遍歷getEvents中全部的事件和監聽者 經過實際調用Dispatcher的makeListener建立監聽者
# 以event名爲鍵 建立的監聽者閉包爲值 保存在數組屬性中 供事件觸發的時候查找調用
// 向調度器註冊事件監聽器
public function listen($events, $listener)
{
    // dump($events, $listener);
    foreach ((array) $events as $event) {
        // 若是事件名稱中包含*
        if (Str::contains($event, '*')) {
            $this->setupWildcardListen($event, $listener);
        } else {
            // 正常事件名
            $this->listeners[$event][] = $this->makeListener($listener);
        }
    }
}

# 綁定事件和監聽者閉包的映射
protected function setupWildcardListen($event, $listener)
{   
    // 當一系列的事件都想觸發指定的監聽者的時候 就可使用*進行匹配
    $this->wildcards[$event][] = $this->makeListener($listener, true);
    // 每次更新了通配事件後 都清除緩存
    $this->wildcardsCache = [];
}

# 官方註釋表名此方法向事件分發器中註冊一個事件監聽者
# 其實就是返回事件觸發時執行的監聽者閉包
# 傳入的listener可使App\Listener\MyListener 或 App\Listener\MyListener@myHandle這種字符串
# 或者是一個接收兩個參數的閉包
public function makeListener($listener, $wildcard = false)
{   
    if (is_string($listener)) {
        // 若是傳遞的是一個字符串的話 調用createClassListener放回閉包
        return $this->createClassListener($listener, $wildcard);
    }

    // 若是listener是個閉包 那麼直接將事件對象做爲參數傳入監聽者
    // 事件觸發的時候 直接執行此閉包
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            return $listener($event, $payload);
        }

        // 可變數量的參數列表
        return $listener(...array_values($payload));
    };
}

# createClassListener方法
public function createClassListener($listener, $wildcard = false)
{   
    // 當傳遞的是一個class名或者是帶@method的字符串的時候
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            // createClassCallable返回一個數組 第一個參數是$listener的實例 第二個參數是method
            return call_user_func($this->createClassCallable($listener), $event, $payload);
        }

        return call_user_func_array(
            $this->createClassCallable($listener), $payload
        );
    };
}

# createClassCallable方法
protected function createClassCallable($listener)
{
    // 從字符串中獲取類名和方法名
    [$class, $method] = $this->parseClassCallable($listener);

    // 判斷是否須要隊列化監聽器
    if ($this->handlerShouldBeQueued($class)) {
        // class類名 method 方法名
        return $this->createQueuedHandlerCallable($class, $method);
    }
	
    // 若是不須要異步化執行監聽者 直接返回[$listener, 'method']數組
    // class經過container得到 意味着咱們能夠利用容器方便的注入listner須要的依賴
    // 注意此處返回的是listener的實例 和 調用監聽者時執行的方法名
    return [$this->container->make($class), $method];
}

# handlerShouldBeQueued方法 判斷若是一個監聽者實現了ShouldQueue接口 就認爲此監聽者須要隊列化執行
protected function handlerShouldBeQueued($class)
{   
    // 檢查監聽者是否實現了ShouldQueue接口
    // 是否使用隊列處理事件
    try {
        return (new ReflectionClass($class))->implementsInterface(
            ShouldQueue::class
        );
    } catch (Exception $e) {
        return false;
    }
}

# createQueuedHandlerCallable方法
protected function createQueuedHandlerCallable($class, $method)
{
    return function () use ($class, $method) {
        $arguments = array_map(function ($a) {
            return is_object($a) ? clone $a : $a;
        }, func_get_args());
		// handlerWantsToBeQueued方法 動態判斷監聽者是否須要投遞到隊列執行
        if ($this->handlerWantsToBeQueued($class, $arguments)) {
            $this->queueHandler($class, $method, $arguments);
        }
    };
}

# handlerWantsToBeQueued
protected function handlerWantsToBeQueued($class, $arguments)
{
    $instance = $this->container->make($class);

    // 動態判斷是否須要異步化事件處理
    // 須要咱們在監聽器shouldQueue方法中return bool值
    if (method_exists($instance, 'shouldQueue')) {
        // 能夠在監聽者的shouldQueue方法中返回bool值 動態判斷是否須要異步化
        return $instance->shouldQueue($arguments[0]);
    }

    return true;
}

# queueHandler方法
// 判斷listener的各類屬性 將監聽者投遞到隊列
// laravel 隊列之後會單獨講解 此篇先到這裏
protected function queueHandler($class, $method, $arguments)
{
    [$listener, $job] = $this->createListenerAndJob($class, $method, $arguments);
	// resolveQueue獲取註冊事件服務時設置的queueResolver
    $connection = $this->resolveQueue()->connection(
        $listener->connection ?? null
    );

    $queue = $listener->queue ?? null;

    isset($listener->delay)
        ? $connection->laterOn($queue, $listener->delay, $job)
        : $connection->pushOn($queue, $job);
}

# 以上即是事件註冊的基本代碼 整體來講 咱們看到調用Dispatcher的listen方法 能夠註冊監聽者和事件的綁定
# 監聽者都已閉包的形式進行包裹 這樣的好處是能夠保存上下文變量
# 涉及到的異步處理 其餘文章會進行講解
# 值得注意的是 註冊好的閉包 並不會執行 當觸發相應的事件時纔會執行
事件的觸發
# 業務代碼中調用event()方法就能夠觸發一個事件了 執行的就是Dispatch::dispatch方法
public function dispatch($event, $payload = [], $halt = false)
{
    // 傳遞事件對象自己做爲disaptch的參數 會將對象類名做爲事件名 並將事件對象做爲payload傳遞到監聽者
    // 參考使用方式 event(new SomeEvent()) Event::disaptch(new SomeEvent())
    [$event, $payload] = $this->parseEventAndPayload(
        $event, $payload
    );

    if ($this->shouldBroadcast($payload)) {
        $this->broadcastEvent($payload[0]);
    }

    $responses = [];

    foreach ($this->getListeners($event) as $listener) {
        // 執行每一個監聽者閉包
        $response = $listener($event, $payload);

        if ($halt && ! is_null($response)) {
            // 直接返回結果給事件觸發
            return $response;
        }

        // 若是某個監聽者返回了false 那麼終止後續監聽者的執行
        if ($response === false) {
            break;
        }

        $responses[] = $response;
    }
    // 返回結果給事件觸發
    return $halt ? null : $responses;
}

# parseEventAndPayload方法
protected function parseEventAndPayload($event, $payload)
{   
    // 若是傳遞的是一個事件對象
    if (is_object($event)) {
        [$payload, $event] = [[$event], get_class($event)];
    }

    // 若是event是一個字符串 那麼直接包裝payload
    return [$event, Arr::wrap($payload)];
}

// 獲取全部事件監聽者
public function getListeners($eventName)
{
    $listeners = $this->listeners[$eventName] ?? [];

    $listeners = array_merge(
        $listeners,
        $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
    );
	
    // 若是插入的event類存在
    return class_exists($eventName, false)
        ? $this->addInterfaceListeners($eventName, $listeners)
        : $listeners;
}

# addInterfaceListeners方法
protected function addInterfaceListeners($eventName, array $listeners = [])
{
    foreach (class_implements($eventName) as $interface) {
        // 判斷事件或者其父類實現的接口是否綁定了監聽器
        if (isset($this->listeners[$interface])) {
            foreach ($this->listeners[$interface] as $names) {
                $listeners = array_merge($listeners, (array) $names);
            }
        }
    }
    // 返回合併後的監聽者
    return $listeners;
}

# 部分其餘方法
# until方法 
# 觸發事件 並返回第一個不爲null的監聽器結果
public function until($event, $payload = [])
{   
    return $this->dispatch($event, $payload, true);
}

# push方法
# 調用的仍是listen方法 只不過指定了payload參數
public function push($event, $payload = [])
{
    $this->listen($event.'_pushed', function () use ($event, $payload) {
        $this->dispatch($event, $payload);
    });
}

# flush方法 調用push註冊的監聽者
public function flush($event)
{
    $this->dispatch($event.'_pushed');
}

第三部分 使用

使用一 經過觸發事件給監聽者傳參異步

1 在App\Porviders\EventServiceProvider的listen屬性中綁定事件和監聽者映射關係
...
use App\Events\TestEvent1;
use App\Listeners\TestListener1;
use App\Listeners\TestListener2;
...
protected $listen = [
	...
    TestEvent1::class => [
        TestListener1::class,
        // 自定義監聽者閉包調用的方法myHandle
        TestListener2::class . '@myHandle'
    ]
];
...
    
2 php artisan event:generate 按照listen數組的事件監聽者映射生成
    
3 咱們不在TestEvent1事件中作過多處理 在本示例中保持原樣便可

4 編寫TestListener1文件
<?php

namespace App\Listeners;

use App\Components\Log\LogManager;
use App\Events\TestEvent1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Providers\LogManagerServiceProvider;

class TestListener1
{   
    protected $logger;
	// 經過上面的分析 咱們知道監聽者時經過容器解析出來的 因此能夠盡情的注入
    public function __construct(LogManager $logger)
    {
        $this->logger = $logger;
    }	
	// 自定義傳參給事件監聽者
    public function handle(TestEvent1 $event, string $type)
    {
        // dump($type);
        // dump(debug_backtrace());
        $this->logger->driver($type)->logCertains('emergency', 'something emergency');
    }
}

5 編寫TestListener2文件
<?php

namespace App\Listeners;

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

class TestListener2
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
	
    // 其實指定了myHandle方法以後 原生的handle方法就能夠註釋或者什麼都不寫了
    // 經過上面的分析咱們知道 框架會執行監聽者中全部以handle開頭的方法
    // public function handle(TestEvent1 $event)
    // {   
    //     echo '345';
    // }

    // 此方法也是會執行的
    // public function handleabc(TestEvent1 $event)
    // {   
    //     echo 'abc';
    // }
        
    public function myHandle(TestEvent1 $event)
    {
        dump('do whatever you like');
    }
}

6 編寫測試路由 觸發事件
Route::get('event1', function () {
    // 狀況一 傳遞的是事件實例 若是你須要在事件實例中注入依賴 固然可使用容器解析事件對象
    // 當注入的是一個事件對象的時候 會觸發事件類名這個事件 而且將事件對象做爲payload傳遞給handle方法
    // Event::dispatch(new \App\Events\TestEvent1());

    // 狀況二 傳遞的是事件名 第二個參數生效
    // 演示如何不依賴事件對象 傳遞參數
    Event::dispatch(TestEvent1::class, [new TestEvent1(), 'stream']);
});

使用二 設置事件自動發現ide

1 在App\Porviders\EventServiceProvider中重寫shouldDiscoverEvents方法 啓用事件發現
// 啓用事件自動發現
public function shouldDiscoverEvents()
{
    return true;
}

2 咱們更近一步 設置自動監聽者所在目錄 重寫discoverEventsWithin方法 指定自動發現目錄
// 指定自動發現目錄
// 默認的就是app_path('Listeners')
public function discoverEventsWithin()
{
    return [
        // 這裏能夠註釋掉Listeners目錄爲了不和上面的listen屬性重複 致使全部的監聽者都會執行兩遍
        // app_path('Listeners'),
        app_path('MyListeners')
    ];
}

3 編寫事件App\Events\TestEvent2文件 我這裏的代碼沒有任何的實際意義
<?php

namespace App\Events;

class TestEvent2
{   
    protected $name = 'xy';

    public function __construct()
    {
        // 隨意發揮
    }

    public function getName()
    {
        return $this->name;
    }
}

4 手動建立App\MyListeners\Listener1文件
# 經過上面的源碼分析 咱們知道laravel會將全部以handle開頭的方法參數遍歷
# 而後將第一個參數的類名做爲要觸發的事件名,將事件參數做爲payload傳入
<?php

namespace App\MyListeners;

use App\Events\TestEvent2;

class MyListener1
{
    public function handle(TestEvent2 $evt)
    {
        dump($evt->getName(), 'abc');
        return false; // 若是不註釋掉此行代碼,事件的調用鏈到此終結
    }

    public function handleAbc(TestEvent2 $evt)
    {
        dump($evt->getName());
    }
}

5 手動建立App\MyListeners\Listener2文件
<?php

namespace App\MyListeners;

use App\Events\TestEvent2;

class MyListener2
{ 
    public function handle(TestEvent2 $evt)
    {
        dump($evt->getName());
    }
}

6 建立事件自動發現路由
// 測試自動發現
Route::get('event2', function(){
    Event::dispatch(new TestEvent2());
});

使用三 implement的使用 當事件實現了其餘事件接口,會自動觸發其餘事件綁定的監聽者

對應的方法爲Dispatcher::addInterfaceListeners 請看第二部分

1 建立事件接口
<?php

namespace App\Events;

interface TestEvent3
{ }

<?php

namespace App\Events;

interface TestEvent4
{ }


2 建立監聽者
<?php

namespace App\Listeners;

class TestListener3
{
    public function handle()
    {
        dump('listener3 added by event interface3');
    }
}

<?php

namespace App\Listeners;

class TestListener4
{
    public function handle()
    {
        dump('listener3 added by event interface4');
    }
}
    
<?php

namespace App\Listeners;

class TestListener5 implements TestEvent3, TestEvent4
{
    public function handle()
    {
        dump('five own listener');
    }
}

3 事件實現上面的兩個接口
<?php

namespace App\Events;

class TestEvent5 implements TestEvent3, TestEvent4
{ }

4 註冊事件監聽者
protected $listen = [
    ...
    TestEvent3::class => [
        TestListener3::class
    ],
    TestEvent4::class => [
        TestListener4::class
    ],
    # 甚至能夠註釋掉下面3行 只須要TestEvent5實現上面兩個接口便可觸發上面註冊的監聽者
    TestEvent5::class => [
    	TestListener5::class
    ]
]; 

5 最重要的一步 force and brutal 改源碼 沒錯 就是改源碼
# Dispatcher::getListeners方法
...    
// return class_exists($eventName, false)
    return class_exists($eventName)
    	? $this->addInterfaceListeners($eventName, $listeners)
    	: $listeners;
...
6 建立測試路由
Route::get('event5', function () {
    Event::dispatch(TestEvent5::class);
});

使用四 until和flush

until方法默認調用dispatch方法 當時間監聽者返回不爲null則中止執行後面的監聽者 並返回結果給事件觸發位置

1 配置時間監聽者
protected $listen = [
...
    TestEvent6::class => [
        TestListener6::class,
        TestListener7::class,
        TestListener8::class
    ]
];

2 php artisan event:generate
    
3 簡單編寫事件監聽者
# listener6
public function handle(TestEvent6 $event)
{
    dump('return null');
}
# listener7
public function handle(TestEvent6 $event)
{	
    // 注意此監聽者是有返回值的
    return 123;
}
# listener8
public function handle(TestEvent6 $event)
{	
 	// 並不會執行7後面的監聽者 根本就不會執行
    return 'return something in vain';
}

4 編寫測試路由
Route::get('event6', function () {
    $res = Event::until(new TestEvent6());
    // 能夠看到監聽者8並無執行 由於7返回的不是null
    dump($res);
});

使用五 push&flush 請查看上面的源碼分析

push方法就是提早將event和payload註冊好 供flush調用

1 在App\Providers\EventServiceProvider::boot方法中註冊(這裏只是舉例在boot中進行註冊,你能夠在你喜歡的任何地方註冊)
public function boot()
{
    parent::boot();
	// 註冊一個保存了payload的事件監聽者
    Event::push('longAssEventName', ['type' => 'redis']);
    Event::listen('longAssEventName', function ($type) {
        dump($type);
    });
}

2 建立測試路由
Route::get('event7', function () {
    Event::flush('longAssEventName');
});

以上用法沒那麼常見,這裏只是簡單演示下,細節還需各位自行嘗試,常見使用還要各位仔細查閱文檔。

至於監聽者的異步化,只須要監聽者實現ShouldQueue接口,而後簡單配置就能夠了。你們能夠先行查看文檔事件部分,
具體使用會在laravel隊列篇章講解。若有錯誤,勞煩指正,感謝。

最後,祝各位十一快樂!!!

相關文章
相關標籤/搜索