diff --git a/README.md b/README.md index 7db5e50..a4cc5b9 100644 --- a/README.md +++ b/README.md @@ -371,3 +371,62 @@ App::get('/', function ($request) { ``` This way our controller stays clean, and readable, and each responsibility is separated to it's own class to make maintainance easier in the long run. This would also make testing easier, as you could test the individual classes, and also the overall pipeline result, without needing to test the controller itself. + +## Hub + +A `Hub` class, is a way to store a similar group of pipelines so they can be retrieved and executed from the same object. + +```php +$app = AppFactory::create(); +$userWorkflows = new Hub($app->getContainer()); + +// By default register the user +$userWorkflows->defaults(function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through([ + ValidateRequest::class, + RegisterUser::class, + SendRegistrationEmail::class + ]) + ->thenReturn(); +}); + +$userWorkflows->pipeline('user-requested-reset-password', function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through([ + ValidateRequestData::class, + ValidateUser::class, + EmailResetPasswordLink::class + ]) + ->thenReturn(); +}); + +$userWorkflows->pipeline('user-enabled-2fa', function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through([ + ValidateRequestData::class, + ValidateUser::class, + Handle2faSetup::class + ]) + ->thenReturn(); +}); + +// Then we can call them easily like so +App::post('/user/register', function($request) use ($userWorkflows) { + $result = $userWorkflows->pipe($request); // Since our default is our register pipe we only need the first arg + + return response()->json(['data' => $result])->get(); +}); + +App::post('/user/password-reset', function($request) use ($userWorkflows) { + $result = $userWorkflows->pipe($request, 'user-requested-password-reset'); + + return response()->json(['data' => $result])->get(); +}); + +App::post('/user/enable-2fa', function($request) use ($userWorkflows) { + $result = $userWorkflows->pipe($request, 'user-enabled-2fa'); + + return response()->json(['data' => $result])->get(); +}); +``` diff --git a/src/Facades/Pipeline.php b/src/Facades/Pipeline.php index 3b2d520..47cab9a 100644 --- a/src/Facades/Pipeline.php +++ b/src/Facades/Pipeline.php @@ -5,8 +5,21 @@ use SlimFacades\Support\Facade; use SlimFacades\Support\Pipeline as PipelineBase; +/** + * @method \SlimFacades\Support\Pipeline send(mixed $passable) + * @method \SlimFacades\Support\Pipeline through(array|mixed $pipes) + * @method \SlimFacades\Support\Pipeline pipe(array|mixed $pipes) + * @method \SlimFacades\Support\Pipeline via(string $method) + * @method \SlimFacades\Support\Pipeline then(\Closure $destination) + * @method mixed thenReturn() + * @method \SlimFacades\Support\PipelineBase getContainer() + * @method \SlimFacades\Support\PipelineBase setContainer(ContainerInterface $container) + */ class Pipeline extends Facade { + /** + * @inheritDoc + */ public static function getFacadeRoot() { return new PipelineBase(static::$app->getContainer()); diff --git a/src/Support/Hub.php b/src/Support/Hub.php new file mode 100644 index 0000000..0228066 --- /dev/null +++ b/src/Support/Hub.php @@ -0,0 +1,94 @@ +container = $container; + } + + /** + * Define the default named pipeline. + * + * @param \Closure $callback + * @return void + */ + public function defaults(\Closure $callback) + { + return $this->pipeline('default', $callback); + } + + /** + * Define a new named pipeline. + * + * @param string $name + * @param \Closure $callback + * @return void + */ + public function pipeline($name, \Closure $callback) + { + $this->pipelines[$name] = $callback; + } + + /** + * Send an object through one of the available pipelines. + * + * @param mixed $object + * @param string|null $pipeline + * @return mixed + */ + public function pipe($object, $pipeline = null) + { + $pipeline = $pipeline ?: 'default'; + + return call_user_func($this->pipelines[$pipeline], new Pipeline($this->container), $object); + } + + /** + * Get the container instance used by the hub. + * + * @return \Psr\Container\ContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * Set the container instance used by the hub. + * + * @param \Psr\Container\ContainerInterface $container + * @return $this + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + return $this; + } +} diff --git a/tests/Facades/PipelineTest.php b/tests/Facades/PipelineTest.php index f3599cb..b9178bc 100644 --- a/tests/Facades/PipelineTest.php +++ b/tests/Facades/PipelineTest.php @@ -24,7 +24,7 @@ public function tearDown(): void } - public function testPipelineMethodsExist() + public function testIsInstanceOfPipeline() { $this->assertInstanceOf(PipelineContract::class, Pipeline::getFacadeRoot()); } diff --git a/tests/Support/HubTest.php b/tests/Support/HubTest.php new file mode 100644 index 0000000..4a14b06 --- /dev/null +++ b/tests/Support/HubTest.php @@ -0,0 +1,78 @@ +container = $app->getContainer(); + } + + public function tearDown(): void + { + unset($this->container); + } + + public function testGetContainer() + { + $hub = new Hub($this->container); + $this->assertInstanceOf(ContainerInterface::class, $hub->getContainer()); + } + + public function testHubReceivesDefault() + { + $hub = new Hub($this->container); + $hub->defaults(function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through(PipelineEmpty::class) + ->thenReturn(); + }); + + $this->assertTrue($hub->pipe(true)); + } + + public function testHubReceivesNamedPipe() + { + $hub = new Hub($this->container); + + $hub->pipeline('test-pipeline', function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through(PipelineEmpty::class) + ->thenReturn(); + }); + + $hub->defaults(function ($pipeline, $passable) { + return $pipeline->send($passable) + ->through(PipelineFoo::class) + ->thenReturn(); + }); + + $this->assertEquals('foo', $hub->pipe('foo', 'test-pipeline')); + $this->assertEquals('foo', $hub->pipe('bar')); + } +} + +class PipelineEmpty +{ + public function handle($piped, $next) + { + return $next($piped); + } +} + +class PipelineFoo +{ + public function handle($piped, $next) + { + return $next('foo'); + } +}