earc / event-tree
eArc - 显式架构框架 - 事件树组件
Requires
- php: ^7.3 || ^8.0
- earc/core: ^1.1
- earc/di: ^3.0
- earc/observer: ^1.1
- psr/event-dispatcher: ^1.0
Requires (Dev)
- phpunit/phpunit: ^7.5 || 8.5 || 9.5
README
PHP应用程序的开发者可能知道,在大量使用事件监听器的情况下,调试地狱就在身边。有趣的是,尽管JS代码通常负载着大量的事件监听器,但大多数JS开发者并不知道这种痛苦。
为了通过这个优势丰富PHP社区,我开发了一种观察者树模式/架构,它实际上是组合模式和观察者模式的结合,并作为eArc事件树包的架构基础。
它可以作为一种简单的方式暴露生命周期钩子(在协作工作流中是一种极其强大的工具),实现复杂的迭代器、经纪人、责任链或策略模式、MVC/ADR等。
与所有eArc包一样,其驱动思想之一是尽可能使代码清晰易懂,而不对其施加过多的限制。
目录
安装
composer install earc/event-tree
引导
earc/event-tree 使用 earc/di 进行依赖注入,并使用 earc/core 进行配置文件。
use eArc\Core\Configuration; use eArc\DI\DI; DI::init(); Configuration::build();
将上述代码放置在您的脚本/框架引导部分或您的 index.php
中。
配置
在vendor目录下创建一个名为 .earc-config.php
的文件。这是所有earc组件的配置文件。
<?php #.earc-config.php return ['earc' => [ 'is_production_environment' => false, 'event_tree' => [ 'directories' => [ '../path/to/your/eventTree/root/folder' => '\\your\\eventTree\\root\\namespace', ] ] ]];
事件树位于您的项目目录中的一个文件夹中。可以从其他项目中导入和扩展树。
最佳实践是只有一个目录,它是所有事件树的根目录。这个限制确保了每个参与或将要参与您项目的开发者都可以轻松跟踪所有事件树。
根文件夹的路径必须相对于您的项目vendor目录或绝对路径。
基本用法
由于我们使用现代操作系统的本地树数据结构来组织我们的代码,因此将它们用于解耦代码和定义数据处理结构只需小小一步。
它简单得不能再简单了。
- 选择一个目录,所有观察者树都应该生活在这个目录中。(更多详情请参阅
configure
) - 根据需要扩展此树根以包含尽可能多的子目录,以便有观察者叶子。
- 将您的监听器保存在它应该附加到观察者的目录中。(更多详情请参阅
监听器
) - 分发您的事件。(更多详情请参阅
事件
)
观察者树
每个事件树实际上是一个观察者树。每个目录都映射到一个事件观察者。实现接口 eArc\Observer\Interfaces\ListenerInterface
的类,并且其相应的类文件位于这样的观察者目录中,都将被附加到观察者上。(阅读 the listener
获取更多详细信息)
事件从树的根节点传播到叶子节点。
因此,事件树是一个观察者树,其叶子节点由监听器填充,事件以定义良好的方式传播。(阅读 the event
获取更多详细信息)
如果您明确命名观察者叶子和监听器,要获得对事件树的基本理解,只需在树的根目录中输入命令 tree
或使用 view-tree 命令行工具。
监听器
事件监听器是事件与您应用程序的业务逻辑之间的桥梁。它可以向事件附加负载并读取其他监听器附加的负载。通过这种方式,您可以通过事件树将应用程序连接起来。
与前端控制器附加到路由/请求一样,事件监听器附加到观察者/事件。最佳实践是编写仅包含与应用程序流程相关的逻辑的小型监听器。事件监听器中不应发生业务逻辑或持久性调用。
每个监听器都必须实现 ListenerInterface
。
use eArc\Observer\Interfaces\ListenerInterface; use eArc\Observer\Interfaces\EventInterface; class MyListener implements ListenerInterface { public function process(EventInterface $event): void { // The listener logic goes here... } }
监听器在第一次访问事件时自动加载并初始化。
事件
每个事件都与上述描述的观察者树相关联。事件必须继承自 TreeEvent
。
use eArc\EventTree\TreeEvent; class MyEvent extends TreeEvent { // Code to handle information specific for your event... }
每个事件都初始化一个 PropagationType
。
use eArc\EventTree\Propagation\PropagationType; $event = new MyEvent(new PropagationType());
传播类型
传播类型限制了事件的传播。
第一个参数 start
确定了相对于事件树根节点在目录树中的起始点。例如,给定值 ['product','export']
,事件将从 event-tree-root/product/export
文件夹开始。如果 start
是一个空数组,事件将从事件树的根节点开始。
第二个参数 destination
确定了事件应如何以线性方式传播。例如,给定值 ['init','collect','process','finish']
,事件将从 ../export
文件夹传播到 ../export/init
,然后到 ../export/init/collect
,再到 ../export/init/collect/process
,最后到 ../export/init/collect/process/finish
文件夹。如果 destination
参数是空数组,则 start
文件夹也是 destination
文件夹。
在 destination
之后,事件的行为就像它在对剩余树执行 广度优先搜索(BFS)。(目录按名称升序排序。)
最后一个参数 maxDepth
限制了整体传播到距离 destination
文件夹/顶点最大距离为 maxDepth
的文件夹/顶点。如果将 maxDepth
配置为 null
,则没有限制。例如,如果提供 0
作为参数,则事件在访问目标观察者叶子后就会结束。
PropagationType
是不可变的。因此,这些标准一旦构建事件就不能更改。它们定义了四个 传播阶段。
分发事件
每个 TreeEvent
都有一个 dispatch()
方法。您不需要分发器。它由 earc/di 的依赖注入魔法在内部调用。
注意:您只能分发事件实例一次。
高级用法
耐心
如果监听器实现了 SortableListenerInterface
,则它可以将其耐心定义为浮点数。(否则耐心为0。)
use eArc\Observer\Interfaces\ListenerInterface; use eArc\Observer\Interfaces\EventInterface; use eArc\EventTree\Interfaces\SortableListenerInterface; class MyListener implements ListenerInterface, SortableListenerInterface { public function process(EventInterface $event): void { // The listener logic goes here... } public static function getPatience() : float { return -12.7; } }
耐心越小,监听器被调用的就越快。
提示:如果两个监听器具有相同的耐心,则不能依赖于它们被调用的顺序。
监听特定传输阶段
PropagationType
产生了四个事件阶段
start
- 事件尚未传播。before
- 事件在其start
和destination
顶点之间。destination
- 事件在其destination
顶点上。beyond
- 事件已传播到其destination
顶点之外。
如果 destination
为空,则 start
阶段也是 before
阶段和 destination
阶段。
实现 PhaseSpecificListenerInterface
的监听器可以监听一个、两个或三个事件阶段,而不是所有四个事件阶段。使用 ObserverTreeInterface
常量 PHASE_START
、PHASE_BEFORE
、PHASE_DESTINATION
和 PHASE_BEYOND
。如果您监听多个阶段,则使用位字段(通过 |
连接它们)。
use eArc\Observer\Interfaces\ListenerInterface; use eArc\Observer\Interfaces\EventInterface; use eArc\EventTree\Interfaces\PhaseSpecificListenerInterface; use eArc\EventTree\Interfaces\Transformation\ObserverTreeInterface; class MyListener implements ListenerInterface, PhaseSpecificListenerInterface { public function process(EventInterface $event): void { // The listener logic goes here... } public static function getPhase() : int { // Listening to the phases destination and beyond only. // Keep in mind it is only one pipe. It is a bit field not a boolean. return ObserverTreeInterface::PHASE_DESTINATION | ObserverTreeInterface::PHASE_BEYOND; } }
如果没有使用 PhaseSpecificListenerInterface
,则假定 PHASE_ACCESS
,这是监听所有四个事件阶段的快捷方式。
操作已分发事件的传输
监听器不能更改不可变的 PropagationType
,但它们可以限制事件的传播。如果您想使用事件树实现责任链模式或类似模式,这将很有用。
每个由其相应的观察器叶调用的监听器都可以通过事件的四种方法之一抑制事件的进一步传播。
它们可以通过结束事件来停止事件进一步传播。
use eArc\Observer\Interfaces\ListenerInterface; use eArc\Observer\Interfaces\EventInterface; class MyListener implements ListenerInterface { public function process(EventInterface $event): void { // ... $event->getHandler()->kill(); // ... } }
甚至同一目录中剩余的监听器也不会被调用。
forward()
强制事件离开当前观察器并丢弃其监听器堆栈。
// ... $event->getHandler()->forward(); // ...
事件直接跳到下一个叶(如果有的话)。同一目录中的任何监听器都不能再监听该特定事件。
terminate()
停止事件访问观察器叶的直接或间接子叶。
// ... $event->getHandler()->terminate(); // ...
但当前观察器不会停止在当前监听器堆栈上的工作。
请记住,在 beyond
阶段,存在活动观察者,它们不是当前观察者的子节点或父节点。
您可以通过调用 tie()
来取消这些观察者。
// ... $event->getHandler()->tie(); // ...
事件绑定到当前观察器和其子节点。事件在相邻叶上的传播被停止。
自定义事件
自定义事件
为了保持组件解耦,事件应该是唯一保留运行时信息的地方(当监听器完成其工作)。由于运行时信息是应用程序特定的,因此设计自己的事件是您架构责任的一部分。
最佳实践是使用接口来描述运行时信息。遵循接口分离原则(ISP)。设计实现接口的对象并扩展 eArc\EventTree\TreeEvent
以提供这些对象。
以下是一个导入过程的示例。
use eArc\EventTree\TreeEvent; use eArc\EventTree\Propagation\PropagationType; interface ImportInformationInterface { public function getRunnerId(): int; public function addWarning(Exception $exception); public function getWarnings(): array; public function getImported(): array; public function getChanged(): array; } class ImportInformation implements ImportInformationInterface { protected $runnerId; protected $warnings = []; protected $imported = []; protected $changed = []; //... } class AppRouterEvent extends TreeEvent { protected $runtimeInformation; public function __construct(PropagationType $propagationType) { parent::__construct($propagationType); $this->runtimeInformation = di_get(ImportInformation::class); } public function getRI(): ImportInformationInterface {/*...*/} }
现在,所有需要在您的监听器之间交换的导入信息都公开了,易于查找和理解。
子系统处理
如果您需要一个仅触发子集监听器的事件,您可以修改 EventInterface
提供的 getApplicableListener()
方法。它返回由事件调用的事件接口数组。
例如,如果核心应用程序支持多个版本,则可以使用这种方式为不同的版本使用单独的监听器。如果控制器支持多个版本,它简单地实现了多个监听器接口。
此功能有用的其他用例
- 应用程序的某些部分仅在某些国家或某些语言中可用。
- 应用程序的部分功能仅在调试模式下有效。
- 对于支付更多费用的付费用户,应用程序的行为会有显著变化。
- 应用程序的不同部分可以切换。
- 在树的同一部分上处理事件的不同阶段。
扩展(第三方)观察者树
如果您为库使用观察者树,则存在某些场景,其中库的用户需要使用、扩展或覆盖提供的观察者树。没有合适的方法可以写入供应商目录。为了克服这一点,earc事件树提供了从其他地方继承树和黑名单监听器的方法。
每个定义的目录树都有一个它所在的根目录和自动加载的根命名空间。您可以指定任意多的earc.event_tree.directories
。
di_import_param(['earc' => ['event_tree' => ['directories' => [ '../src/MyProject/Events/TreeRoot' => 'MyProject\\Events\\TreeRoot', 'Framework/src/EventTreeRoot' => 'Framework\\EventTreeRoot', 'ShopCreator/Engine/events/tree/root' => 'ShopCE\\events\\tree\\root', ]]]]);
配置的根被视为一个大根。如果有相对于根的相同路径,监听器将捆绑在一个观察者叶子上。
由于自动加载的需要,对应树的监听器将具有不同的命名空间,因此不能被覆盖。要注销它们,请将它们的完全限定类名添加到earc.event_tree.blacklist
中。
use Framework\EventTreeRoot\Some\Path\SomeListener; use Framework\EventTreeRoot\Some\Other\Path\SomeOtherListener; use ShopCE\events\tree\root\a\third\path\to\a\ThirdUnwantedListener; di_import_param(['earc' => ['event_tree' => ['blacklist' => [ SomeListener::class => true, SomeOtherListener::class => true, ThirdUnwantedListener::class => true, ]]]]);
提示:在构建ObserverTree
之前必须将监听器黑名单化。因此,一旦事件被分发,对黑名单的更改将不再被识别。(您可以使用di_clear_cache
强制依赖注入系统丢弃对ALL
旧构建对象的引用。)
重定向指令
树扩展/继承机制有一个显著的缺点:如果继承的树不是您仓库的一部分,您就不能更改它。这阻碍了树的重构。或者更糟糕的是,如果继承的树发生变化,您必须更改自己的树以保留功能。处理这种情况的平滑工具是.redirect
指令。
这是一个名为.redirect
的文件,您可以将它放在目录中以操作观察者叶子。每行是一个重定向。在行首,您放入要重定向的子文件夹名称(它不需要存在),在第二个位置,通过空格与目标路径分开(它必须在至少一个事件树目录中存在)。目标路径必须相对于事件树的根目录,但您可以使用~/
作为当前目录的快捷方式。
要排除现有或继承的目录,只需留空目标。.redirect
指令是树继承的一部分。如果存在多个同一路径的.redirect
指令,命名相同的子文件夹,则`earc.event_tree.directories`的顺序很重要。指令按其目录树注册的顺序覆盖。您可以使用目标快捷方式~
取消重定向。
lama creatures/animals/fox #redirects events targeting the lama subfolder to creatures/animals/fox
eagle ~/extinct/2351 #redirects events targeting the eagle subfolder to the extinct/2351 subfolder
maple #hides the maple subdirectory from the events
tiger ~ #cancels all redirects for tiger made by the event trees inherited so far
例如,将路径routing/imported/products
重写为routing/products/imported
需要两步(每个重写部分一步)
1 ) 在routing
目录中放置.redirect
指令
products routing/imported
imported
2 ) 在routing/imported
目录中放置.redirect
指令
imported ~/products
products
显然,使用这种方法,您不能依赖于路由的目录参数,而只能依赖于参数。
要重写基本叶子,将.redirect
指令放入事件树根。
提示:对字符和保留字的命名空间约束限制了可用的叶子名称。.redirect
指令允许您定义没有任何限制的叶子名称。
查找指令
您使用的每个.redirect
指令都会破坏事件树显式设计提供的清晰度。因此,大量使用.redirect
指令是一种反模式。如果您需要重定向大量树,则最好重写它,并使用.lookup
指令包含旧树的监听器。
与 .redirect
指令类似,.lookup
指令也是一个纯文本文件。如果你在其中放置一个路径,它将被包含进来。这意味着事件树链接的叶子节点中的每个监听器都将被处理,就好像它驻留在当前叶子节点中一样。每一行都是一个独立的包含。路径必须相对于事件树根目录。
如果我们再次以将路径 routing/imported/products
重写为 routing/products/imported
为例
1) 创建一个目录路径 routing/products/imported
2) 在 routing/imported
目录中放置 .lookup
指令
routing/imported/products
3) 要取消旧路径,在 routing
目录中放置 .redirect
指令
imported
性能优化
事件树的概念深深植根于文件系统。文件访问在时间和成本上并不便宜,可能会成为瓶颈。如果你使用像 ACPu 这样的文件缓存,值得考虑将事件树结构(即使它可能非常大)加载到内存中。这是通过一个静态文件完成的。请注意,earc/event-tree 只在找不到该文件时才写入此文件。如果你对树进行了更改,你必须手动删除该文件或通过命令行脚本 build-cache
重新生成它。
要使用缓存,请将以下行添加到你的 .earc-config.php
中的 event_tree
部分。
#.earc-config.php //... 'event_tree' => [ 'use_cache' => true, 'cache_file' => '/absolute/path/to/your/cache_file.php', 'report_invalid_observer_node' => false, ], //...
如果你省略了 cache_file
参数,它默认为 /tmp/earc_event_tree_cache.php
。
report_invalid_observer_node
默认为 true。将其设置为 false 将抑制当树中存在一些错误配置的子树时发生的 InvalidObserverNodeException
。因此,即使树的部分失败,也可以写入树缓存文件。
提示:如果你使用自定义的配置文件位置,你必须将文件位置作为参数传递给脚本 vendor/earc/event-tree/tools/build-cache
。
视图树工具
要查看观察者树及其中的监听器,请使用命令行工具 view-tree
。
vendor/earc/event-tree/tools/view-tree
提示:如果你使用自定义的配置文件位置,你必须将文件位置作为参数。
结论
有了这个库,你可以将你的进程逻辑的主要部分与事件树绑定(同时将生命周期挂钩暴露给它),同时保持其他对象解耦,让对象做它们最擅长的事情:处理状态。
当然,你也可以保持你的架构风格,进一步使用你喜欢的框架,并以事件处理的一种明确方式添加事件树。
版本
版本 2.1
- PHP ^7.3 || ^8.0
版本 2.1
- 每个事件都有一个
before
和destination
阶段
版本 2.0
- 通过 earc/core 启动
- 观察者树的缓存
版本 1.1
- 重定向指令
- 查找指令
版本 1.0
- 简化的语法
- 使用 earc/di 作为依赖注入框架
- 新的
view-tree
命令行工具 - 放弃了在运行时构建树的支持
版本 0.0
- 初始发布