*/ final class HigherOrderExpectation { use RetrievesValues; /** * @var Expectation|Each */ private Expectation|Each $expectation; private bool $opposite = false; private bool $shouldReset = false; /** * Creates a new higher order expectation. * * @param Expectation $original * @param TValue $value */ public function __construct(private Expectation $original, mixed $value) { $this->expectation = $this->expect($value); } /** * Creates the opposite expectation for the value. * * @return self */ public function not(): HigherOrderExpectation { $this->opposite = !$this->opposite; return $this; } /** * Creates a new Expectation. * * @template TExpectValue * * @param TExpectValue $value * * @return Expectation */ public function expect(mixed $value): Expectation { return new Expectation($value); } /** * Creates a new expectation. * * @template TExpectValue * * @param TExpectValue $value * * @return Expectation */ public function and(mixed $value): Expectation { return $this->expect($value); } /** * Dynamically calls methods on the class with the given arguments. * * @param array $arguments * * @return self|self */ public function __call(string $name, array $arguments): self { if (!$this->expectationHasMethod($name)) { /* @phpstan-ignore-next-line */ return new self($this->original, $this->getValue()->$name(...$arguments)); } return $this->performAssertion($name, $arguments); } /** * Accesses properties in the value or in the expectation. * * @return self|self */ public function __get(string $name): self { if ($name === 'not') { return $this->not(); } if (!$this->expectationHasMethod($name)) { /** @var array|object $value */ $value = $this->getValue(); return new self($this->original, $this->retrieve($name, $value)); } return $this->performAssertion($name, []); } /** * Determines if the original expectation has the given method name. */ private function expectationHasMethod(string $name): bool { return method_exists($this->original, $name) || $this->original::hasMethod($name) || $this->original::hasExtend($name); } /** * Retrieve the applicable value based on the current reset condition. * * @return TOriginalValue|TValue */ private function getValue(): mixed { // @phpstan-ignore-next-line return $this->shouldReset ? $this->original->value : $this->expectation->value; } /** * Performs the given assertion with the current expectation. * * @param array $arguments * * @return self */ private function performAssertion(string $name, array $arguments): self { /* @phpstan-ignore-next-line */ $this->expectation = ($this->opposite ? $this->expectation->not() : $this->expectation)->{$name}(...$arguments); $this->opposite = false; $this->shouldReset = true; return $this; } }