From e92d9bfaae44c70ee94c7f54617725033ef9aaaa Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 8 Oct 2021 15:29:35 +0200 Subject: [PATCH] implements decorators pipeline --- src/Concerns/Extendable.php | 34 +++++++++ src/Expectation.php | 48 ++++++++++--- src/Support/Extendable.php | 5 ++ src/Support/Pipeline.php | 69 +++++++++++++++++++ .../Features/Expect/HigherOrder/decorate.php | 56 +++++++++++++++ 5 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 src/Support/Pipeline.php create mode 100644 tests/Features/Expect/HigherOrder/decorate.php diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index ee87f9ab..e5758ed1 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -17,6 +17,9 @@ trait Extendable */ private static $extends = []; + /** @var array> */ + private static $decorators = []; + /** * Register a custom extend. */ @@ -25,6 +28,11 @@ trait Extendable static::$extends[$name] = $extend; } + public static function decorate(string $name, Closure $decorator): void + { + static::$decorators[$name][] = $decorator; + } + /** * Checks if extend is registered. */ @@ -33,6 +41,32 @@ trait Extendable return array_key_exists($name, static::$extends); } + /** + * Checks if decorator are registered. + */ + public static function hasDecorators(string $name): bool + { + return array_key_exists($name, static::$decorators); + } + + /** + * @return array + */ + public function decorators(string $name, object $context, string $scope): array + { + if (!self::hasDecorators($name)) { + return []; + } + + $decorators = []; + foreach (self::$decorators[$name] as $decorator) { + //@phpstan-ignore-next-line + $decorators[] = $decorator->bindTo($context, $scope); + } + + return $decorators; + } + /** * Dynamically handle calls to the class. * diff --git a/src/Expectation.php b/src/Expectation.php index 5c64b3fd..aa6b3efc 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,6 +7,7 @@ namespace Pest; use BadMethodCallException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; +use Pest\Support\Pipeline; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; @@ -250,23 +251,50 @@ final class Expectation * * @param array $parameters * - * @return HigherOrderExpectation|mixed + * @return HigherOrderExpectation|Expectation */ public function __call(string $method, array $parameters) { - if (method_exists($this->coreExpectation, $method)) { - //@phpstan-ignore-next-line - $this->coreExpectation = $this->coreExpectation->{$method}(...$parameters); - - return $this; - } - - if (!self::hasExtend($method)) { + if (!$this->hasExpectation($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - return $this->__extendsCall($method, $parameters); + Pipeline::send(...$parameters) + ->through($this->decorators($method, $this, Expectation::class)) + ->finally(function ($parameters) use ($method): void { + $this->callExpectation($method, $parameters); + }); + + return $this; + } + + /** + * @param array $parameters + */ + private function callExpectation(string $name, array $parameters): void + { + if (method_exists($this->coreExpectation, $name)) { + //@phpstan-ignore-next-line + $this->coreExpectation->{$name}(...$parameters); + } else { + if (self::hasExtend($name)) { + $this->__extendsCall($name, $parameters); + } + } + } + + private function hasExpectation(string $name): bool + { + if (method_exists($this->coreExpectation, $name)) { + return true; + } + + if (self::hasExtend($name)) { + return true; + } + + return false; } /** diff --git a/src/Support/Extendable.php b/src/Support/Extendable.php index dcbb7a5a..341bea02 100644 --- a/src/Support/Extendable.php +++ b/src/Support/Extendable.php @@ -30,4 +30,9 @@ final class Extendable { $this->extendableClass::extend($name, $extend); } + + public function decorate(string $name, Closure $extend): void + { + $this->extendableClass::decorate($name, $extend); + } } diff --git a/src/Support/Pipeline.php b/src/Support/Pipeline.php new file mode 100644 index 00000000..2fd7121b --- /dev/null +++ b/src/Support/Pipeline.php @@ -0,0 +1,69 @@ + */ + private $pipes = []; + + /** @var array */ + private $passable; + + /** + * @param array $passable + */ + public function __construct(...$passable) + { + $this->passable = $passable; + } + + /** + * @param array $passable + */ + public static function send(...$passable): self + { + return new self(...$passable); + } + + /** + * @param array $pipes + */ + public function through(array $pipes): self + { + $this->pipes = $pipes; + + return $this; + } + + public function finally(Closure $finalClosure): void + { + $pipeline = array_reduce( + array_reverse($this->pipes), + $this->carry(), + $this->prepareFinalClosure($finalClosure) + ); + + $pipeline(...$this->passable); + } + + public function carry(): Closure + { + return function ($stack, $pipe): Closure { + return function (...$passable) use ($stack, $pipe) { + return $pipe($stack, ...$passable); + }; + }; + } + + private function prepareFinalClosure(Closure $finalClosure): Closure + { + return function (...$passable) use ($finalClosure) { + return $finalClosure($passable); + }; + } +} diff --git a/tests/Features/Expect/HigherOrder/decorate.php b/tests/Features/Expect/HigherOrder/decorate.php new file mode 100644 index 00000000..cb1602f7 --- /dev/null +++ b/tests/Features/Expect/HigherOrder/decorate.php @@ -0,0 +1,56 @@ +value = $value; + } +} + +class Character +{ + public $value; + + public function __construct($value) + { + $this->value = $value; + } +} + +expect()->decorate('toBe', function ($next, $expected) { + if ($this->value instanceof Character) { + assertInstanceOf(Character::class, $expected); + assertEquals($this->value->value, $expected->value); + + return; + } + + $next($expected); +}); + +expect()->decorate('toBe', function ($next, $expected) { + if ($this->value instanceof Number) { + assertInstanceOf(Number::class, $expected); + assertEquals($this->value->value, $expected->value); + + return; + } + + $next($expected); +}); + +test('pass', function () { + $number = new Number(1); + + $letter = new Character('A'); + + expect($number)->toBe(new Number(1)); + expect($letter)->toBe(new Character('A')); + expect(3)->toBe(3); +});