Observer
含义
定义对象之间的一对多依赖关系,以便当一个对象更改状态时,它的所有依赖关系将被自动通知和更新
适用性
当希望对目标对象的特定事件做出反应时,将使用观察者模式。什么时候目标对象分发生变化,就通知观察者。其实,PHP 已经有了观察者模式的内置接口:SplSubject
和 SplObserver
抽象结构
-
SplSubject
是一个抽象类或者接口。其内部可以定义一个数组保存附加的观察者们.SplSubject
包含三种方法。attach
方法将观察者添加到目标对象,detach
方法分离观察者。notify
方法通常包含循环,遍历所有附加的观察者并调用其更新方法。 -
SplObserver
也是一个抽象类或者接口。SplObserver
内有一个update
方法,由于SplSubject
触发目标对象的更新。 -
RealSubject
是SplSubject
接口的实现。 包含存储附加观察者的对象实例(数组/集合等),实际上是标准里SplObjectStorage
类的实例,可以看做是一个整洁的助手类(helper class),该类提供了一种对象到数据的映射,具体参见手册。 -
RealObserver
是SplObserver
接口的实现。它的update
方法传递一个SplSubject
实例对象作为参数,任何时候目标对象发生变动,它就会执行相应的业务逻辑
示例
在这个例子中,将使用 PHP 内置的 SplObserver
和 SplSubject
来显示一个相当通用的观察者模式,之后,探索一下 Laravel Eloquent
模型内部结构,以及如何使用事件来处理附加的观察者。
SPL Obeserver
使用通用的 SPL 接口,只需定义两个类,为了演示方面,不作命名空间区分
<?php
class RealSubject implements \SplSubject
{
private $observers;
public function __construct($name)
{
$this->name = $name;
$this->observers = new \SplObjectStorage();
}
public function attach(\SplObserver $observer)
{
// TODO: Implement attach() method.
$this->observers->attach($observer);
}
public function detach(\SplObserver $observer)
{
// TODO: Implement detach() method.
$this->observers->detach($observer);
}
public function notify()
{
// TODO: Implement notify() method.
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
class RealObserver implements \SplObserver
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function update(\SplSubject $subject)
{
// TODO: Implement update() method.
print "{$this->name} was notified by {$this->name}" . PHP_EOL;
}
}
$subject1 = new RealSubject('subject1');
$observer1 = new RealObserver('observer1');
$observer2 = new RealObserver('observer2');
$observer3 = new RealObserver('observer3');
$subject1->attach($observer1);
$subject1->attach($observer2);
$subject1->attach($observer3);
$subject1->notify();
测试结果:
observer1 was notified by subject1
observer2 was notified by subject1
observer3 was notified by subject1
RealSubject
和 RealObserver
,都是通用的实现方式,但是有其很大局限性,这些通用的方法是约束的,你不能在 update 方法里添加任何除了 SplSubject
类的类型提示(type hint),你可能也并不总是希望通知的方式是公开的,而希望是内部流程发生时,观察者才得到通知,正是使用通用 SPL 接口的限制,这里你无法更改。那怎么办呢?
抽烟的故事
我们都知道人是复杂的,可以是观察者也可以是被观察者,或者说他既是 目标对象(Subject) 又是 观察者(Observer),下面通过一个故事来模拟下:
大学通宵打 LOL 或 Dota 的时候,什么不能少?香烟!没错,尤其后半夜闹烟慌了,没烟抽了,这时候你只剩唯一一根,刚一点着,室友闻到了,挖槽,不给我来一口,这时候你咋办?是我就赶紧跑出去自己偷偷吸起来,哈哈,开玩笑。下面用代码模拟下这个情景,这里 “你” 可以看作目标对象,“室友” 看作观察者,我们期望的输出应该是这样:
--- 你 点着了一根 芙蓉王 ---
小明说: “擦,我闻到了芙蓉王的味道”
小贱说: “擦,我闻到了芙蓉王的味道”
小王说: “擦,我闻到了芙蓉王的味道”
你说: “握草,一帮畜生,我先来一口”
--- 你 点着了一个根 中华 ---
你说: “麻蛋,幸亏还剩一根中华,哈哈”
接下来用代码实现:
<?php
// 先定义两个接口,点烟的人,就是你,闻到香烟的室友
interface CigaretteSmeller
{
public function smell(CigaretteSmoker $smoker, $cigarette);
}
interface CigaretteSmoker
{
public function nearBy(CigaretteSmeller $smeller);
public function noLongerNearBy(CigaretteSmeller $smeller);
public function lightUp($cigarette);
}
// 给故事里的人都起个名字,定义一个说话的方法
class Person implements CigaretteSmoker, CigaretteSmeller
{
public function __construct($name)
{
$this->name = $name;
$this->observers = new \SplObjectStorage();
}
public function says($phrase)
{
print "{$this->name} 说: \t\"" . $phrase . "\"" . PHP_EOL;
}
// 实现接口中的 nearBy 方法
public function nearBy(CigaretteSmeller $smeller)
{
$smellers = func_get_args();
foreach ($smellers as $smeller) {
$this->observers->attach($smeller);
}
}
// 实现接口中的 noLongerNearBy 方法
public function noLongerNearBy(CigaretteSmeller $smeller)
{
$smellers = func_get_args();
foreach ($smellers as $smeller) {
$this->observers->detach($smeller);
}
}
// 实现接口中的 lightUp 方法,你一点着香烟附近的室友就靠过来
public function lightUp($cigarette)
{
print "--- {$this->name} lightUp {$cigarette} ---" . PHP_EOL;
foreach ($this->observers as $observer) {
$observer->smell($this, $cigarette);
}
}
// 实现接口中的 smell 方法
public function smell(CigaretteSmoker $smoker, $cigarette)
{
$this->says("擦,我闻到了{$cigarette}的味道");
}
}
$you = new Person('你');
$wang = new Person('小王');
$li = new Person('小李');
$jian = new Person('小贱');
$you->nearBy($wang, $li, $jian);
$you->lightUp('芙蓉王');
$you->says('握草,一帮畜生,我先来一口');
$you->noLongerNearBy($wang, $li, $jian);
$you->lightUp('芙蓉王');
$you->says("麻蛋,幸亏还剩一根中华,哈哈");
哈哈。是不是稍微理解了 Observers
模式的应用呢。这里与前面通用的 SPL 示例不同,这段代码反映了一定的业务逻辑,两者都使用到了观察者模式。接下来我们看看 Observer
在 Eloquent
里是如何应用的。
Eloquent Observer:开箱即用
所有的 Eloquent
模型都内置了观察者模式。我们创建一个 Car
模型,汽车会有些基本属性:制造商,车辆识别码(vin),年代,描述等。
app/Car.php
namespace App;
use Illuminate\Datebase\Eloquent\Model;
class Car extends Model
{
}
怎么在这个 model 上设置观察者呢,很简单,使用 observe
方法。
app/test1.php
\App\Car::observe(new Observers\ObserveEverything);
创建了一个通用的观察者,名为 ObserveEverything
,它包含了所有可以在 Eloquent
模型上使用的那些开箱即用的方法。简单起见,每个方法打印一个语句,以便了解何时被调用的。方法列表如下:
app/Observers/ObserveEverything.php
class ObserveEverything
{
public function creating($model)
{
print "creating model" . PHP_EOL;
}
public function created($model)
{
print "created model" . PHP_EOL;
}
public function updating($model)
{
print "updating model" . PHP_EOL;
}
public function updated($model)
{
print "updated model" . PHP_EOL;
}
public function saving($model)
{
print "saving model" . PHP_EOL;
}
public function saved($model)
{
print "saved model" . PHP_EOL;
}
public function deleting($model)
{
print "deleting model" . PHP_EOL;
}
public function deleted($model)
{
print "deleted model" . PHP_EOL;
}
public function restoring($model)
{
print "restoring model" . PHP_EOL;
}
public function restored($model)
{
print "restored model" . PHP_EOL;
}
}
从每个方法名当中你或许已经知道何时被调用的。不过我还是稍微解释下:
- 当 model 首次在数据库被创建之前,creating 方法被触发。可以通过新建 model 的 save 方法或静态 create 方法来触发。注意,当新的 model 已经被构建或检索时,不会触发该方法。如果方法返回 false,则 model 不会被创建。
- 当 model 已经在数据库创建完成后,created 方法被触发。
- 当已经存在的 model 在被更新之前,updating 方法会被触发。同理,返回 false ,model 不会被更新。
- 当已经存在的 model 被更新之后,updated 方法被触发。
- 当 model 被创建或者更新之前,saving 方法会被触发。同理,返回 false model 不会被保存。
- 当 model 被创建或者更新之后,saved 方法被触发
- 当 model 被删除之前,deleting 方法被触发。同理,返回 false,model 不会被删除。
- 当 model 被删除之后 deleted 方法被调用。
- 当 model 被还原之前 restoring 方法被调用,还原适用于应用了软删除(soft deletes)的模型。它将删除当前记录的 deleted_at 列。同理,返回 false,model 不会被删除。
- 当 model 被还原之后 restored 方法被调用。
接下来,添加该 Obeserver 到 Car model 上。
app/test1.php
Car::observe(new Observers\ObserveEverything);
当 Car 模型发生一系列改变时将触发 ObserveEverything 观察者的调用。
app/test1.php
$car1 = Car::find(1);
$car->vin = str_random(32)
print "Saving car #1 to database" . "<br>";
$car1->save();
输出:
Saving car #1 to database
saving model
updating model
updated model
saved model
app/test1.php
$car2 = new \App\Car();
$car2->description = "cool car description";
$car2->vin = str_random(32);
$car2->manufacturer = "Honda";
$car2->year = '2012';
print "Creating a new car";
$car2->save();
输出:
Creating new car
saving model
creating model
created model
saved model
app/test1.php
print "Deleting a car that you just made";
$car2->delete();
输出:
Deleting that new car you just made
deleting model
deleted model
app/test1.php
print "Restoring that car you just deleted";
$car2->restore();
输出:
Restoring that car you just deleted
restoring model
saving model
updating model
updated model
saved model
restored model
使用观察者阻止更新
上面我们重现了这些开箱即用的事件模式。里面的每个方法都与 Eloquent model 事件相挂钩。前面我们说了在相应事件返回 false,则对应 model 将不会执行,这意味着我们就可以在这里做些事情,来阻止 save,create,delete,restore 等。来看一个例子,假设说所有的汽车 vin 码必须包含字母 h 才能保存,那么不包含字母 h 的模型就不会写入数据库。
app/test1.php
Car::observe(new Observers\VinObserver);
$car1 = Car::find(1);
// attempt #1 with no h
$car1->vin = "asdfasdfasdf";
$car1->save() && print "attempt #1 saved\n";
// attempt #2 contains h
$car1->vin = "hasdfasdfasdf";
$car1->save() && print "attempt #2 saved\n";
输出:
model vin does not contain letter 'h', canceling update...
attempt #2 saved
来看一下怎么实现
app/Observers/VinObserver.php
namespace App\Observers;
class VinObserver
{
public function updating($model)
{
$origin = $model->getOriginal("vin");
if ($model->vin === $original) {
return true; // ignore unchanged vin
}
if (! str_contains($model->vin, 'h')) {
print "model vin does not contain letter 'h', canceling updating vi \n";
return false;
}
}
}
忽略所有未改变 vin 码的 model。并且不包含字母 h 的 vin 返回 false,这样就可以阻止更新。来看 laravel 底层是如何处理的。
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
/**
* 执行模型更新操作
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return bool
*/
protected function performUpdate(Builder $query)
{
// 如果更新时间返回 false,我们将取消更新操作,以便开发人员可以将验证系统
// 挂接到其模型中,并在模型未通过验证时取消此操作。否则,我们就更新
if ($this->fireModelEvent('updating') === false) {
return false;
}
// 首先,为了开发者方便,我们需要创建一个新的查询实例,并接触我们维护的
// 模型上的创建和更新时间戳。然后继续保存模型实例。
if ($this->timestamps) {
$this->updateTimestamps();
}
// 一旦我们执行了更新操作,就将触发该模型实例的 update 事件。这将允许开发者
// 有机会在模型更新后挂钩处理的事情,
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$this->setKeysForSaveQuery($query)->update($dirty);
$this->fireModelEvent('updated', false);
}
return true;
}
当执行更新操作时,其中一件事就是检查脏字段(dirty fields)。如果模型没有发生改变,就不要更新,接下来是 fireModelEvent 方法
,作用就是触发模型事件,如果返回 false,就不执行更新操作,继续看看 fireModelEvent
方法内部。
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
/**
* 触发模型上给定的事件
*
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent($event, $halt = true)
{
if (! isset(static::$dispatcher)) {
return true;
}
// 我们把类的名称附加到事件中,以区别于被触发的其他模型事件,从而
// 允许我们单独监听每个模型事件,而不是捕获所有模型的事件。
$event = "eloquent.{$event}: ".static::class;
$method = $halt ? 'until' : 'fire';
return static::$dispatcher->$method($event, $this);
}
这个方法内部调用了调度器(dispatcher)的 until 或者 fire 方法并返回结果。你附加到模型上的所有观察者都放在调度器中。这就是为什么你在这个 fireModelEvent 方法中没有看到观察者的内容。那么这个静态调度器到底是个什么东西呢? Eloquent 模型使用了共享的调度器,尤其是 $app['events']
单例。该事件调度器是消息总线,它是 Illuminate\Events\Dispatcher
的一个实例。当应用程序启动数据服务提供者时,事件调度器将被注入到 Eloquent 模型中。
vendor/laravel/framework/src/Illuminate/Database/DatabaseServiceProvider.php
/**
* 启动应用程序事件
*
* @return void
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
这里,我们已经发现模型事件如何被触发以及如何阻止模型的更新操作。但是还有一点,假设所有注册的模型事件都被放在事件调度器中处理,你不知道内部如何处理的,接下来看一看观察者是如何附加到模型上的。
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
之前版本注册模型观察者等一系列方法放在 model.php
,里,后面 laravel 将这部分功能以 trait 的方式重构了,并命名为 HasEvents
。所以路径是
Illuminate\Database\Eloquent\Concerns\HasEvents.php
/**
* 注册模型的观察者
*
* @param object|string $class
* @param int $priority
* @return void
*/
public static function observe($class, $priority = 0)
{
$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, $priority);
}
}
}
当在模型上调用观察者方法时,它会遍历可能出现的事件,然后使用事件和类名称做参数调用 registerModelEvent 方法。getObservableEvents 方法返回一个字符串数组。字符串便是前面提及的那些事件(updating, updated, creating, created 等等),它还包括模型属性数组 $observables 里存放的其他任何可被观察(监听)的事件。根据源码得知,该方法具有以下参数:
static::registerModelEvent('updating', 'Observers\VinObserver@updating');
那么问题来了,registerModelEvent 方法究竟做了什么?
/**
* 使用调度器注册模型事件
*
* @param string $event
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
protected static function registerModelEvent($event, $callback, $priority = 0)
{
if (isset(static::$dispatcher)) {
$name = static::class;
static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority);
}
}
共享的调度器被告知去监听 App\Car
的更新事件,任何时候在调度器上触发该事件,对应回调也将触发。这就是观察者如何附加到模型上的。注意,每个模型都没有得到自己的观察者数组。这将会比附加观察者数组到每一个模型实例上使用更少的内存空间。这也意味着我们所有的 Car 模型都有相同的观察者,这是符合预期的。如果你就想给 Car 的其中一个实例创建观察者,而其他实例并不创建,那么就需要做一些不同的处理。现在,我们已经明白了观察者是如何附加到 Eloquent 模型上并被触发。
我们已经得知 laravel 提供的一些列标准事件可以帮助我们实现好多功能。然后。现实情况是复杂的,对应的 model 也是复杂的,你可能需要好多不同的标准事件以外的事件。比如你有一个不同状态的 content 模型,对应状态可能是草稿,待审核,已发布等。如果你想根据不同状态执行不能的 model 行为,那么你就需要自定义观察者附加到模型上,去监听对应事件。
前面已经提及到模型属性数组 $observable
。这里就是存放我们自定义的事件。该数组允许你去监听其他事件。比如:
<?php
use Illuminate\Database\Eloquent\Model;
class MyContentModel extends Model
{
/**
* Additional observable events.
*/
protected $observables = [
'sentforreview', // 即将发布进入待审核状态
'rejected', // 撤销,驳回,意味着内容可能因为审核失误而发布了,马上下架
];
}
本质上,它使用了 Eloquent 内部的 getObservableEvents 方法,前面已提及,该方法会合并自定义的事件到标准事件数组中。
Illuminate\Database\Eloquent\Concerns\HasEvents
/**
* 获取能监听到的事件名
*
* @return array
*/
public function getObservableEvents()
{
return array_merge(
[
'creating', 'created', 'updating', 'updated',
'deleting', 'deleted', 'saving', 'saved',
'restoring', 'restored',
],
$this->observables // <--- merge in custom events
);
}
紧接着,生成对应观察者实现:
<?php
namespace App\Observers;
class MyContentObserver
{
public function sentforreview(MyContent $myContent)
{
// Your logic here.
}
public function rejected(MyContent $myContent)
{
// Your logic here.
}
}
这一步我们就完成了自义定的事件等待触发了,那么什么时候触发,怎么触发?
<?php
use Illuminate\Database\Eloquent\Model;
class MyContentModel extends Model
{
public function sendForReview()
{
// 对模型作相应的处理然后触发事件
$this->fireModelEvent('sentforreview', false);
}
public function reject()
{
// 对模型作相应的处理然后触发事件
$this->fireModelEvent('rejected', false);
}
}
到这里我们就拥有了一个单一的观察者来处理自定义事件。在最新版本 5.4 里,laravel 提供了一种 map 方式,允许我们将 Eloquent 模型的生命周期的多个事件映射到专门的事件系统中的事件类(EventServiceProvider),关于 laravel 的事件系统,请参考文档。这将使我们的 model 更轻量。基本示例:
<?php
namespace App;
use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* 模型事件映射。
*
* @var array
*/
protected $events = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
关于这个特性,请参考
https://laracasts.com/series/whats-new-in-laravel-5-4/episodes/10
总结
事件驱动架构(Event-driven architecture)是一种围绕应用程序状态而设计的软件架构模式。观察者模式可用于这种类型的软件架构设计。 还有其他类似的模式。比如:
- 中介者模式(mediator pattern)
- 命令总线模式(command bus pattern)
- 订阅发布模式((subscribe/publish pattern)laravel 内置的事件系统就是用的这种模式
说实话,这些内在区别有啥不同,我也搞不清,总之别太迷恋设计模式。。。。
参考书籍:《Design Patterns in PHP and Laravel》