italystrap / event
WordPress 和 Psr-14 事件 API 的面向对象方法
Requires
- php: >=7.4
- psr/log: ^1.1
Requires (Dev)
- codeception/module-asserts: ^1.0
- crell/tukio: ^1.0
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- fig/event-dispatcher-util: ^1.3
- humanmade/psalm-plugin-wordpress: ^3.0.0-alpha1
- infection/codeception-adapter: ^0.4.1
- infection/infection: ^0.26.6
- inpsyde/object-hooks-remover: ^0.1
- inpsyde/wp-stubs: dev-main
- italystrap/config: ^2.2
- italystrap/debug: dev-master
- italystrap/empress: dev-master
- lucatume/function-mocker-le: ^1.0
- lucatume/wp-browser: <3.5
- phpbench/phpbench: ^1.2
- phpcompatibility/php-compatibility: ^9.3
- phpmetrics/phpmetrics: ^2.8
- phpspec/prophecy-phpunit: ^2.0
- psr/container: ^1.0
- psr/event-dispatcher: ^1.0
- rector/rector: ^0.15.17
- squizlabs/php_codesniffer: ^3.7
- vimeo/psalm: ^5.6
Suggests
- inpsyde/objects-hooks-remover: Package to remove WordPress hook callbacks that uses object methods or closures.
- italystrap/empress: Config driven for Auryn Injector the OOP way
Provides
This package is auto-updated.
Last update: 2024-08-30 09:39:08 UTC
README
PSR-14 事件分发器实现,用于 WordPress 和 WordPress 插件 API 的封装(事件即钩子)
重要
它仍然是 WIP(工作进行中)
请记住,即使它运行得非常好,PSR-14 实现也运行得非常好,但这个包仍然是 WIP,直到它达到 1.x.x 版本,目前它是一个 0.x.x 版本(如果您不知道这意味着什么,请阅读 SemVer 规范)。
我个人非常自豪这个包(就像我自豪所有其他的包一样 😊),最终实现 PSR-14 标准并不复杂,但我需要稍微修改一下 WordPress 插件 API 才能使其按预期工作。
顺便说一句,我在自己的项目中使用这个包进行生产,现在还没有问题,如果您发现任何问题,请告诉我,请打开一个 issue(或者如果您想修复它,请打开一个 PR),这里是 issue 跟踪器的链接 issue tracker。
我使用的命名约定是:当您遇到单词 Global*
时,它表示与 WordPress 相关的东西,因为 WP 在底层使用全局变量,我不想使用与 WP 相关的任何前缀,即使它也是为 WordPress,我也从不希望在我的代码中使用与它相关的前缀。
目录
安装
使用此包的最佳方式是通过 Composer
composer require italystrap/event
此包遵循 SemVer 规范,并且将在次要版本之间实现完全向后兼容。
简介
欢迎来到 ItalyStrap Event 的文档!在本介绍部分,我们将向您提供 PHP 开发、WordPress 上下文中事件驱动编程的概述,并强调采用此方法的优势。
事件驱动编程:概述
事件驱动编程是一种在软件开发中广泛使用的范例,用于处理反应性场景和管理软件系统内的交互。它围绕事件的概念,事件表示特定的发生或交互。
在事件驱动编程中,软件组件(监听器或订阅者)注册对特定事件的兴趣,并定义当这些事件发生时它们应该如何响应。这种解耦和反应式架构促进了复杂应用程序的模块化、灵活性和可维护性。事件驱动编程的优势
事件驱动编程提供了几个优势,使其在各种软件开发场景中成为一种有价值的做法。
1. 松散耦合
事件充当系统不同部分之间的通信渠道,允许它们在不紧密依赖的情况下进行交互。这种松散耦合增强了代码的可重用性,并促进了关注点的分离。
2. 可扩展性
事件驱动系统可以有效地处理大量的并发事件,确保系统保持响应性和适应不同的工作负载。
3. 灵活性
事件驱动架构灵活且可扩展。可以通过引入新的事件和监听器来添加新功能,而无需对现有代码进行重大更改。
4. 可测试性
隔离事件监听器可以简化单元测试,因为您可以专注于测试单个组件,而无需进行复杂的集成测试。
在本文档的以下章节中,我们将深入探讨PHP生态系统中事件驱动编程的细节。我们将探讨如何处理WordPress中的事件、事件处理的核心API以及如何将PSR-14标准无缝集成到您的项目中。
让我们继续我们的旅程,进入事件驱动编程的世界!
事件驱动系统是如何工作的
在本节中,我们将概述事件驱动系统的工作原理,解释基本概念,突出关键组件,并提供事件驱动系统常见用例的示例。
理解事件驱动编程
事件驱动编程是一种软件架构,它依赖于事件来触发和管理程序的流程。以下是一些关键概念需要掌握
事件
事件代表系统中的特定发生或交互。它们作为信号或通知,表明发生了某些事情或需要关注。事件可以是从用户操作(例如,按钮点击)到系统生成的通知(例如,数据更新)。
事件处理器(监听器)
事件处理器,通常称为监听器或订阅者,是负责响应特定事件的组件。这些监听器注册对特定事件的兴趣,并在这些事件发生时执行预定义的操作。
事件循环
事件循环是事件驱动系统的基本组成部分。它持续监视事件,并将它们调度到相应的处理器。循环确保事件按发生顺序处理,提供一个响应性和非阻塞的执行环境。
事件驱动系统的关键组件
事件驱动系统通常由以下组件组成
1. 事件
事件定义了系统中可能发生的事情,并封装了与这些发生相关联的相关数据。
2. 事件处理器(监听器)
事件处理器或监听器通过执行相应的操作或函数来响应特定的事件。
3. 事件循环
事件循环管理事件的流程,确保它们被调度到正确的监听器。
4. 分发器
分发器负责协调将事件调度到相应的监听器。它作为事件处理的中枢。
事件驱动系统的常见用例
事件驱动系统具有多功能性,并在各个领域找到应用。以下是一些常见的用例
1. 用户界面
图形用户界面(GUI)通常依赖于事件驱动架构来响应用户交互,如按钮点击、鼠标移动和键盘输入。
2. 实时应用
需要实时处理系统,如在线游戏、聊天应用程序和金融交易平台,通过事件驱动设计确保响应性。
3. 通知和警报
事件驱动系统非常适合根据特定的触发器或条件发送通知和警报。
4. 物联网(IoT)
物联网应用程序使用事件驱动原则来管理和处理由连接的设备和传感器生成数据。
在接下来的章节中,我们将探讨事件驱动编程在WordPress中的实现方式,深入了解事件处理的核心API,并展示如何将PSR-14标准无缝集成到您的项目中。
让我们继续我们的旅程,进入事件驱动编程的世界!
WordPress事件系统的工作原理:核心API
在本节中,我们将深入研究WordPress中用于事件处理的核心API。这些API对于管理操作和过滤器至关重要,它们是WordPress事件驱动架构的构建块。遗憾的是,WordPress事件API在底层使用全局变量,这是一种不好的做法,但我们无法对此做任何事情,因为WordPress永远不会改变这一点,所以我们需要忍受它并闭上我们的鼻子。
操作和过滤器
操作
WordPress中的动作是在请求执行过程中特定时刻触发的事件。动作对于在WordPress生命周期中预定义时刻执行副作用或自定义代码至关重要。
动作不返回任何值;相反,它们作为事件处理程序执行任务的信号。当动作被触发时,所有附加的动作处理程序(称为“动作钩子”)会顺序执行。
以下是在WordPress中派发一个简单的动作事件(钩子)的示例,不带有任何参数
\do_action('my_custom_action');
这里与上面相同的动作,但这次带有参数
\do_action('my_custom_action', $arg1, $arg2);
在这里,“my_custom_action”代表事件名称或钩子。WordPress为开发者提供了许多预定义的动作钩子,可用于扩展和自定义平台。以下是一些注册在核心中的
init
wp_loaded
admin_init
admin_menu
- 等等...
过滤器
WordPress中的过滤器与动作类似,但有一个重要的区别:过滤器允许在数据被系统其他部分使用之前对其进行修改。当您想要修改值、内容或通过过滤器传递的数据时,就会使用过滤器(您也可以使用动作来做这件事,但稍后会谈到)。
过滤器始终返回修改后的或未修改的值,并且可以像动作一样顺序应用多个过滤器处理程序(监听器)。过滤器广泛应用于WordPress中自定义和操作数据。
定义过滤器钩子并应用过滤器的示例
$data_to_modify = 'Some data to modify'; $filtered_data = \apply_filters('my_custom_filter', $data_to_modify);
在这里,“my_custom_filter”代表过滤器的事件名称或钩子。WordPress允许开发者在利用预定义过滤器的同时创建自定义过滤器。
理解钩子/事件名称的概念对于在WordPress中处理动作和过滤器至关重要。事件名称是自定义代码可以附加的特定执行流程中的标识符。开发者可以使用WordPress定义的钩子和创建自定义钩子来扩展和自定义WordPress功能。
所以,要记住 动作 和 过滤器 主要是一回事,它们是WordPress事件系统的 派发器。
WordPress使用字符串作为事件名称,在WordPress文档中它们被称为 钩子。
关于派发动作和过滤器需要注意的危险事项:永远不要在类的构造函数内部派发事件,这是一种非常糟糕的做法,如果你这么做,你就是一个糟糕的开发者。
基本用法
《\ItalyStrap\Event\GlobalDispatcher::class》、《\ItalyStrap\Event\GlobalOrderedListenerProvider::class》和《\ItalyStrap\Event\GlobalState::class》是对WordPress插件API的包装
《\ItalyStrap\Event\Dispatcher::class》和《\ItalyStrap\Event\GlobalOrderedListenerProvider::class》实现了PSR-14事件派发器。
动作的简单示例
$listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); // Listen for `event_name` $listenerProvider->addListener( 'event_name', function () { echo 'Event Called'; }, 10 ); $globalDispatcher = new \ItalyStrap\Event\GlobalDispatcher(); // This will echo 'Event Called' on `event_name` $globalDispatcher->trigger( 'event_name' );
过滤器的简单示例
$listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); // Listen for `event_name` $listenerProvider->addListener( 'event_name', function ( array $value ) { // $value['some-key'] === 'some-value'; true // Do your stuff here in the same ways you do with filters return $value; }, 10 ); /** @var array $value */ $value = [ 'some-key' => 'some-value' ]; $globalDispatcher = new \ItalyStrap\Event\GlobalDispatcher(); // This will filter '$value' on `event_name` $filtered_value = $globalDispatcher->filter( 'event_name', $value );
好吧,到目前为止,它非常简单,您将像使用WordPress插件API一样使用它,但更面向对象,您可以注入《GlobalDispatcher::class》或《GlobalOrderedListenerProvider::class》到您的类中。
订阅者注册
那么,关于订阅者注册呢?这里有一个简单的示例
use ItalyStrap\Event\GlobalDispatcher; use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscriberInterface; // Your class must implement the ItalyStrap\Event\SubscriberInterface class MyClassSubscriber implements SubscriberInterface { // Now add the method from the interface and return an iterable with // event name and the method to executed on the event public function getSubscribedEvents(): iterable { return ['event_name' => 'methodName']; } public function methodName(/* could have some arguments */){ // Do some stuff with hooks } } $subscriber = new MyClassSubscriber(); $globalDispatcher = new GlobalDispatcher(); $listenerProvider = new GlobalOrderedListenerProvider(); $subscriberRegister = new SubscriberRegister($listenerProvider); $subscriberRegister->addSubscriber($subscriber); // It will execute the subscriber MyClassSubscriber::methodName $globalDispatcher->trigger('event_name', $some_value); // or $globalDispatcher->filter('event_name', $some_value);
订阅者是一个实现了《ItalyStrap\Event\SubscriberInterface::class》接口的类,它可以自身作为监听器,或者是一个将特定事件的方法执行委托给包装类的类。
《ItalyStrap\Event\SubscriberInterface::getSubscribedEvents()`》必须返回类似下面的可迭代对象
use ItalyStrap\Event\SubscriberInterface; class MyClassSubscriber implements SubscriberInterface { // Just one event => method form generator public function getSubscribedEvents(): iterable { yield 'event_name' => 'method_name'; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface; class MyClassSubscriber implements SubscriberInterface { // Just one event => method form Iterators public function getSubscribedEvents(): iterable { yield new \ArrayObject(['event_name' => 'methodName']); yield new \ItalyStrap\Config\Config(['event_name' => 'methodName']); yield (new \ItalyStrap\Config\Config())->add( 'event_name', 'methodName' ); } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface; class MyClassSubscriber implements SubscriberInterface { // Just one event => method public function getSubscribedEvents(): iterable { return ['event_name' => 'method_name']; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface; class MyClassSubscriber implements SubscriberInterface { // Multiple events => methods public function getSubscribedEvents(): iterable { return [ 'event_name' => 'method_name', 'event_name2' => 'method_name2' // ... more event => method ]; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface as Subscriber; class MyClassSubscriber implements Subscriber { public function getSubscribedEvents(): iterable { // Event with method and priority (for multiple events the logic is the same as above) return [ 'event_name' => [ Subscriber::CALLBACK => 'method_name', Subscriber::PRIORITY => 20, // 10 default ], // ... more event => method ]; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface as Subscriber; class MyClassSubscriber implements Subscriber { public function getSubscribedEvents(): iterable { // Event with method, priority and accepted args (for multiple events the logic is the same as above) return [ 'event_name' => [ Subscriber::CALLBACK => 'method_name', Subscriber::PRIORITY => 20, // 10 default Subscriber::ACCEPTED_ARGS => 4 // 3 default ], // ... more event => method ]; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
use ItalyStrap\Event\SubscriberInterface as Subscriber; class MyClassSubscriber implements Subscriber { public function getSubscribedEvents(): iterable { // Event with methods, priority and accepted args (for multiple events the logic is the same as above) return [ 'event_name' => [ [ Subscriber::CALLBACK => 'method_name', Subscriber::PRIORITY => 20, // 10 default Subscriber::ACCEPTED_ARGS => 4 // 3 default ], [ Subscriber::CALLBACK => 'method_name2', Subscriber::PRIORITY => 20, // 10 default Subscriber::ACCEPTED_ARGS => 4 // 3 default ], ], // ... more event => method ]; } public function methodName(/* could have some arguments if you use the ::filter() method */){ // Do some stuff with hooks } }
如果订阅者需要订阅许多事件,最好是分离业务逻辑和订阅者,在另一个类中,然后使用订阅者来注册这个类,如下所示
use ItalyStrap\Event\GlobalDispatcher; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscriberInterface; class MyBusinessLogic { public function methodOne() { // Do some stuff } public function methodTwo() { // Do some stuff } public function methodThree() { // Do some stuff } } class MyClassSubscriber implements SubscriberInterface { /** * @var MyBusinessLogic */ private $logic; public function __construct( MyBusinessLogic $logic ) { $this->logic = $logic; } public function getSubscribedEvents(): array { return [ 'event_name_one' => 'onEventNameOne', 'event_name_two' => 'onEventNameTwo', 'event_name_three' => 'onEventNameThree', ]; } public function onEventNameOne(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodOne(); } public function onEventNameTwo(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodTwo(); } public function onEventNameThree(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodThree(); } } $logic = new MyBusinessLogic(); $subscriber = new MyClassSubscriber( $logic ); $dispatcher = new GlobalDispatcher(); $subscriber_register = new SubscriberRegister( $dispatcher ); $subscriber_register->addSubscriber( $subscriber ); // It will execute the subscriber MyClassSubscriber::methodName $dispatcher->trigger( 'event_name' ); // or $dispatcher->filter( 'event_name', ['some_value'] ); // You can also remove a listener: $subscriber_register->removeSubscriber( $subscriber ); // The instance of the subscriber you want to remove MUST BE the same instance of the subscriber you // added earlier, and BEFORE you dispatch the event.
这个库类似于Symfony事件派发器
高级用法
如果您想要更多的功能,可以结合使用Empress库。这样做的优点是现在您可以为您的应用程序进行自动绑定(auto-wiring),懒加载监听器/订阅者等。
use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\Injector; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscribersConfigExtension; use ItalyStrap\Event\GlobalDispatcher; use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberInterface; // From Subscriber.php class Subscriber implements SubscriberInterface { public int $check = 0; private \stdClass $stdClass; public function __construct(\stdClass $stdClass) { $this->stdClass = $stdClass; } public function getSubscribedEvents(): array { yield 'event' => $this; } public function __invoke() { echo 'Some text'; } } // From your bootstrap.php file // Create a new InjectorContainer $injector = new Injector(); // This is optional, you could share the injector instance // if you need this instance inside a class for registering stuff // Remember that the Auryn\Injector is not a service locator // Do not use it for locating services $injector->share($injector); // Now it's time to create a configuration for dependencies to inject in the AurynConfig $dependencies = ConfigFactory::make([ // Share the instances of the GlobalDispatcher and SubscriberRegister AurynConfig::SHARING => [ GlobalDispatcher::class, SubscriberRegister::class, ], // Now add in the array all your subscribers that implement the ItalyStrap\Event\SubscriberInterface // The instances create are shared by default for later removing like you se above. SubscribersConfigExtension::SUBSCRIBERS => [ Subscriber::class, ], // You can also add more configuration for the AurynConfig https://github.com/ItalyStrap/empress ]); // This will instantiate the EventResolverExtension::class $eventResolver = $injector->make(SubscribersConfigExtension::class, [ // In the EventResolverExtension object you can pass a config key value pair for adding or not listener at runtime // from your theme or plugin options ':config' => ConfigFactory::make([ // If the 'option_key_for_subscriber' is true than the Subscriber::class will load 'option_key_for_subscriber' => Subscriber::class // Optional ]), ]); // Create the object for the AurynConfig::class and pass the instance of $injector and the dependencies collection $empress = new \ItalyStrap\Empress\AurynConfig($injector, $dependencies); // Is the same as above if you want to use Auryn and you have shared the Auryn instance: $empress = $injector->make(AurynConfig::class, [ ':dependencies' => $dependencies ]); // Pass the $event_resolver object created earlier $empress->extend($eventResolver); // When you are ready call the resolve() method for auto-wiring your application $empress->resolve(); $this->expectOutputString( 'Some text' ); ($injector->make(GlobalDispatcher::class))->trigger('event'); // or $dispatcher = $injector->make(GlobalDispatcher::class); $dispatcher->trigger('event'); // $dispatcher will be the same instance because you have shared it in the above code
懒加载一个订阅者
要懒加载一个订阅者,您可以在AurynConfig配置中添加一个新的代理值,如下例所示
use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\Injector; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscribersConfigExtension; use ItalyStrap\Event\GlobalDispatcher; use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberInterface; // From MyBusinessLogic.php class MyBusinessLogic { public function __construct(/*Heavy dependencies*/){ // Initialize } public function methodOne() { // Do some stuff } public function methodTwo() { // Do some stuff } public function methodThree() { // Do some stuff } } // From MyClassSubscriber.php class MyClassSubscriber implements SubscriberInterface { private MyBusinessLogic $logic; public function __construct(MyBusinessLogic $logic) { // This will be the proxy version of the $logic object $this->logic = $logic; } public function getSubscribedEvents(): array { // The first method that will be called will sobstitute the // proxy version of the object with the real one. return [ 'event_name_one' => 'onEventNameOne', 'event_name_two' => 'onEventNameTwo', 'event_name_three' => 'onEventNameThree', ]; } public function onEventNameOne(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodOne(); } public function onEventNameTwo(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodTwo(); } public function onEventNameThree(/* may be with some arguments if you use the ::filter() method of the dispatcher */){ $this->logic->methodThree(); } } // From your bootstrap.php file // Create a new InjectorContainer $injector = new Injector(); // This is optional, you could share the injector instance // if you need this instance inside a class for registering stuff // Remember that the Auryn\Injector is not a service locator // Do not use it for locating services $injector->share($injector); // Now it's time to create a configuration for dependencies to inject in the AurynConfig $dependencies = ConfigFactory::make([ // Share the instances of the GlobalDispatcher and SubscriberRegister AurynConfig::SHARING => [ GlobalDispatcher::class, SubscriberRegister::class, ], // Now we declare what class we need to lazy load // In our case is the MyBusinessLogic::class injected in the MyClassSubscriber::class AurynConfig::PROXY => [ MyBusinessLogic::class, ], // Now add in the array all your subscribers that implement the ItalyStrap\Event\SubscriberInterface // The instances create are shared by default for later removing like you se above. SubscribersConfigExtension::SUBSCRIBERS => [ MyClassSubscriber::class, ], // You can also add more configuration for the AurynConfig https://github.com/ItalyStrap/empress ]); // This will instantiate the EventResolverExtension::class $event_resolver = $injector->make( SubscribersConfigExtension::class, [ // In the EventResolverExtension object you can pass a config key value pair for adding or not listener at runtime // from your theme or plugin options ':config' => ConfigFactory::make([ // If the 'option_key_for_subscriber' is true than the Subscriber::class will load 'option_key_for_subscriber' => Subscriber::class // Optional ]), ] ); // Create the object for the AurynConfig::class and pass the instance of $injector and the dependencies collection $empress = new AurynConfig( $injector, $dependencies ); // Is the same as above if you want to use Auryn and you have shared the Auryn instance: $empress = $injector->make( AurynConfig::class, [ ':dependencies' => $dependencies ] ); // Pass the $event_resolver object created earlier $empress->extend( $event_resolver ); // When you are ready call the resolve() method for auto-wiring your application $empress->resolve(); $dispatcher = $injector->make(GlobalDispatcher::class); $dispatcher->trigger('event_name_one'); $dispatcher->trigger('event_name_two'); $dispatcher->trigger('event_name_three');
请记住,对象的代理版本是一个“哑”对象,直到您调用某些方法之前都不会做任何事情,而实际的对象将被执行。这对于仅在需要运行时运行代码非常有用。
伪代码示例;
\do_action('save_post', [$proxyObject, 'executeOnlyOnSavePost']);
关于Empress\AurynConfig
的更多信息,您可以在这里找到。您可以在ItalyStrap主题框架中找到实现。
贡献
所有反馈/错误报告/拉取请求都受欢迎。
许可
版权(c)2019 Enea Overclokk,ItalyStrap
此代码根据MIT许可证授权。