[ Laravel 5.1 文档 ] 服务 —— 事件

1、简介

Laravel事件提供了简单的观察者模式实现,允许你订阅和监听应用中的事件。事件类通常存放在app/Events目录,监听器存放在app/Listeners

2、注册事件/监听器

Laravel自带的EventServiceProvider为事件注册提供了方便之所。其中的listen属性包含了事件(键)和对应监听器(值)数组。如果应用需要,你可以添加多个事件到该数组。例如,让我们添加PodcastWasPurchased事件:

/**
 * 事件监听器映射
 *
 * @var array
 */
protected $listen = [
    'App\Events\PodcastWasPurchased' => [
        'App\Listeners\EmailPurchaseConfirmation',
    ],
];

2.1 生成事件/监听器类

当然,手动为每个事件和监听器创建文件是很笨重的,取而代之地,我们可见简单添加监听器和事件到EventServiceProvider然后使用event:generate命令。该命令将会生成罗列在EventServiceProvider中的所有事件和监听器。当然,已存在的事件和监听器不会被创建:

php artisan event:generate

3、定义事件

事件类是一个处理与事件相关的简单数据容器,例如,假设我们生成的PodcastWasPurchased事件接收一个Eloquent ORM对象:

<?php

namespace App\Events;

use App\Podcast;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;

class PodcastWasPurchased extends Event{
    use SerializesModels;

    public $podcast;

    /**
     * 创建新的事件实例
     *
     * @param  Podcast  $podcast
     * @return void
     */
    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }
}

正如你所看到的,该事件类不包含任何特定逻辑,只是一个存放被购买的Podcast对象的容器,如果事件对象被序列化的话,事件使用的 SerializesModels trait将会使用PHP的serialize函数序列化所有Eloquent模型。

4、定义监听器

接下来,让我们看看我们的示例事件的监听器,事件监听器在handle方法中接收事件实例,event:generate命令将会自动在handle方法中导入合适的事件类和类型提示事件。在handle方法内,你可以执行任何需要的逻辑以响应事件。

<?php

namespace App\Listeners;

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

class EmailPurchaseConfirmation{
    /**
     * 创建事件监听器
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * 处理事件
     *
     * @param  PodcastWasPurchased  $event
     * @return void
     */
    public function handle(PodcastWasPurchased $event)
    {
        // Access the podcast using $event->podcast...
    }
}

你的事件监听器还可以在构造器中类型提示任何需要的依赖,所有事件监听器通过服务容器解析,所以依赖会自动注入:

use Illuminate\Contracts\Mail\Mailer;

public function __construct(Mailer $mailer){
    $this->mailer = $mailer;
}

停止事件继续往下传播

有时候,你希望停止事件被传播到其它监听器,你可以通过从监听器的handle方法中返回false来实现。

4.1 事件监听器队列

需要将事件监听器放到队列中?没有比这更简单的了,只需要让监听器类实现ShouldQueue接口即可,通过Artisan命令event:generate生成的监听器类已经将接口导入当前命名空间,所有你可以立即拿来使用:

<?php

namespace App\Listeners;

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

class EmailPurchaseConfirmation implements ShouldQueue{
    //
}

就是这么简单,当监听器被事件调用,将会使用Laravel的队列系统通过队列分发器自动队列化。如果通过队列执行监听器的时候没有抛出任何异常,队列任务在执行完成后被自动删除。

4.1.1 手动访问队列

如果你需要手动访问底层队列任务的deleterelease方法,在生成的监听器中默认导入的Illuminate\Queue\InteractsWithQueue trait提供了访问这两个方法的权限:

<?php

namespace App\Listeners;

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

class EmailPurchaseConfirmation implements ShouldQueue{
    use InteractsWithQueue;

    public function handle(PodcastWasPurchased $event)
    {
        if (true) {
            $this->release(30);
        }
    }
}

5、触发事件

要触发一个事件,可以使用Event门面,传递一个事件实例到fire方法,fire方法会分发事件到所有监听器:

<?php

namespace App\Http\Controllers;

use Event;
use App\Podcast;
use App\Events\PodcastWasPurchased;
use App\Http\Controllers\Controller;

class UserController extends Controller{
    /**
     * 显示指定用户属性
     *
     * @param  int  $userId
     * @param  int  $podcastId
     * @return Response
     */
    public function purchasePodcast($userId, $podcastId)
    {
        $podcast = Podcast::findOrFail($podcastId);

        // Purchase podcast logic...

        Event::fire(new PodcastWasPurchased($podcast));
    }
}

此外,你还可以使用全局的帮助函数event来触发事件:

event(new PodcastWasPurchased($podcast));

6、广播事件

在很多现代web应用中,web套接字被用于实现实时更新的用户接口。当一些数据在服务器上被更新,通常一条消息通过websocket连接被发送给客户端处理。

为帮助你构建这样的应用,Laravel让通过websocket连接广播事件变得简单。广播Laravel事件允许你在服务端和客户端JavaScript框架之间共享同一事件名。

6.1 配置

所有的事件广播配置选项都存放在config/broadcasting.php配置文件中。Laravel支持多种广播驱动:PusherRedis以及一个服务于本地开发和调试的日志驱动。每一个驱动都有一个配置示例。

6.1.1 广播预备知识

事件广播需要以下两个依赖:

  • Pusherpusher/pusher-php-server ~2.0
  • Redis: predis/predis ~1.0
6.1.2 队列预备知识

在开始介绍广播事件之前,还需要配置并运行一个队列监听器。所有事件广播都通过队列任务来完成以便应用的响应时间不受影响。

6.2 将事件标记为广播

要告诉Laravel给定事件应该被广播,需要在事件类上实现Illuminate\Contracts\Broadcasting\ShouldBroadcast接口。ShouldBroadcast接口要求你实现一个方法:broadcastOn。该方法应该返回事件广播”频道“名称数组:

<?php

namespace App\Events;

use App\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ServerCreated extends Event implements ShouldBroadcast{
    use SerializesModels;

    public $user;

    /**
     * 创建新的事件实例
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * 获取事件广播频道
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['user.'.$this->user->id];
    }
}

然后,你只需要和正常一样触发该事件,事件被触发后,一个队列任务将通过指定广播驱动自动广播该事件。

6.3 广播数据

如果某个事件被广播,其所有的public属性都会按照事件负载自动序列化和广播,从而允许你从JavaScript中访问所有public数据,因此,举个例子,如果你的事件有一个单独的包含Eloquent模型的$user属性,广播负载定义如下:

{
    "user": {
        "id": 1,
        "name": "Jonathan Banks"
        ...
    }
}

然而,如果你希望对广播负载有更加细粒度的控制,可以添加broadcastWith方法到事件,该方法应该返回你想要通过事件广播的数组数据:

/**
 * 获取广播数据
 *
 * @return array
 */
public function broadcastWith(){
    return ['user' => $this->user->id];
}

6.4 消费事件广播

6.4.1 Pusher

你可以通过Pusher的JavaScript SDK方便地使用Pusher驱动消费事件广播。例如,让我们从之前的例子中消费App\Events\ServerCreated事件:

this.pusher = new Pusher('pusher-key');

this.pusherChannel = this.pusher.subscribe('user.' + USER_ID);

this.pusherChannel.bind('App\\Events\\ServerCreated', function(message) {
    console.log(message.user);
});
6.4.2 Redis

如果你在使用Redis广播,你将需要编写自己的Redis pub/sub消费者来接收消息并使用自己选择的websocket技术将其进行广播。例如,你可以选择使用使用Node编写的流行的Socket.io库。

使用Node库socket.ioioredis,你可以快速编写事件广播发布所有广播事件:

var app = require('http').createServer(handler);
var io = require('socket.io')(app);

var Redis = require('ioredis');
var redis = new Redis();

app.listen(6001, function() {
    console.log('Server is running!');});

function handler(req, res) {
    res.writeHead(200);
    res.end('');}

io.on('connection', function(socket) {
    //
});

redis.psubscribe('*', function(err, count) {
    //
});

redis.on('pmessage', function(subscribed, channel, message) {
    message = JSON.parse(message);
    io.emit(channel + ':' + message.event, message.data);
});

7、事件订阅者

事件订阅者是指那些在类本身中订阅到多个事件的类,从而允许你在单个类中定义一些事件处理器。订阅者应该定义一个subscribe方法,该方法中传入一个事件分发器实例:

<?php

namespace App\Listeners;

class UserEventListener{
    /**
     * 处理用户登录事件
     */
    public function onUserLogin($event) {}

    /**
     * 处理用户退出事件
     */
    public function onUserLogout($event) {}

    /**
     * 为订阅者注册监听器
     *
     * @param  Illuminate\Events\Dispatcher  $events
     * @return array
     */
    public function subscribe($events)
    {
        $events->listen(
            'App\Events\UserLoggedIn',
            'App\Listeners\UserEventListener@onUserLogin'
        );

        $events->listen(
            'App\Events\UserLoggedOut',
            'App\Listeners\UserEventListener@onUserLogout'
        );
    }

}

7.1 注册一个事件订阅者

订阅者被定义后,可以通过事件分发器进行注册,你可以使用EventServiceProvider上的$subcribe属性来注册订阅者。例如,让我们添加UserEventListener

<?php

namespace App\Providers;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider{
    /**
     * 事件监听器映射数组
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * 要注册的订阅者
     *
     * @var array
     */
    protected $subscribe = [
        'App\Listeners\UserEventListener',
    ];
}

扩展阅读:实例教程 —— Laravel 5.1 定义事件、事件监听器以及触发事件

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长