Laravel Event——事件系統的啟動與執行原始碼分析

leoyang發表於2017-12-01

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...

Laravel 的事件系統是一個簡單的觀察者模式,主要目的是用於程式碼的解耦,可以防止不同功能的程式碼耦合在一起。laravel 中事件系統由兩部分構成,一個是事件的名稱,事件的名稱可以是個字串,例如 event.email,也可以是一個事件類,例如 App\Events\OrderShipped;另一個是事件的 listener,可以是一個閉包,還可以是監聽類,例如 App\Listeners\SendShipmentNotification

 

事件服務的註冊

事件服務的註冊分為兩部分,一個是 Application 啟動時所呼叫的 registerBaseServiceProviders 函式:

protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}

其中的 EventServiceProvider/Illuminate/Events/EventServiceProvider:

public function register()
{
    $this->app->singleton('events', function ($app) {
        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
            return $app->make(QueueFactoryContract::class);
        });
    });
}

這部分為 Ioc 容器註冊了 events 例項,Dispatcher 就是 events 真正的實現類。QueueResolver 是佇列化事件的實現。

另一個註冊是普通註冊類 /app/Providers/EventServiceProvider :

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        'App\Events\SomeEvent' => [
            'App\Listeners\EventListener',
        ],
    ];

    public function boot()
    {
        parent::boot();
        //
    }
}

這個註冊類的主要作用是事件系統的啟動,這個類繼承自 /Illuminate/Foundation/Support/Providers/EventServiceProvider:

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [];

    protected $subscribe = [];

    public function boot()
    {
        foreach ($this->listens() as $event => $listeners) {
            foreach ($listeners as $listener) {
                Event::listen($event, $listener);
            }
        }

        foreach ($this->subscribe as $subscriber) {
            Event::subscribe($subscriber);
        }
    }
}

可以看到,事件系統的啟動主要是進行事件系統的監聽與訂閱。

 

事件系統的監聽 listen

所謂的事件監聽,就是將事件名與閉包函式,或者事件類與監聽類之間建立關聯。

public function listen($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);
}

對於有萬用字元的事件名,會統一放入 wildcards 陣列中,makeListener 是建立事件的關鍵:

public function makeListener($listener, $wildcard = false)
{
    if (is_string($listener)) {
        return $this->createClassListener($listener, $wildcard);
    }

    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            return $listener($event, $payload);
        } else {
            return $listener(...array_values($payload));
        }
    };
}

建立監聽者的時候,會判斷監聽物件是監聽類還是閉包函式。

對於閉包監聽來說,makeListener 會再包上一層閉包函式,根據是否含有萬用字元來確定具體的引數。

對於監聽類來說,會繼續 createClassListener:

public function createClassListener($listener, $wildcard = false)
{
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            return call_user_func($this->createClassCallable($listener), $event, $payload);
        } else {
            return call_user_func_array(
                $this->createClassCallable($listener), $payload
            );
        }
    };
}

protected function createClassCallable($listener)
{
    list($class, $method) = $this->parseClassCallable($listener);

    if ($this->handlerShouldBeQueued($class)) {
        return $this->createQueuedHandlerCallable($class, $method);
    } else {
        return [$this->container->make($class), $method];
    }
}

對於監聽類來說,程式首先會判斷監聽類對應的函式:

protected function parseClassCallable($listener)
{
    return Str::parseCallback($listener, 'handle');
}

如果未指定監聽類的對應函式,那麼會預設 handle 函式。

如果當前監聽類是佇列的話,會將任務推送給佇列。

 

觸發事件

事件的觸發可以利用事件名,或者事件類的例項:

public function dispatch($event, $payload = [], $halt = false)
{
    list($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 (! is_null($response) && $halt) {
            return $response;
        }

        if ($response === false) {
            break;
        }

        $responses[] = $response;
    }

    return $halt ? null : $responses;
}

parseEventAndPayload 函式利用傳入引數是事件名還是事件類例項來確定監聽類函式的引數:

protected function parseEventAndPayload($event, $payload)
{
    if (is_object($event)) {
        list($payload, $event) = [[$event], get_class($event)];
    }

    return [$event, array_wrap($payload)];
}

如果是事件類的例項,那麼監聽函式的引數就是事件類自身;如果是事件類名,那麼監聽函式的引數就是觸發事件時傳入的引數。

獲得事件與引數後,就要獲取監聽類:

public function getListeners($eventName)
{
    $listeners = isset($this->listeners[$eventName]) ? $this->listeners[$eventName] : [];

    $listeners = array_merge(
        $listeners, $this->getWildcardListeners($eventName)
    );

    return class_exists($eventName, false)
                ? $this->addInterfaceListeners($eventName, $listeners)
                : $listeners;
}

尋找監聽類的時候,也要從萬用字元監聽器中尋找:

protected function getWildcardListeners($eventName)
{
    $wildcards = [];

    foreach ($this->wildcards as $key => $listeners) {
        if (Str::is($key, $eventName)) {
            $wildcards = array_merge($wildcards, $listeners);
        }
    }

    return $wildcards;
}

如果監聽類繼承自其他類,那麼父類也會一併當做監聽類返回。

獲得了監聽類之後,就要呼叫監聽類相應的函式。

觸發事件時有一個引數 halt,這個引數如果是 true 的時候,只要有一個監聽類返回了結果,那麼就會立刻返回。例如:

public function testHaltingEventExecution()
{
    unset($_SERVER['__event.test']);
    $d = new Dispatcher;
    $d->listen('foo', function ($foo) {
        $this->assertTrue(true);

        return 'here';
    });
    $d->listen('foo', function ($foo) {
        throw new Exception('should not be called');
    });
    $d->until('foo', ['bar']);
}

多個監聽類在執行的時候,只要有一個返回了 false,那麼就會中斷事件。

push 函式

push 函式可以將觸發事件的引數事先設定好,這樣觸發的時候只要寫入事件名即可,例如:

public function testQueuedEventsAreFired()
{
    unset($_SERVER['__event.test']);
    $d = new Dispatcher;
    $d->push('update', ['name' => 'taylor']);
    $d->listen('update', function ($name) {
        $_SERVER['__event.test'] = $name;
    });

    $this->assertFalse(isset($_SERVER['__event.test']));
    $d->flush('update');
    $this->assertEquals('taylor', $_SERVER['__event.test']);
}

原理也很簡單:

public function push($event, $payload = [])
{
    $this->listen($event.'_pushed', function () use ($event, $payload) {
        $this->dispatch($event, $payload);
    });
}

public function flush($event)
{
    $this->dispatch($event.'_pushed');
}

 

資料庫 Eloquent 的事件

資料庫模型的事件的註冊除了以上的方法還有另外兩種,具體詳情可以看:Laravel 模型事件實現原理 ;

事件註冊

  • 靜態方法定義
class EventServiceProvider extends ServiceProvider
{
    public function boot()
    {
        parent::boot();

        User::saved(function(User$user) {

        });

        User::saved('UserSavedListener@saved');
    }
}
  • 觀察者
class UserObserver
{
    public function created(User $user)
    {
        //
    }

    public function saved(User $user)
    {
        //
    }
}

然後在某個服務提供者的boot方法中註冊觀察者:

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        User::observe(UserObserver::class);
    }

    public function register()
    {
        //
    }
}

這兩種方法都是向事件系統註冊事件名 eloquent.{$event}: {static::class}:

  • 靜態方法
public static function saved($callback)
{
    static::registerModelEvent('saved', $callback);
}

protected static function registerModelEvent($event, $callback)
{
    if (isset(static::$dispatcher)) {
        $name = static::class;

        static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback);
    }
}
  • 觀察者
public static function observe($class)
{
    $instance = new static;

    $className = is_string($class) ? $class : get_class($class);

    foreach ($instance->getObservableEvents() as $event) {
        if (method_exists($class, $event)) {
            static::registerModelEvent($event, $className.'@'.$event);
        }
    }
}

public function getObservableEvents()
{
    return array_merge(
        [
            'creating', 'created', 'updating', 'updated',
            'deleting', 'deleted', 'saving', 'saved',
            'restoring', 'restored',
        ],
        $this->observables
    );
}

事件觸發

模型事件的觸發需要呼叫 fireModelEvent 函式:

protected function fireModelEvent($event, $halt = true)
{
    if (! isset(static::$dispatcher)) {
        return true;
    }

    $method = $halt ? 'until' : 'fire';

    $result = $this->fireCustomModelEvent($event, $method);

    return ! is_null($result) ? $result : static::$dispatcher->{$method}(
        "eloquent.{$event}: ".static::class, $this
    );
}

fireCustomModelEvent 是我們本文中著重講的事件類與監聽類的觸發:

protected function fireCustomModelEvent($event, $method)
{
    if (! isset($this->events[$event])) {
        return;
    }

    $result = static::$dispatcher->$method(new $this->events[$event]($this));

    if (! is_null($result)) {
        return $result;
    }
}

如果沒有對應的事件後,會繼續利用事件名進行觸發。

until 是我們上一節講的如果任意事件返回正確結果,就會直接返回,不會繼續進行下一個事件。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章