diff --git a/composer.json b/composer.json index 631785d..dfd29e5 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,8 @@ "pull-request": "https://github.com/hyperf/hyperf/pulls" }, "require": { - "php": ">=8.1", - "hyperf/macroable": "~3.1.0", + "php": ">=8.2", + "hyperf/macroable": "~3.2.0", "psr/container": "^1.0 || ^2.0" }, "autoload": { @@ -38,4 +38,4 @@ "dev-master": "3.1-dev" } } -} +} \ No newline at end of file diff --git a/src/Pipeline.php b/src/Pipeline.php index 7efaeda..d90ee2e 100644 --- a/src/Pipeline.php +++ b/src/Pipeline.php @@ -39,6 +39,11 @@ class Pipeline */ protected string $method = 'handle'; + /** + * The final callback to be executed after the pipeline ends regardless of the outcome. + */ + protected ?Closure $finally = null; + public function __construct(protected ContainerInterface $container) { } @@ -81,7 +86,13 @@ public function then(Closure $destination) { $pipeline = array_reduce(array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)); - return $pipeline($this->passable); + try { + return $pipeline($this->passable); + } finally { + if ($this->finally) { + ($this->finally)($this->passable); + } + } } /** @@ -92,6 +103,18 @@ public function thenReturn() return $this->then(fn ($passable) => $passable); } + /** + * Set a final callback to be executed after the pipeline ends regardless of the outcome. + * + * @return $this + */ + public function finally(Closure $callback) + { + $this->finally = $callback; + + return $this; + } + /** * Get the final piece of the Closure onion. */ diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php index a11917a..b972514 100644 --- a/tests/PipelineTest.php +++ b/tests/PipelineTest.php @@ -12,6 +12,7 @@ namespace HyperfTest\Pipeline; +use Exception; use Hyperf\Context\ApplicationContext; use Hyperf\Pipeline\Pipeline; use HyperfTest\Pipeline\Stub\FooPipeline; @@ -19,6 +20,7 @@ use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use stdClass; /** * @internal @@ -211,6 +213,123 @@ public function testHandleCarry() $this->assertSame($id + 6, $result); } + public function testPipelineFinally() + { + $pipeTwo = function ($piped, $next) { + $_SERVER['__test.pipe.two'] = $piped; + + $next($piped); + }; + + $result = (new Pipeline($this->getContainer())) + ->send('foo') + ->through([PipelineTestPipeOne::class, $pipeTwo]) + ->finally(function ($piped) { + $_SERVER['__test.pipe.finally'] = $piped; + }) + ->then(function ($piped) { + return $piped; + }); + + $this->assertSame(null, $result); + $this->assertSame('foo', $_SERVER['__test.pipe.one']); + $this->assertSame('foo', $_SERVER['__test.pipe.two']); + $this->assertSame('foo', $_SERVER['__test.pipe.finally']); + + unset($_SERVER['__test.pipe.one'], $_SERVER['__test.pipe.two'], $_SERVER['__test.pipe.finally']); + } + + public function testPipelineFinallyMethodWhenChainIsStopped() + { + $pipeTwo = function ($piped) { + $_SERVER['__test.pipe.two'] = $piped; + }; + + $result = (new Pipeline($this->getContainer())) + ->send('foo') + ->through([PipelineTestPipeOne::class, $pipeTwo]) + ->finally(function ($piped) { + $_SERVER['__test.pipe.finally'] = $piped; + }) + ->then(function ($piped) { + return $piped; + }); + + $this->assertSame(null, $result); + $this->assertSame('foo', $_SERVER['__test.pipe.one']); + $this->assertSame('foo', $_SERVER['__test.pipe.two']); + $this->assertSame('foo', $_SERVER['__test.pipe.finally']); + + unset($_SERVER['__test.pipe.one'], $_SERVER['__test.pipe.two'], $_SERVER['__test.pipe.finally']); + } + + public function testPipelineFinallyOrder() + { + $std = new stdClass(); + + $result = (new Pipeline($this->getContainer())) + ->send($std) + ->through([ + function ($std, $next) { + $std->value = 1; + + return $next($std); + }, + function ($std, $next) { + ++$std->value; + + return $next($std); + }, + ])->finally(function ($std) { + $this->assertSame(3, $std->value); + + ++$std->value; + })->then(function ($std) { + ++$std->value; + + return $std; + }); + + $this->assertSame(4, $std->value); + $this->assertSame(4, $result->value); + } + + public function testPipelineFinallyWhenExceptionOccurs() + { + $std = new stdClass(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('My Exception: 1'); + + try { + (new Pipeline($this->getContainer())) + ->send($std) + ->through([ + function ($std, $next) { + $std->value = 1; + + return $next($std); + }, + function ($std) { + throw new Exception('My Exception: ' . $std->value); + }, + ])->finally(function ($std) { + $this->assertSame(1, $std->value); + + ++$std->value; + })->then(function ($std) { + $std->value = 0; + + return $std; + }); + } catch (Exception $e) { + $this->assertSame('My Exception: 1', $e->getMessage()); + $this->assertSame(2, $std->value); + + throw $e; + } + } + public function testPipelineMacro() { Pipeline::macro('customMethod', function ($value) {