stellarwp/pipeline

实现责任链模式的库。

1.1.1 2024-04-26 17:39 UTC

This package is auto-updated.

Last update: 2024-08-26 21:59:25 UTC


README

Tests Static Analysis

基于Laravel的Pipeline实现的一个Pipeline/责任链设计模式实现。

Pipeline是一个接受输入数据并通过一系列处理器(或“管道”)将其发送以在结束时获得结果的对象。

目录

安装

建议您通过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对象,并想使用管道来处理响应并基于对象的内容触发一些操作。

我们将从几个假设开始

  1. 我们正在构建一些WordPress逻辑。
  2. 我们将声明可重用的管道在扩展名为MyProject\AbstractServiceProvider的类中的Service Provider类中,并可以假设它接受一个容器实例作为构造函数参数。
  3. 该Service Provider类在应用程序的某个地方被实例化,并有一个名为$container的类属性,其中包含一个容器实例。
  4. 我们的容器符合来自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_RequestWP_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' );