diff --git a/src/ActionServiceProvider.php b/src/ActionServiceProvider.php index 4fe9b87..82c280c 100644 --- a/src/ActionServiceProvider.php +++ b/src/ActionServiceProvider.php @@ -8,6 +8,7 @@ use Lorisleiva\Actions\DesignPatterns\CommandDesignPattern; use Lorisleiva\Actions\DesignPatterns\ControllerDesignPattern; use Lorisleiva\Actions\DesignPatterns\ListenerDesignPattern; +use Lorisleiva\Actions\DesignPatterns\PipelineDesignPattern; class ActionServiceProvider extends ServiceProvider { @@ -21,6 +22,7 @@ public function register(): void new ControllerDesignPattern(), new ListenerDesignPattern(), new CommandDesignPattern(), + new PipelineDesignPattern(), ]); }); diff --git a/src/Concerns/AsAction.php b/src/Concerns/AsAction.php index c34ff59..6c813e3 100644 --- a/src/Concerns/AsAction.php +++ b/src/Concerns/AsAction.php @@ -10,4 +10,5 @@ trait AsAction use AsJob; use AsCommand; use AsFake; + use AsPipeline; } diff --git a/src/Concerns/AsPipeline.php b/src/Concerns/AsPipeline.php new file mode 100644 index 0000000..2fbb9ce --- /dev/null +++ b/src/Concerns/AsPipeline.php @@ -0,0 +1,8 @@ +setAction($action); + } + + public function __invoke(mixed ...$arguments): mixed + { + $passable = array_shift($arguments); + $closure = array_pop($arguments); + + $method = $this->hasMethod('asPipeline') ? 'asPipeline' : 'handle'; + + return $closure($this->callMethod($method, [$passable]) ?? $passable) ?? $passable; + } +} diff --git a/src/DesignPatterns/PipelineDesignPattern.php b/src/DesignPatterns/PipelineDesignPattern.php new file mode 100644 index 0000000..e828a53 --- /dev/null +++ b/src/DesignPatterns/PipelineDesignPattern.php @@ -0,0 +1,28 @@ +matches(Pipeline::class, 'Illuminate\Pipeline\{closure}') + || $frame->matches(Pipeline::class, '{closure:{closure:Illuminate\Pipeline\Pipeline::carry():184}:185}'); + } + + public function decorate($instance, BacktraceFrame $frame) + { + return app(PipelineDecorator::class, ['action' => $instance]); + } +} diff --git a/tests/AsPipelineTest.php b/tests/AsPipelineTest.php new file mode 100644 index 0000000..abfe115 --- /dev/null +++ b/tests/AsPipelineTest.php @@ -0,0 +1,87 @@ +increment(); + } + + public function asPipeline(PipelinePassable $passable): void + { + $this->handle($passable); + } +} + +it('can run as a pipe in a pipeline, with an explicit asPipeline method', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineTest::class, + AsPipelineTest::class, + AsPipelineTest::class, + AsPipelineTest::class, + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(4); +}); + +it('can run with an arbitrary via method configured on Pipeline', function () { + $passable = Pipeline::send(new PipelinePassable) + ->via('arbitraryMethodThatDoesNotExistOnTheAction') + ->through([ + AsPipelineTest::class, + app()->make(AsPipelineTest::class), + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(2); +}); + +it('can run as a pipe in a pipeline with only one explicit container resolved instance at the bottom of the stack', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineTest::class, // implicit container resolved instance + app()->make(AsPipelineTest::class), // explicit container resolved instance + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(2); +}); + +it('cannot run as a pipe in a pipeline with an explicit container resolved instance in the middle of the stack', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineTest::class, // implicit container resolved instance + app()->make(AsPipelineTest::class), // explicit container resolved instance + AsPipelineTest::class, // implicit container resolved instance + AsPipelineTest::class, // implicit container resolved instance + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(2); +}); + +it('cannot run as a pipe in a pipeline as an standalone instance', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + new AsPipelineTest, // standalone instance + AsPipelineTest::class, // implicit container resolved instance + app()->make(AsPipelineTest::class), // explicit container resolved instance + ]) + ->thenReturn(); + + expect(is_null($passable))->toBe(true); +}); diff --git a/tests/AsPipelineWithExplicitTraitTest.php b/tests/AsPipelineWithExplicitTraitTest.php new file mode 100644 index 0000000..d491ccb --- /dev/null +++ b/tests/AsPipelineWithExplicitTraitTest.php @@ -0,0 +1,29 @@ +increment(); + } +} + +it('can run as a pipe in a pipeline, with explicit trait, without asPipeline method', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineWithExplicitTraitTest::class, + app()->make(AsPipelineWithExplicitTraitTest::class), + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(2); +}); diff --git a/tests/AsPipelineWithImplicitTraitTest.php b/tests/AsPipelineWithImplicitTraitTest.php new file mode 100644 index 0000000..1e15405 --- /dev/null +++ b/tests/AsPipelineWithImplicitTraitTest.php @@ -0,0 +1,29 @@ +increment(); + } +} + +it('can run as a pipe in a pipeline, with implicit trait, without asPipeline method', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineWithImplicitTraitTest::class, + app()->make(AsPipelineWithImplicitTraitTest::class), + ]) + ->thenReturn(); + + expect(is_a($passable, PipelinePassable::class))->toBe(true); + expect($passable->count)->toBe(2); +}); diff --git a/tests/AsPipelineWithMultipleNonOptionalParametersTest.php b/tests/AsPipelineWithMultipleNonOptionalParametersTest.php new file mode 100644 index 0000000..b023d6d --- /dev/null +++ b/tests/AsPipelineWithMultipleNonOptionalParametersTest.php @@ -0,0 +1,44 @@ +increment(); + } + + public function asPipeline(PipelinePassable $passable): PipelinePassable + { + $this->handle($passable); + + return $passable; + } +} + +it('cannot run as a pipe in a pipeline expecting multiple non-optional parameters', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + AsPipelineWithMultipleNonOptionalParametersTest::class, + app()->make(AsPipelineWithMultipleNonOptionalParametersTest::class), + ]) + ->thenReturn(); +})->throws(ArgumentCountError::class); + +it('cannot run as a pipe in a pipeline as an explicit container resolved instance preceding an implicit container resolved instance', function () { + $passable = Pipeline::send(new PipelinePassable) + ->through([ + app()->make(AsPipelineWithMultipleNonOptionalParametersTest::class), + AsPipelineWithMultipleNonOptionalParametersTest::class, + ]) + ->thenReturn(); +})->throws(TypeError::class); diff --git a/tests/Stubs/PipelinePassable.php b/tests/Stubs/PipelinePassable.php new file mode 100644 index 0000000..ebb2c7d --- /dev/null +++ b/tests/Stubs/PipelinePassable.php @@ -0,0 +1,19 @@ +count++; + } +}