*/ final class Expectation { /** * The exporter instance, if any. * * @readonly */ private Exporter|null $exporter = null; /** * Creates a new expectation. * * @param TValue $value */ public function __construct( public mixed $value ) { // .. } /** * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. * * @return Expectation */ public function toBe(mixed $expected): Expectation { Assert::assertSame($expected, $this->value); return $this; } /** * Asserts that the value is empty. * * @return Expectation */ public function toBeEmpty(): Expectation { Assert::assertEmpty($this->value); return $this; } /** * Asserts that the value is true. * * @return Expectation */ public function toBeTrue(): Expectation { Assert::assertTrue($this->value); return $this; } /** * Asserts that the value is truthy. * * @return Expectation */ public function toBeTruthy(): Expectation { Assert::assertTrue((bool) $this->value); return $this; } /** * Asserts that the value is false. * * @return Expectation */ public function toBeFalse(): Expectation { Assert::assertFalse($this->value); return $this; } /** * Asserts that the value is falsy. * * @return Expectation */ public function toBeFalsy(): Expectation { Assert::assertFalse((bool) $this->value); return $this; } /** * Asserts that the value is greater than $expected. * * @return Expectation */ public function toBeGreaterThan(int|float $expected): Expectation { Assert::assertGreaterThan($expected, $this->value); return $this; } /** * Asserts that the value is greater than or equal to $expected. * * @return Expectation */ public function toBeGreaterThanOrEqual(int|float $expected): Expectation { Assert::assertGreaterThanOrEqual($expected, $this->value); return $this; } /** * Asserts that the value is less than or equal to $expected. * * @return Expectation */ public function toBeLessThan(int|float $expected): Expectation { Assert::assertLessThan($expected, $this->value); return $this; } /** * Asserts that the value is less than $expected. * * @return Expectation */ public function toBeLessThanOrEqual(int|float $expected): Expectation { Assert::assertLessThanOrEqual($expected, $this->value); return $this; } /** * Asserts that $needle is an element of the value. * * @return Expectation */ public function toContain(mixed ...$needles): Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { // @phpstan-ignore-next-line Assert::assertStringContainsString((string) $needle, $this->value); } else { if (!is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } Assert::assertContains($needle, $this->value); } } return $this; } /** * Asserts that the value starts with $expected. * * @param non-empty-string $expected * *@return Expectation */ public function toStartWith(string $expected): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertStringStartsWith($expected, $this->value); return $this; } /** * Asserts that the value ends with $expected. * * @param non-empty-string $expected * *@return Expectation */ public function toEndWith(string $expected): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertStringEndsWith($expected, $this->value); return $this; } /** * Asserts that $number matches value's Length. * * @return Expectation */ public function toHaveLength(int $number): Expectation { if (is_string($this->value)) { Assert::assertEquals($number, mb_strlen($this->value)); return $this; } if (is_iterable($this->value)) { return $this->toHaveCount($number); } if (is_object($this->value)) { if (method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); } else { $array = (array) $this->value; } Assert::assertCount($number, $array); return $this; } throw new BadMethodCallException('Expectation value length is not countable.'); } /** * Asserts that $count matches the number of elements of the value. * * @return Expectation */ public function toHaveCount(int $count): Expectation { if (!is_countable($this->value) && !is_iterable($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertCount($count, $this->value); return $this; } /** * Asserts that the value contains the property $name. * * @return Expectation */ public function toHaveProperty(string $name, mixed $value = null): Expectation { $this->toBeObject(); // @phpstan-ignore-next-line Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { /* @phpstan-ignore-next-line */ Assert::assertEquals($value, $this->value->{$name}); } return $this; } /** * Asserts that the value contains the provided properties $names. * * @param iterable $names * *@return Expectation */ public function toHaveProperties(iterable $names): Expectation { foreach ($names as $name) { $this->toHaveProperty($name); } return $this; } /** * Asserts that two variables have the same value. * * @return Expectation */ public function toEqual(mixed $expected): Expectation { Assert::assertEquals($expected, $this->value); return $this; } /** * Asserts that two variables have the same value. * The contents of $expected and the $this->value are * canonicalized before they are compared. For instance, when the two * variables $expected and $this->value are arrays, then these arrays * are sorted before they are compared. When $expected and $this->value * are objects, each object is converted to an array containing all * private, protected and public attributes. * * @return Expectation */ public function toEqualCanonicalizing(mixed $expected): Expectation { Assert::assertEqualsCanonicalizing($expected, $this->value); return $this; } /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. * * @return Expectation */ public function toEqualWithDelta(mixed $expected, float $delta): Expectation { Assert::assertEqualsWithDelta($expected, $this->value, $delta); return $this; } /** * Asserts that the value is one of the given values. * * @param iterable $values * * @return Expectation */ public function toBeIn(iterable $values): Expectation { Assert::assertContains($this->value, $values); return $this; } /** * Asserts that the value is infinite. * * @return Expectation */ public function toBeInfinite(): Expectation { Assert::assertInfinite($this->value); return $this; } /** * Asserts that the value is an instance of $class. * * @param class-string $class * * @return Expectation */ public function toBeInstanceOf(string $class): Expectation { Assert::assertInstanceOf($class, $this->value); return $this; } /** * Asserts that the value is an array. * * @return Expectation */ public function toBeArray(): Expectation { Assert::assertIsArray($this->value); return $this; } /** * Asserts that the value is of type bool. * * @return Expectation */ public function toBeBool(): Expectation { Assert::assertIsBool($this->value); return $this; } /** * Asserts that the value is of type callable. * * @return Expectation */ public function toBeCallable(): Expectation { Assert::assertIsCallable($this->value); return $this; } /** * Asserts that the value is of type float. * * @return Expectation */ public function toBeFloat(): Expectation { Assert::assertIsFloat($this->value); return $this; } /** * Asserts that the value is of type int. * * @return Expectation */ public function toBeInt(): Expectation { Assert::assertIsInt($this->value); return $this; } /** * Asserts that the value is of type iterable. * * @return Expectation */ public function toBeIterable(): Expectation { Assert::assertIsIterable($this->value); return $this; } /** * Asserts that the value is of type numeric. * * @return Expectation */ public function toBeNumeric(): Expectation { Assert::assertIsNumeric($this->value); return $this; } /** * Asserts that the value is of type object. * * @return Expectation */ public function toBeObject(): Expectation { Assert::assertIsObject($this->value); return $this; } /** * Asserts that the value is of type resource. * * @return Expectation */ public function toBeResource(): Expectation { Assert::assertIsResource($this->value); return $this; } /** * Asserts that the value is of type scalar. * * @return Expectation */ public function toBeScalar(): Expectation { Assert::assertIsScalar($this->value); return $this; } /** * Asserts that the value is of type string. * * @return Expectation */ public function toBeString(): Expectation { Assert::assertIsString($this->value); return $this; } /** * Asserts that the value is a JSON string. * * @return Expectation */ public function toBeJson(): Expectation { Assert::assertIsString($this->value); // @phpstan-ignore-next-line Assert::assertJson($this->value); return $this; } /** * Asserts that the value is NAN. * * @return Expectation */ public function toBeNan(): Expectation { Assert::assertNan($this->value); return $this; } /** * Asserts that the value is null. * * @return Expectation */ public function toBeNull(): Expectation { Assert::assertNull($this->value); return $this; } /** * Asserts that the value array has the provided $key. * * @return Expectation */ public function toHaveKey(string|int $key, mixed $value = null): Expectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); } else { $array = (array) $this->value; } try { Assert::assertTrue(Arr::has($array, $key)); /* @phpstan-ignore-next-line */ } catch (ExpectationFailedException $exception) { throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); } if (func_num_args() > 1) { Assert::assertEquals($value, Arr::get($array, $key)); } return $this; } /** * Asserts that the value array has the provided $keys. * * @param array $keys * * @return Expectation */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $key) { $this->toHaveKey($key); } return $this; } /** * Asserts that the value is a directory. * * @return Expectation */ public function toBeDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryExists($this->value); return $this; } /** * Asserts that the value is a directory and is readable. * * @return Expectation */ public function toBeReadableDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsReadable($this->value); return $this; } /** * Asserts that the value is a directory and is writable. * * @return Expectation */ public function toBeWritableDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsWritable($this->value); return $this; } /** * Asserts that the value is a file. * * @return Expectation */ public function toBeFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileExists($this->value); return $this; } /** * Asserts that the value is a file and is readable. * * @return Expectation */ public function toBeReadableFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileIsReadable($this->value); return $this; } /** * Asserts that the value is a file and is writable. * * @return Expectation */ public function toBeWritableFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileIsWritable($this->value); return $this; } /** * Asserts that the value array matches the given array subset. * * @param iterable $array * * @return Expectation */ public function toMatchArray(iterable|object $array): Expectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $valueAsArray = $this->value->toArray(); } else { $valueAsArray = (array) $this->value; } foreach ($array as $key => $value) { Assert::assertArrayHasKey($key, $valueAsArray); Assert::assertEquals( $value, $valueAsArray[$key], sprintf( 'Failed asserting that an array has a key %s with the value %s.', $this->export($key), $this->export($valueAsArray[$key]), ), ); } return $this; } /** * Asserts that the value object matches a subset * of the properties of an given object. * * @param iterable|object $object * * @return Expectation */ public function toMatchObject(iterable|object $object): Expectation { foreach ((array) $object as $property => $value) { if (!is_object($this->value) && !is_string($this->value)) { InvalidExpectationValue::expected('object|string'); } Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ $propertyValue = $this->value->{$property}; Assert::assertEquals( $value, $propertyValue, sprintf( 'Failed asserting that an object has a property %s with the value %s.', $this->export($property), $this->export($propertyValue), ), ); } return $this; } /** * Asserts that the value matches a regular expression. * * @return Expectation */ public function toMatch(string $expression): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertMatchesRegularExpression($expression, $this->value); return $this; } /** * Asserts that the value matches a constraint. * * @return Expectation */ public function toMatchConstraint(Constraint $constraint): Expectation { Assert::assertThat($this->value, $constraint); return $this; } /** * Asserts that executing value throws an exception. * * @param (Closure(Throwable): mixed)|string $exception * * @return Expectation */ public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation { $callback = NullClosure::create(); if ($exception instanceof Closure) { $callback = $exception; $parameters = (new ReflectionFunction($exception))->getParameters(); if (1 !== count($parameters)) { throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); } if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); } $exception = $type->getName(); } try { ($this->value)(); } catch (Throwable $e) { if (!class_exists($exception)) { if ($e instanceof Error && $e->getMessage() === "Class \"$exception\" not found") { throw $e; } Assert::assertStringContainsString($exception, $e->getMessage()); return $this; } if ($exceptionMessage !== null) { Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); } Assert::assertInstanceOf($exception, $e); $callback($e); return $this; } if (!class_exists($exception)) { throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); } throw new ExpectationFailedException("Exception \"$exception\" not thrown."); } /** * Exports the given value. */ private function export(mixed $value): string { if ($this->exporter === null) { $this->exporter = new Exporter(); } return $this->exporter->export($value); } }