stellarwp / pipeline
实现责任链模式的库。
Requires
- php: >=7.4
- stellarwp/container-contract: ^1.1.1
Requires (Dev)
- composer-runtime-api: ^2.2
- codeception/codeception: ^4
- codeception/module-asserts: ^1.0
- codeception/module-cli: ^1.0
- codeception/module-db: ^1.0
- codeception/module-filesystem: ^1.0
- codeception/module-phpbrowser: ^1.0
- codeception/module-webdriver: ^1.0
- lucatume/di52: ^3.3.6
- phpcompatibility/phpcompatibility-wp: ^2.1
- stellarwp/coding-standards: ^2.0
- symfony/event-dispatcher-contracts: ^2.5.1
- symfony/string: ^5.4
- szepeviktor/phpstan-wordpress: ^1.1
README
基于Laravel的Pipeline实现的一个Pipeline/责任链设计模式实现。
Pipeline
是一个接受输入数据并通过一系列处理器(或“管道”)将其发送以在结束时获得结果的对象。
目录
- 安装
- 先决条件
- 入门
- 方法
pipe()
(别名:add_pipe()
)send()
set_container()
(别名:setContainer()
)then()
(别名:run()
)then_return()
(别名:run_and_return()
,thenReturn()
)through()
(别名:pipes()
)via()
安装
建议您通过Composer将Pipeline作为项目依赖项安装
composer require stellarwp/pipeline
实际上,我们建议您使用Strauss将此库包含到您的项目中。
幸运的是,将Strauss添加到您的
composer.json
比添加典型依赖项稍微复杂一点,因此请参阅我们的strauss文档。
关于命名空间的重要说明
此存储库中的文档将使用
StellarWP\Pipeline
作为基本命名空间,但是,如果您正在使用Strauss来为您项目的命名空间添加前缀,则您需要相应地调整命名空间。(例如:Boom\Shakalaka\StellarWP\Pipeline
)
先决条件
没有值得注意的先决条件。
但是!如果您希望使用基于容器的管道,则需要一个符合stellarwp/container-contract中定义的ContainerInterface
接口的容器。
入门
幸运的是,从代码角度来看,Pipeline没有太多内容,因此实现它们相当容易。我们将介绍一些基本概念。
重要说明:示例提供了一个字符串作为输入数据。这只是出于简便的考虑!您实际上可以传递任何您想要的 - 原子、对象、数组,等等。
创建简单的Pipeline
假设你有一个字符串,你想通过一系列步骤来操作它。你可以创建一个管道来实现这一点,如下所示
use StellarWP\Pipeline\Pipeline; // Create a new pipeline instance. $pipeline = new Pipeline(); // Send a string through the pipeline. $result = $pipeline->send( 'a sample string that is passed through to all pipes.' ) ->through( 'ucwords', 'trim' )->then_return(); // The output would be stored in $result and would be: // A Sample String That Is Passed Through To All Pipes. echo $result;
分部分构建Pipeline
你不必一次性构建整个管道,可以将其分散到多行中。
use StellarWP\Pipeline\Pipeline; // Create a new pipeline instance. $pipeline = new Pipeline(); // Declare the pipes that you want to run against the // string in the order you want them to execute. // The method `pipes()` is an alias of `through()`. $pipeline->pipes( 'ucwords', 'trim' ); // Add another pipe to the pipeline. // The method `add_pipe()` is an alias of `pipe()`. $pipeline->add_pipe( 'strrev' ); // Declare what you are sending through the pipeline. $pipeline->send( 'potato ' ); // Process the pipeline and get the result. // The method `run()` is an alias of `then_return()`. $result = $pipeline->run(); // The result will be: `otatoP` echo $result;
使用闭包
如果你有一个更复杂的函数想要用作管道,你可以传递一个可调用的对象而不是字符串。你的闭包需要接受两个参数,第一个是输入数据,第二个是管道中的下一个项目。
use StellarWP\Pipeline\Pipeline; $pipeline = new Pipeline(); $pipeline->pipes( static function ( $passable, Closure $next ) { $passable = str_ireplace( 'All', 'All The', $passable ); return $next( $passable ); }, 'ucwords' ); $pipeline->send( 'a sample string that is passed through to all pipes. ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: // A Sample String That Is Passed Through To All The Pipes. echo $result;
使用具有handle
方法的类
你甚至可以创建自己的类来作为管道使用。为了在管道中使用类,它需要一个接受两个参数的方法,第一个是输入数据,第二个是管道中的下一个项目。
默认情况下,Pipeline期望方法被命名为handle
。如果你想使用该方法名,你可以选择实现StellarWP\Pipeline\Contracts\Pipe
接口以强制该方法约定。
示例类
第一个类
use StellarWP\Pipeline\Contracts\Pipe; class SweetUppercasePipe implements Pipe { public function handle( $passable, Closure $next ) { $passable = ucwords( $passable ); return $next( $passable ); } }
第二个类
use StellarWP\Pipeline\Contracts\Pipe; class TrimTheStringPipe implements Pipe { public function handle( $passable, Closure $next ) { $passable = trim( $passable ); return $next( $passable ); } }
示例管道
use StellarWP\Pipeline\Pipeline; $pipeline = new Pipeline(); $pipeline->pipes( new SweetUppercasePipe(), new TrimTheStringPipe() ); $pipeline->send( 'a sample string that is passed through to all pipes. ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: // A Sample String That Is Passed Through To All Pipes. echo $result;
使用具有自定义方法的类
如果你想使用类但想使用不同于预期的默认方法(handle
),你可以使用via()
方法声明备用方法名。
示例类
第一个类
class DifferentSweetUppercasePipe { public function execute( $passable, Closure $next ) { $passable = ucwords( $passable ); return $next( $passable ); } }
第二个类
class DifferentTrimTheStringPipe { public function execute( $passable, Closure $next ) { $passable = trime( $passable ); return $next( $passable ); } }
示例管道
use StellarWP\Pipeline\Pipeline; $pipeline = new Pipeline(); // Set the method to use the `execute()` method instead of the default `handle()`. $pipeline->via( 'execute' ); $pipeline->pipes( new DifferentSweetUppercasePipe(), new DifferentTrimTheStringPipe() ); $pipeline->send( 'a sample string that is passed through to all pipes. ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: // A Sample String That Is Passed Through To All Pipes. echo $result;
提前退出
有时在管道的中间,你可能想停止处理剩余的管道并返回一个值。幸运的是,你可以使用return
语句来做到这一点!
示例管道
use StellarWP\Pipeline\Pipeline; $pipeline = new Pipeline(); $pipeline->pipes( 'trim', static function ( $passable, Closure $next ) { if ( $passable === 'bork' ) { return $passable; } return $next( $passable ); }, 'ucwords' ); $pipeline->send( 'bork ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: "bork" // It would not get to the `ucwords` pipe. echo $result; $pipeline->send( 'cowbell ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: "Cowbell" because it WOULD get to the `ucwords` pipe due to // the second pipe only returning if the value is "bork". echo $result;
返回之外的操作
有时你可能想在管道完成后执行更多操作,而不仅仅是返回结果。你可以通过使用then()
(或其别名run()
)方法而不是then_return()
方法来实现。
示例管道
use StellarWP\Pipeline\Pipeline; // Create a new pipeline instance. $pipeline = new Pipeline(); // Declare the pipes that you want to run against the // string in the order you want them to execute. // The method `pipes()` is an alias of `through()`. $pipeline->pipes( 'ucwords', 'trim' ); // Declare what you are sending through the pipeline. $pipeline->send( 'a sample string that is passed through to all pipes. ' ); // Process the pipeline and get the result. $result = $pipeline->then( static function ( $passable ) { return str_ireplace( 'A Sample', 'A Nice Long', $passable ); } ); // The output would be stored in $result and would be: // A Nice Long String That Is Passed Through To All Pipes. echo $result;
在Pipeline中使用容器
管道可以使用符合stellarwp/container-contract StellarWP\ContainerContract\ContainerInterface
接口的容器来实例化。将容器添加到管道中允许你将类作为管道传递,并允许在运行管道时实例化这些类。
use StellarWP\Pipeline\Pipeline; use MyProject\Container; // Create a new container instance. $container = new Container(); $pipeline = new Pipeline( $container ); // Let's add some classes to the pipeline that we declared in a previous example. $pipeline->pipes( SweetUppercasePipe::class, TrimTheStringPipe::class ); $pipeline->send( 'a sample string that is passed through to all pipes. ' ); $result = $pipeline->run(); // The output would be stored in $result and would be: // A Sample String That Is Passed Through To All Pipes. echo $result;
声明可重用的Pipeline
使用管道的常见方法是在依赖注入容器中声明它们,这样你就可以在需要时获取一个特定配置的管道实例。
示例
在这个例子中,我们接受一个WP_REST_Response
对象,并想使用管道来处理响应并基于对象的内容触发一些操作。
我们将从几个假设开始
- 我们正在构建一些WordPress逻辑。
- 我们将声明可重用的管道在扩展名为
MyProject\AbstractServiceProvider
的类中的Service Provider类中,并可以假设它接受一个容器实例作为构造函数参数。 - 该Service Provider类在应用程序的某个地方被实例化,并有一个名为
$container
的类属性,其中包含一个容器实例。 - 我们的容器符合来自stellarwp/container-contract库的
StellarWP\ContainerContract\ContainerInterface
接口。
本例的目录结构看起来如下所示
MyProject/
Listeners/
Listener.php
Providers/
Service_Provider.php
Response/
Intake_Response.php
Failed_Response.php
Response_Transporter.php
Container.php
Put_It_All_Together.php
服务提供者
首先,我们将创建我们的服务提供者类。
namespace MyProject\Providers; use StellarWP\Pipeline\Pipeline; use MyProject\Container; use MyProject\Response\Intake_Response; use MyProject\Response\Failed_Response; class Service_Provider { /** * @var string */ const REQUEST_PIPELINE = 'myproject.request-pipeline'; /** * @var ContainerInterface */ protected $container; /** * @param ContainerInterface $container */ public function __construct( ContainerInterface $container ) { $this->container = $container; } /** * Register some services into the container. */ public function register() { // Bind `request-pipeline` to the container as a singleton. The first time that `->get( 'request-pipeline' )` is // called, the pipeline will will be instantiated and returned. Subsequent calls to `->get( 'request-pipeline' )` // will return the same instance of the pipeline. $this->container->singleton( self::REQUEST_PIPELINE, function(): Pipeline { $pipeline = new Pipeline( $this->container ); $pipeline->pipes( Intake_Response::class, Failed_Response::class, ); return $pipeline; } ); // Bind the class name of Listener to the container. Any time that `->get( Listener::class )` is called, a new // instance of the Listener will be returned with the `request-pipeline` injected into the constructor. $this->container->bind( Listener::class, static function ( ContainerInterface $container ): Listener { return new Listener( $container->get( self::REQUEST_PIPELINE ) ); } ); } }
Response_Transporter
让我们创建一个非常简单的对象,该对象将同时持有WP_REST_Request
和WP_REST_Response
实例。这个对象将通过我们的管道。
namespace MyProject\Response; use WP_REST_Request; use WP_REST_Response; class Response_Transporter { /** * @var WP_REST_Request */ public $request; /** * @var WP_REST_Response */ public $response; /** * @param WP_REST_Request $request * @param WP_REST_Response $response */ public function __construct( WP_REST_Request $request, WP_REST_Response $response ) { $this->request = $request; $this->response = $response; } }
Intake_Response
接下来,我们将创建我们的Intake_Response类。
namespace MyProject\Response; use StellarWP\Pipeline\Contracts\Pipe; use WP_REST_Response; use WP_Http; class Intake_Response implements Pipe { public static $name = 'Response received'; public static $endpoint = '/myproject/v1/borkborkbork'; public function handle( Response_Transporter $transporter, Closure $next ): WP_REST_Response { // If the response is for the endpoint we're looking for, we'll process it. // Otherwise, it'll just keep moving through the pipeline. if ( $transporter->request->get_route() === static::$endpoint ) { $params = (array) $transporter->response->get_data(); $status = $transporter->response->get_status(); $data = [ 'status' => $status, 'params' => $params, ]; /** * Advertise that we've received the response and what its data is. * * @param string $name The name of the response. * @param array $data The data that was received. */ do_action( 'myproject/rest/event', static::$name, $data ); } // Pass the transporter on to the next pipe in the pipeline. return $next( $transporter ); } }
Failed_Response
接下来,我们将创建我们的Failed_Response类。
namespace MyProject\Response; use StellarWP\Pipeline\Contracts\Pipe; use WP_REST_Response; class Failed_Response implements Pipe { public static $name = 'Response failed'; public static $endpoint = '/myproject/v1/borkborkbork'; public function handle( Response_Transporter $transporter, Closure $next ): WP_REST_Response { // If the response is for the endpoint we're looking for, we'll process it. // Otherwise, it'll just keep moving through the pipeline. if ( $transporter->request->get_route() === static::$endpoint ) { $status = $transporter->response->get_status(); $success = $status >= WP_Http::OK && $status < WP_Http::BAD_REQUEST; // If the response was successful, let's keep moving through the pipeline. if ( $success ) { return $next( $transporter ); } /** * Oh no! The response was not successful. Let's notify our application that something went wrong. * * @param string $name The name of the response. * @param array $data The data associated with the error. */ do_action( 'myproject/rest/event', static::$name, [ 'error-params' => $transporter->response->get_data(), ] ); } // Pass the transporter on to the next pipe in the pipeline. return $next( $transporter ); } }
监听器
最后,我们将创建我们的Listener类。
namespace MyProject\Listeners; use MyProject\Container; use MyProject\Response\Response_Transporter; use WP_REST_Request; use WP_REST_Response; class Listener { /** * @var Pipeline */ protected $response_pipeline; /** * @param Pipeline $response_pipeline */ public function __construct( Pipeline $response_pipeline ) { $this->response_pipeline = $response_pipeline; } /** * @param WP_REST_Response $response The response that was received. * @param WP_REST_Request $request The request that was made. */ public function handle_response( WP_REST_Response $response, WP_REST_Request $request ): void { $response = rest_ensure_response( $response ); if ( is_wp_error( $response ) ) { $response = rest_convert_error_to_response( $response ); } return $this->response_pipeline->send( new Response_Transporter( $request, $response ) )->then_return(); }
让我们把它放在一起
namespace MyProject; use MyProject\Container; use MyProject\Listeners\Listener; use MyProject\Providers\Service_Provider; // Typically the next three lines would be done in a more application-relevant location, however, for the sake of // this example, we'll just include them here. $container = new Container(); $provider = new Service_Provider( $container ); $provider->register(); // Likewise, these lines would likely be done in a class somewhere. $request = new WP_REST_Request( 'GET', '/myproject/v1/borkborkbork', [ 'color' => 'blue' ] ); $response = rest_do_request( $request ); // Get an instance of the Listener class. $listener = $container->get( Listener::class ); // Pass the request to the listener, which will invoke the pipeline. $listener->handle_response( $response, $request ); // If the request was successful, the `myproject/rest/event` action will be fired once to indicate // that the response was received. // If the request was NOT successful, the `myproject/rest/event` action will be fired once to indicate // that the response was received. And a second time to indicate that there was an error. // We can do the same thing for other requests. // Likewise, these lines would likely be done in a class somewhere. These would probably live in different classes. $request = new WP_REST_Request( 'GET', '/myproject/v1/something-else' ); $response = rest_do_request( $request ); $listener = $container->get( Listener::class ); $listener->handle_response( $response, $request ); $request = new WP_REST_Request( 'GET', 'myproject/v1/borkborkbork' ); $response = rest_do_request( $request ); $listener = $container->get( Listener::class ); $listener->handle_response( $response, $request );
方法
pipe()
此方法用于向管道添加管道。
public function pipe( array|mixed $pipes ): self
别名:add_pipe()
示例
$pipeline->pipe( 'ucwords' ); // or $pipeline->add_pipe( 'ucwords' ); // or $pipeline->pipe( [ 'ucwords', 'trim' ] );
send()
此方法用于设置通过管道的对象。
public function send( mixed $passable ): self
示例
// Send a scalar. $pipeline->send( 'Some string' ); // Send an object. $pipeline->send( $my_object );
set_container()
此方法用于设置容器实例。
public function set_container( ContainerInterface $container ): self
别名: setContainer()
示例
$pipeline->set_container( $container );
then()
此方法用于运行管道并返回结果。
public function then( Closure $destination = null ): mixed
别名: run()
示例
$pipeline->then(); // Use the alias. $pipeline->run(); // Provide a function to run before returning the result. $pipeline->then( 'trim' ); // Provide a closure to run before returning the result. $pipeline->then( static function ( $passable ) { return trim( $passable ); } ); // Provide an object as a pipe to run before returning the result. $pipeline->then( new TrimTheStringPipe() ); // Provide an class name as a pipe to run before returning the result. $pipeline->then( TrimTheStringPipe::class );
then_return()
此方法用于运行管道并返回结果。
public function then_return(): mixed
别名:run_and_return()
,thenReturn()
示例
$pipeline->then_return(); // Use an alias. $pipeline->thenReturn(); // Use the other alias. $pipeline->run_and_return();
through()
此方法用于设置用于处理数据的处理程序(或“管道”)。
public function through( array|mixed $pipes ): self
别名: pipes()
示例
// You can provide any number of pipes. $pipeline->through( 'ucwords', 'trim' ); // Using the alias. $pipeline->pipes( 'ucwords', 'trim' ); // Pass an array of pipes. $pipeline->through( [ 'ucwords', 'trim' ] ); // Pass closures as pipes. $pipeline->through( static function ( $passable, Closure $next ) { $passable = str_ireplace( 'All', 'All The', $passable ); return $next( $passable ); } ); // Pass objects as pipes. $pipeline->through( new SweetUppercasePipe(), new TrimTheStringPipe() ); // If you have a container, you can pass class names as pipes. $pipeline->through( SweetUppercasePipe::class, TrimTheStringPipe::class );
via()
此方法用于设置在管道中所有管道上要调用的方法。
public function via( string $method ): self
示例
// Set the method used in all classes in the pipeline to process the data as `execute()`. $pipeline->via( 'execute' ); // Set the method used in all classes in the pipeline to process the data as `borkborkbork()`. $pipeline->via( 'borkborkbork' );