implements decorators pipeline

This commit is contained in:
Fabio Ivona
2021-10-08 15:29:35 +02:00
parent d802e88148
commit e92d9bfaae
5 changed files with 202 additions and 10 deletions

View File

@ -17,6 +17,9 @@ trait Extendable
*/
private static $extends = [];
/** @var array<string, array<Closure>> */
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<int, Closure>
*/
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.
*

View File

@ -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<int, mixed> $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<mixed> $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;
}
/**

View File

@ -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);
}
}

69
src/Support/Pipeline.php Normal file
View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Closure;
final class Pipeline
{
/** @var array<Closure> */
private $pipes = [];
/** @var array<mixed> */
private $passable;
/**
* @param array<mixed> $passable
*/
public function __construct(...$passable)
{
$this->passable = $passable;
}
/**
* @param array<mixed> $passable
*/
public static function send(...$passable): self
{
return new self(...$passable);
}
/**
* @param array<Closure> $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);
};
}
}

View File

@ -0,0 +1,56 @@
<?php
use function PHPUnit\Framework\assertEquals;
use function PHPUnit\Framework\assertInstanceOf;
class Number
{
public $value;
public function __construct($value)
{
$this->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);
});