PSR-14元文档

事件调度程序元文档

1.总结

本文档的目的是描述Event Dispatcher规范背后的基本原理和逻辑。

为什么要打扰?

许多库,组件和框架都有很长的支持机制,允许任意第三方代码与它们进行交互。大多数是经典观察者模式的变体,通常通过中间对象或服务进行调解。其他人采用更面向方面编程(AOP)的方法。尽管如此,它们都具有相同的基本概念:在固定点中断程序流,以向任意第三方库提供有关正在执行的操作的信息,并允许它们对程序行为做出反应或影响。

这是一个成熟的模型,但是库这样做的标准机制将允许它们与越来越多的第三方库进行互操作,而原始开发人员和扩展开发人员只需要更少的工作量。

3.范围

3.1目标

  • 简化和标准化库和组件可以通过“事件”将自己暴露给扩展的过程,以便它们可以更容易地合并到应用程序和框架中。
  • 简化和标准化库和组件可以注册对响应事件的兴趣的过程,以便它们可以更容易地合并到任意应用程序和框架中。
  • 在可行的范围内,简化现有代码库向此规范过渡的过程。

3.2非目标

  • 异步系统通常具有“事件循环”的概念来管理交织协同程序。这是一个无关紧要的事情,明显与本规范无关。
  • 实现“事件源”模式的存储系统也具有“事件”的概念。这与此处讨论的事件无关,并明确超出范围。
  • 与现有事件系统的严格向后兼容性不是优先事项,也不是预期的。
  • 虽然此规范无疑会建议实现模式,但它并不寻求定义One True Event Dispatcher Implementation,而只是定义调用者和Listener如何与该Dispatcher进行通信。

4.方法

4.1考虑的用例

工作组根据各种系统中的野外情况,确定了四种可能的事件传递工作流程。

  • 单向通知。(“如果你关心,我做了一件事。”)
  • 对象增强。(“这是一件事,请在我做之前修改它。”)
  • 采集。(“把你所有的东西都给我,我可以用那个清单做点什么。”)
  • 替代链。(“这是一件事;第一个可以处理它的人,然后停止。”)

在进一步审查时,Working Goup确定:

  • 集合是对象增强的一个特例(集合是增强的对象)。
  • 替代链类似于对象增强的特殊情况,因为签名是相同的并且调度工作流程几乎相同,尽管包括额外的检查。
  • 单向通知是其他通知的退化情况,或者可以表示为单向通知。

虽然在概念上单向通知可以异步完成(包括通过队列延迟),但实际上,该模型的显式实现很少,从而提供了更少的地方来绘制细节指导(例如正确的错误处理)。经过充分考虑后,工作组选择不为单向通知提供明确单独的工作流程,因为它可以充分表现为其他通道的退化情况。

4.2示例应用程序

  • 表示系统配置或某些用户操作发生了某些变化,并允许其他系统以不影响程序流的方式作出反应(例如发送电子邮件或记录操作)。
  • 将对象传递给一系列监听器,以便在将对象保存到持久性系统之前对其进行修改。
  • 将集合传递给一系列侦听器,以允许它们使用它来注册值或修改现有值,以便发射器可以对所有收集的信息进行操作。
  • 将一些上下文信息传递给一系列监听器,以便所有人都可以“投票”采取什么行动,Emitter根据提供的汇总信息做出决定。
  • 将对象传递给一系列监听器,并允许任何监听器在其他监听器完成之前提前终止该进程。

4.3不可改变的事件

最初,工作组希望将所有事件定义为不可变消息对象,类似于PSR-7。然而,除了单向通知案例之外,所有这些都证明是有问题的。在其他场景中,Listeners需要一种将数据返回给调用者的方法。从概念上讲,有三种可能的途径:

  • 使事件变得可变并在适当的位置进行修改。
  • 要求事件可以进化(不可变,但使用with*()PSR-7和PSR-13等方法)并且监听器返回事件传递。
  • 使事件不可变,但聚合并返回每个监听器的返回值。

但是,可停止事件(替代链式情况)也需要有一个通道,通过该通道指示不应该调用更多的监听器。这可以通过以下方式完成:

  • 修改事件(例如,调用stopPropagation()方法)
  • 从Listener(truefalse返回sentinel值以指示传播应该终止。
  • 演变要停止的事件(withPropagationStopped()

这些替代方案中的每一种都有缺点。第一个意味着,至少为了指示传播状态,事件必须是可变的。第二个要求Listeners返回一个值,至少当它们打算停止事件传播时; 这可能会影响现有的图书馆,以及文件方面的潜在问题。第三个要求Listeners在所有情况下都返回Event或mutated Event,并要求Dispatchers进行测试以确保返回的值与传递给Listener的值的类型相同; 它有效地对消费者和实施者施加了责任,从而引发了更多潜在的整合问题。

另外,期望的特征是能够基于从监听器收集的值来导出是否停止传播。(例如,当其中一个提供了某个值时,或者在其中至少有三个指示“拒绝此请求”标志或类似之后停止。)虽然技术上可以实现为可进化的对象,但这种行为是本质上是有状态的,因此对于实现者和用户来说都是非常麻烦的。

让听众回归可演化事件也带来了挑战。PHP或其他地方的任何已知实现都不使用该模式。它还依赖于监听器来记住返回事件(监听器作者的额外工作)并且不返回可能与以后的监听器(例如事件的子类或超类)不完全兼容的其他新对象。

不可变事件也依赖于事件作者来尊重警告是不可变的。事实上,事件的设计非常松散,实施者忽视规范的这一部分的可能性很高,甚至是无意中。

这留下了两个可能的选择:

  • 允许事件是可变的。
  • 需要但无法强制执行具有高级别接口的不可变事件,为Listener作者提供更多工作,以及在编译时可能无法检测到的更高的破损可能性。

通过“高礼”,我们暗示需要冗长的语法和/或实现。在前一种情况下,Listener作者需要(a)创建一个新的Event实例,其中传播标志被切换,并且(b)返回新的Event实例,以便Dispatcher可以检查它:

function (SomeEvent $event) : SomeEvent
{
    // do some work
    return $event->withPropagationStopped();
}

后一种情况,Dispatcher实现,需要检查返回值:

foreach ($provider->getListenersForEvent($event) as $listener) {
    $returnedEvent = $listener($event);
    
    if (! $returnedEvent instanceof $event) {
        // This is an exceptional case!
        // 
        // We now have an event of a different type, or perhaps nothing was
        // returned by the listener. An event of a different type might mean:
        // 
        // - we need to trigger the new event
        // - we have an event mismatch, and should raise an exception
        // - we should attempt to trigger the remaining listeners anyway
        // 
        // In the case of nothing being returned, this could mean any of:
        // 
        // - we should continue triggering, using the original event
        // - we should stop triggering, and treat this as a request to
        //   stop propagation
        // - we should raise an exception, because the listener did not
        //   return what was expected
        //
        // In short, this becomes very hard to specify, or enforce.
    }

    if ($returnedEvent instanceof StoppableEventInterface
        && $returnedEvent->isPropagationStopped()
    ) {
        break;
    }
}

在这两种情况下,我们都会引入更多潜在的边缘案例,几乎没有任何好处,并且很少有语言级机制来指导开发人员纠正实现。

鉴于这些选择,工作组认为可变事件是更安全的选择。

也就是说,不要求事件是可变的当且仅当有必要且适合于手头的用例时,实现者才应在Event对象上提供mutator方法

4.4听众注册

在规范开发过程中的实验确定了存在各种可行的合法手段,通过这些手段可以向Dispatcher通知监听器。听众:

  • 可以明确注册;
  • 可以根据其签名的反映明确登记;
  • 可以使用数字优先顺序注册;
  • 可以使用前/后机制进行注册,以更精确地控制排序;
  • 可以从服务容器注册;
  • 可以使用预编译步骤来生成代码;
  • 可以基于事件本身中对象的方法名称;
  • 可以限制为基于任意复杂逻辑的某些情况或上下文(仅限某些用户,仅在特定日期,仅在存在某些系统设置时等)。

这些和其他机制现在都存在于PHP中的野外,所有这些都是值得支持的有效用例,并且很少(如果有的话)可以方便地表示为另一个的特殊情况。也就是说,标准化一种方式,甚至一小组方式,告知系统监听器,如果不是不可能的话,如果不切断应该支持的许多用例,那么结果是不切实际的。

因此,工作组选择将听众的注册封装在ListenerProviderInterfaceProvider对象可以具有可用的显式注册机制,或者具有多个这样的机制,或者没有。它也可以生成由某些编译步骤生成的代码。但是,这也将管理调度事件的过程的责任从映射事件到监听器的过程分开。这样,根据需要,不同的实现可以与不同的提供者机制混合和匹配。

甚至可能允许库包含自己的提供程序,这些提供程序可以聚合到一个公共提供程序中,该提供程序聚合其侦听程序以返回到Dispatcher。这是在任意框架内处理任意监听器注册的一种可能方式,尽管工作组明确表示这不是唯一的选择。

虽然将Dispatcher和Provider组合到一个对象中是一个有效且允许的退化情况,但不建议这样做,因为它会降低系统集成商的灵活性。相反,提供者应该作为依赖对象组成。

4.5延期听众

规范要求在Dispatcher返回之前,必须调用提供者返回的可调用项(除非传播明确停止)。但是,规范还明确指出,监听器可能会将事件排入队列以供以后处理,而不是立即采取措施。提供者也完全允许接受可调用的注册,但在将其返回给Dispatcher之前将其包装在另一个可调用的中。(在这种情况下,包装器是从Dispatcher的角度来看的监听器。)这允许以下所有行为合法:

  • 提供者返回提供给他们的可调用监听器。
  • 提供者返回在队列中创建条目的callables,该队列将在稍后的某个时间点对另一个可调用的事件做出反应。
  • 监听器本身可以在队列中创建一个条目,该条目将在稍后的某个时间点对事件做出反应。
  • 如果在支持异步行为的环境中运行,则监听器或提供程序可能会触发异步任务(假设发送器不需要异步任务的结果。)
  • 提供者可以选择性地基于任意逻辑对监听器执行这种延迟或包装。

最终结果是,提供者和监听者有责任确定何时将对事件的响应推迟到稍后的安全。在这种情况下,提供者或监听者明确选择不能将有意义的数据传递回发射器,但工作组确定他们最有可能知道这样做是否安全。

虽然从技术上讲是设计的副作用,但它与Laravel(Laravel 5)使用的方法基本相同,并且已在野外得到证实。

4.6返回值

根据规范,Dispatcher必须返回发射器传递的事件。这被指定为用户提供更符合人体工程学的体验,允许类似于以下的短手:

$event = $dispatcher->dispatch(new SomeEvent('some context'));

$items = $dispatcher->dispatch(new ItemCollector())->getItems();

EventDispatcher::dispatch()接口,但是,有没有指定返回类型。这主要是为了与现有实现向后兼容,以使它们更容易采用新接口。另外,由于事件可以是任何任意对象,返回类型只能是object,它只提供最小(虽然非零)值,因为该类型声明不会为IDE提供任何有用的信息,也不会有效地强制执行相同的操作。事件被退回。因此,返回的方法在语法上是无类型的。但是,返回相同的Event对象dispatch()仍然是一项要求,如果不这样做,则违反了规范。

5.人

活动经理工作组包括:

5.1编辑

  • 拉里加菲尔德

5.2赞助商

  • Cees-Jan Kiewiet

5.3工作组成员

  • 本杰明麦克
  • Elizabeth Smith
  • 瑞恩韦弗
  • Matthew Weier O'Phinney

6.投票