# 事件系统

## 事件系统介绍

Laravel 为事件提供了一个简单的观察者实现，允许你在应用中订阅和监听各种发生的事件。 在 Laravel 中事件类通常放在 app/Events 目录下，这些事件类的监听器则放在 app/Listeners 目录下。而在 Ecshopx 中事件和监听器则放在 Bundle 对应的目录中。

事件系统为应用各个方面的解耦提供了非常棒的方法，因为单个事件可以拥有多个互不依赖的监听器。

举个例子，在电商系统中，订单完成时，可能希望向用户发送短信通知或者微信通知，同时还需要处理一些订单相关的逻辑。你可以简单地发起一个可以被监听器接收并转化为短信通知的 TradeFinishEvent 事件，而不是将订单处理代码和发送短信通知代码耦合在一起。

## 定义事件

事件类是一个保存与事件相关信息的容器。它保存在 Bundle 的 Events 目录中。例如，假设我们定义一个 TradeFinishEvent 事件，其接收一个 Trade Entity 对象：

```php
<?php

namespace OrdersBundle\Events;

use App\Events\Event;

class TradeFinishEvent extends Event
{
    public $entities;

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

如你所见，这个事件类中没有包含其它逻辑。它只是一个 Trade 的实例的容器。

## 定义监听器

事件监听器是一个特殊的 PHP 类，其包含了一个接受事件的 handle 方法，handle 方法中加入事件的类型提示。它保存在 Bundle 的 Listeners 目录中。例如，在订单完成时，我们会给用户发送一个微信的模板消息。

```php
<?php

namespace OrdersBundle\Listeners;

use OrdersBundle\Events\TradeFinishEvent;

class TradeFinishWxaTemplateMsg {

    /**
     * Handle the event.
     *
     * @param  TradeFinishEvent  $event
     * @return void
     */
    public function handle(TradeFinishEvent $event)
    {
        //...
    }
}
```

## 注册事件和监听器

现在我们定义了一个订单完成的事件 `TradeFinishEvent` 和 一个订单完成时的监听器 `TradeFinishWxaTemplateMsg`，现在我们需要注册事件和监听器。

在 Laravel 中事件和监听器注册是在 `EventServiceProvider` 类中完成的，在 ecshopx 中，我们可以把它们放在每个 Bundle 的 Provider 中。 Provider 放在 Providers 目录下。

```php
<?php

namespace OrdersBundle\Providers;

use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;

class TradeFinishServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'OrdersBundle\Events\TradeFinishEvent' => [
            'OrdersBundle\Listeners\TradeFinishWxaTemplateMsg',
        ],
    ];
}
```

其中， listen 属性包含了所有事件 (键) 以及事件对应的监听器 (值) 的数组。当然，你可以根据应用的需要，添加多个事件到 listen 属性包含的数组中。

在注册完成之后，还需要将 TradeFinishServiceProvider 在 `bootstrap/app.php` 中：

```php
$app->register(OrdersBundle\Providers\TradeFinishServiceProvider::class);
```

## 分发事件

如果要分发事件，你可以将事件实例传递给辅助函数 event。该辅助函数将会把事件分发到所有该事件相应的已经注册了的监听器上。event 辅助函数可以全局使用，你可以在应用中的任何位置进行调用：

```php
event(new TradeFinishEvent($eventsParams));
```

## 事件监听器队列（异步事件）

如果你的监听器中要执行诸如发送电子邮件或发出 HTTP 请求之类的耗时任务，你可以将任务丢给队列处理。在开始使用队列监听器之前，请确保在你的服务器或者本地开发环境中能够 配置队列并启动一个队列监听器。

要指定监听器启动队列，你可以在监听器类中实现 ShouldQueue 接口。同时需要继承 `EspierBundle\Listeners\BaseListeners` 类：

```php
<?php

namespace SystemLinkBundle\Listeners;

// use OrdersBundle\Events\TradeFinishEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use EspierBundle\Listeners\BaseListeners;

class TradeAftersaleCancelSendOme extends BaseListeners implements ShouldQueue {


    protected $queue = 'default';

    /**
     * Handle the event.
     *
     * @param  TradeFinishEvent  $event
     * @return void
     */
    public function handle(TradeAftersalesCancelEvent $event)
    {


        return true;
    }
}
```

现在，当这个监听器被事件调用时，事件调度器会自动使用 Laravel 的队列系统自动排队。如果在队列中执行监听器时没有抛出异常，任务会在执行完成后自动从队列中删除。

### 自定义队列连接 & 队列名称

如果你想要自定义事件监听器所使用的队列的连接和名称，你可以在监听器类中定义 $connection， $queue 或 $delay 属性：

```php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * 任务连接名称。
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * 任务发送到的队列的名称.
     *
     * @var string|null
     */
    public $queue = 'listeners';

    /**
     * 处理任务的延迟时间.
     *
     * @var int
     */
    public $delay = 60;
}
```

### 处理失败任务

有时事件监听器的队列任务可能会失败。如果监听器的队列任务超过了队列中定义的最大尝试次数，则会在监听器上调用 failed 方法。 failed 方法接收事件实例和导致失败的异常作为参数：

```php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * 处理事件.
     *
     * @param  \App\Events\OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        //
    }

    /**
     * 处理失败任务
     *
     * @param  \App\Events\OrderShipped  $event
     * @param  \Exception  $exception
     * @return void
     */
    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}
```
