diff --git a/.gitattributes b/.gitattributes index 7380ba8..009d49f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ * text=auto **/*Test.php export-ignore +**/*TestCase.php export-ignore **/Fake*.php export-ignore /.gitattributes export-ignore /.gitignore export-ignore diff --git a/.styleci.yml b/.styleci.yml index 88b8e58..6c847ba 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,12 +1,5 @@ -preset: recommended - +preset: psr12 enabled: - not_operator_with_successor_space - phpdoc_no_empty_return - unalign_double_arrow - -disabled: - - align_double_arrow - - phpdoc_align - - phpdoc_separation - - simplified_null_return diff --git a/composer.json b/composer.json index fbfc6fd..b062202 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "php": "^7.4" }, "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^8", + "sempro/phpunit-pretty-print": "^1.2" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index fd87600..063c68a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,9 @@ - + src/ diff --git a/src/CriteriaTest.php b/src/CriteriaTest.php index 214b113..515b685 100644 --- a/src/CriteriaTest.php +++ b/src/CriteriaTest.php @@ -6,7 +6,6 @@ use Distil\Exceptions\CannotAddCriterion; use Distil\Exceptions\CannotGetCriterion; -use Distil\FakeCriterion; use PHPUnit\Framework\TestCase; use function array_values; diff --git a/src/Exceptions/InvalidKeyword.php b/src/Exceptions/InvalidKeyword.php new file mode 100644 index 0000000..8f36277 --- /dev/null +++ b/src/Exceptions/InvalidKeyword.php @@ -0,0 +1,37 @@ +value(); - - if (! is_bool($value)) { - throw InvalidCriterionValue::expectedBoolean(static::class); - } - - return new static($value); + return self::fromKeyword(new BooleanKeyword($value)); } public function value(): bool @@ -49,19 +42,6 @@ public function isTruthy(): bool public function isFalsy(): bool { - return ! $this->isTruthy(); - } - - public static function keywords(): array - { - return [ - static::KEYWORD_TRUE => true, - static::KEYWORD_FALSE => false, - ]; - } - - public function __toString(): string - { - return (new Value($this, $this->value))->keyword(); + return ! $this->value; } } diff --git a/src/Types/DateTimeCriterion.php b/src/Types/DateTimeCriterion.php index 74af5e5..d5d65e8 100644 --- a/src/Types/DateTimeCriterion.php +++ b/src/Types/DateTimeCriterion.php @@ -2,42 +2,41 @@ namespace Distil\Types; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use Distil\ActsAsCriteriaFactory; use Distil\Criterion; -use Distil\Exceptions\InvalidCriterionValue; -use Distil\Keywords\Keyword; -use Distil\Keywords\Value; +use Distil\Values\ConstructsFromKeyword; +use Distil\Values\DateTimeKeyword; abstract class DateTimeCriterion implements Criterion { use ActsAsCriteriaFactory; + use ConstructsFromKeyword; private DateTimeInterface $value; private string $format; - public function __construct(DateTimeInterface $value, string $format = DateTime::ATOM) + public function __construct(DateTimeInterface $value, string $format = DateTimeInterface::ATOM) { $this->value = $value; $this->format = $format; } + public function __toString(): string + { + if ($this->keyword) { + return (string) $this->keyword; + } + + return $this->value->format($this->format); + } + /** * @return static */ - public static function fromString(string $value, string $format = DateTime::ATOM): self + public static function fromString(string $value): self { - $value = (new Keyword(static::class, $value))->value(); - - if ($value instanceof DateTimeInterface) { - return new static($value, $format); - } elseif (strtotime($value)) { - return new static(new DateTimeImmutable($value), $format); - } - - throw InvalidCriterionValue::expectedTimeString(static::class); + return self::fromKeyword(new DateTimeKeyword($value)); } public function value(): DateTimeInterface @@ -49,9 +48,4 @@ public function format(): string { return $this->format; } - - public function __toString(): string - { - return (new Value($this, $this->value))->keyword() ?: $this->value->format($this->format); - } } diff --git a/src/Types/IntegerCriterion.php b/src/Types/IntegerCriterion.php index d7ab50a..91bfef1 100644 --- a/src/Types/IntegerCriterion.php +++ b/src/Types/IntegerCriterion.php @@ -5,12 +5,14 @@ use Distil\ActsAsCriteriaFactory; use Distil\Criterion; use Distil\Exceptions\InvalidCriterionValue; -use Distil\Keywords\Keyword; -use Distil\Keywords\Value; +use Distil\Values\ConstructsFromKeyword; + +use function is_numeric; abstract class IntegerCriterion implements Criterion { use ActsAsCriteriaFactory; + use ConstructsFromKeyword; private int $value; @@ -19,13 +21,8 @@ public function __construct(int $value) $this->value = $value; } - /** - * @return static - */ public static function fromString(string $value): self { - $value = (new Keyword(static::class, $value))->value(); - if (! is_numeric($value)) { throw InvalidCriterionValue::expectedNumeric(static::class); } @@ -37,9 +34,4 @@ public function value(): int { return $this->value; } - - public function __toString(): string - { - return (new Value($this, $this->value))->keyword() ?: (string) $this->value; - } } diff --git a/src/Types/StringCriterion.php b/src/Types/StringCriterion.php index ec55bda..6c9787a 100644 --- a/src/Types/StringCriterion.php +++ b/src/Types/StringCriterion.php @@ -4,27 +4,10 @@ use Distil\ActsAsCriteriaFactory; use Distil\Criterion; -use Distil\Keywords\Keyword; -use Distil\Keywords\Value; +use Distil\Values\ConstructsFromKeyword; abstract class StringCriterion implements Criterion { use ActsAsCriteriaFactory; - - private string $value; - - public function __construct(string $value) - { - $this->value = (new Keyword(static::class, $value))->value(); - } - - public function value(): string - { - return $this->value; - } - - public function __toString(): string - { - return (new Value($this, $this->value))->keyword() ?: $this->value; - } + use ConstructsFromKeyword; } diff --git a/src/Values/BooleanKeyword.php b/src/Values/BooleanKeyword.php new file mode 100644 index 0000000..a6d4663 --- /dev/null +++ b/src/Values/BooleanKeyword.php @@ -0,0 +1,54 @@ + true, + '1' => true, + 'false' => false, + '0' => false, + ]; + + private string $stringValue; + private bool $castedValue; + + public function __construct(string $keyword) + { + $this->guardAgainstInvalidValues($keyword); + + $this->stringValue = $keyword; + $this->castedValue = self::CASTED_VALUES[$keyword]; + } + + private function guardAgainstInvalidValues(string $keyword): void + { + if ($this->isNotAKeyword($keyword)) { + throw InvalidKeyword::cannotBeCastedToBoolean($keyword); + } + } + + public function __toString(): string + { + return $this->stringValue; + } + + public function castedValue(): bool + { + return $this->castedValue; + } + + private function isNotAKeyword(string $keyword): bool + { + return ! array_key_exists($keyword, self::CASTED_VALUES); + } +} diff --git a/src/Values/BooleanKeywordTest.php b/src/Values/BooleanKeywordTest.php new file mode 100644 index 0000000..469f077 --- /dev/null +++ b/src/Values/BooleanKeywordTest.php @@ -0,0 +1,100 @@ +expectExceptionObject(InvalidKeyword::cannotBeCastedToBoolean($invalidValue)); + + new BooleanKeyword($invalidValue); + } + + /** + * @test + */ + public function it_implements_the_keyword_interface(): void + { + $keyword = new BooleanKeyword('true'); + + $this->assertInstanceOf(Keyword::class, $keyword); + } + + public function validStringValues(): array + { + return [ + [ + 'string_value' => 'true', + 'expected_casted_value' => true, + ], + [ + 'string_value' => '1', + 'expected_casted_value' => true, + ], + [ + 'string_value' => 'false', + 'expected_casted_value' => false, + ], + [ + 'string_value' => '0', + 'expected_casted_value' => false, + ], + ]; + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_return_its_casted_value(string $stringValue, bool $expectedCastedValue): void + { + $this->assertSame($expectedCastedValue, (new BooleanKeyword($stringValue))->castedValue()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_be_casted_to_a_string(string $keyword): void + { + $this->assertSame($keyword, (string) new BooleanKeyword($keyword)); + } + + /** + * @test + */ + public function it_can_be_initialized_to_accept_nullable_values(): void + { + $keyword = BooleanKeyword::nullable(NullableKeyword::VALUE); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertSame(null, $keyword->castedValue()); + $this->assertNull($keyword->deferredKeyword()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_returns_the_deferred_keyword_when_accepting_nullable_values_but_the_value_is_not_nullable( + string $stringValue, + $expectedCastedValue + ): void { + $keyword = BooleanKeyword::nullable($stringValue); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertEquals($expectedCastedValue, (new BooleanKeyword($stringValue))->castedValue()); + $this->assertEquals(new BooleanKeyword($stringValue), $keyword->deferredKeyword()); + } +} diff --git a/src/Values/ConstructsFromKeyword.php b/src/Values/ConstructsFromKeyword.php new file mode 100644 index 0000000..83565b3 --- /dev/null +++ b/src/Values/ConstructsFromKeyword.php @@ -0,0 +1,30 @@ +keyword) { + return (string) $this->keyword; + } + + return $this->value; + } + + public static function fromKeyword(Keyword $keyword): self + { + $instance = new static($keyword->castedValue()); + $instance->keyword = $keyword; + + return $instance; + } + + public function keyword(): ?Keyword + { + return $this->keyword; + } +} diff --git a/src/Values/ConstructsFromKeywordTest.php b/src/Values/ConstructsFromKeywordTest.php new file mode 100644 index 0000000..62855f4 --- /dev/null +++ b/src/Values/ConstructsFromKeywordTest.php @@ -0,0 +1,80 @@ +assertInstanceOf(get_class($constructsFromKeyword), $instance); + } + + /** + * @test + */ + public function it_can_return_the_keyword(): void + { + $keyword = FakeKeyword::casted(FakeCastsKeyword::ORIGINAL_VALUE); + $constructsFromKeyword = new FakeCastsKeyword(); + + $instance = $constructsFromKeyword::fromKeyword($keyword); + + $this->assertSame($keyword, $instance->keyword()); + } + + /** + * @test + */ + public function it_returns_null_when_not_constructed_from_a_keyword(): void + { + $constructsFromKeyword = new FakeCastsKeyword(); + + $this->assertNull($constructsFromKeyword->keyword()); + } + + /** + * @test + */ + public function it_returns_the_keyword_when_casting_to_a_string(): void + { + $keyword = FakeKeyword::casted(FakeCastsKeyword::ORIGINAL_VALUE); + $constructsFromKeyword = new FakeCastsKeyword(); + + $instance = $constructsFromKeyword::fromKeyword($keyword); + + $this->assertSame((string) $keyword, (string) $instance); + } + + /** + * @test + */ + public function it_returns_the_string_value_when_casting_to_a_string_when_not_constructed_from_a_keyword(): void + { + $constructsFromKeyword = new FakeCastsKeyword(); + + $this->assertSame((string) $constructsFromKeyword::ORIGINAL_VALUE, (string) $constructsFromKeyword); + } +} + +final class FakeCastsKeyword +{ + use ConstructsFromKeyword; + + public const ORIGINAL_VALUE = 'foo'; + + private string $value = self::ORIGINAL_VALUE; +} diff --git a/src/Values/ConstructsNullableKeyword.php b/src/Values/ConstructsNullableKeyword.php new file mode 100644 index 0000000..22a5a11 --- /dev/null +++ b/src/Values/ConstructsNullableKeyword.php @@ -0,0 +1,11 @@ + new self($keyword)); + } +} diff --git a/src/Values/DateTimeKeyword.php b/src/Values/DateTimeKeyword.php new file mode 100644 index 0000000..67fb01b --- /dev/null +++ b/src/Values/DateTimeKeyword.php @@ -0,0 +1,57 @@ +guardAgainstInvalidValues($keyword); + $parsedKeyword = $this->isTimestamp($keyword) ? "@$keyword" : $keyword; + + $this->stringValue = $keyword; + $this->castedValue = new DateTimeImmutable($parsedKeyword); + } + + private function guardAgainstInvalidValues(string $keyword): void + { + if (! $this->isValidKeyword($keyword)) { + throw InvalidKeyword::cannotBeCastedToDateTime($keyword); + } + } + + public function __toString(): string + { + return $this->stringValue; + } + + public function castedValue(): DateTimeInterface + { + return $this->castedValue; + } + + private function isValidKeyword(string $keyword): bool + { + return $this->isTimestamp($keyword) || strtotime($keyword) !== false; + } + + private function isTimestamp(string $keyword): bool + { + return is_numeric($keyword) && date_create("@$keyword") !== false; + } +} diff --git a/src/Values/DateTimeKeywordTest.php b/src/Values/DateTimeKeywordTest.php new file mode 100644 index 0000000..72c2633 --- /dev/null +++ b/src/Values/DateTimeKeywordTest.php @@ -0,0 +1,107 @@ +expectExceptionObject(InvalidKeyword::cannotBeCastedToDateTime($keyword)); + + new DateTimeKeyword($keyword); + } + + /** + * @test + */ + public function it_implements_the_keyword_interface(): void + { + $keyword = new DateTimeKeyword('today'); + + $this->assertInstanceOf(Keyword::class, $keyword); + } + + public function validStringValues(): array + { + return [ + [ + 'string_value' => $today = 'today', + 'expected_casted_value' => new DateTimeImmutable($today), + ], + [ + 'string_value' => $tomorrow = 'today +1 days', + 'expected_casted_value' => new DateTimeImmutable($tomorrow), + ], + [ + 'string_value' => $timestamp = (string) strtotime('2007-07-28 19:30:00'), + 'expected_casted_value' => new DateTimeImmutable("@$timestamp"), + ], + [ + 'string_value' => $formatted = '2007-07-28 19:30:00', + 'expected_casted_value' => new DateTimeImmutable($formatted), + ], + ]; + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_return_its_casted_value(string $stringValue, DateTimeImmutable $expectedCastedValue): void + { + $keyword = new DateTimeKeyword($stringValue); + + $this->assertEquals($expectedCastedValue, $keyword->castedValue()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_be_casted_to_a_string(string $stringValue): void + { + $keyword = new DateTimeKeyword($stringValue); + + $this->assertEquals($stringValue, (string) $keyword); + } + + /** + * @test + */ + public function it_can_be_initialized_to_accept_nullable_values(): void + { + $keyword = DateTimeKeyword::nullable(NullableKeyword::VALUE); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertSame(null, $keyword->castedValue()); + $this->assertNull($keyword->deferredKeyword()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_returns_the_deferred_keyword_when_accepting_nullable_values_but_the_value_is_not_nullable( + string $stringValue, + $expectedCastedValue + ): void { + $keyword = DateTimeKeyword::nullable($stringValue); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertEquals($expectedCastedValue, (new DateTimeKeyword($stringValue))->castedValue()); + $this->assertEquals(new DateTimeKeyword($stringValue), $keyword->deferredKeyword()); + } +} diff --git a/src/Values/FakeKeyword.php b/src/Values/FakeKeyword.php new file mode 100644 index 0000000..0ed254e --- /dev/null +++ b/src/Values/FakeKeyword.php @@ -0,0 +1,41 @@ +stringValue = $keyword; + $this->castedValue = $castedValue; + } + + public static function casted($value, string $keyword = self::VALUE): self + { + return new self($keyword, $value); + } + + public function __toString(): string + { + return $this->stringValue; + } + + public function castedValue() + { + return $this->castedValue; + } +} diff --git a/src/Values/IntegerKeyword.php b/src/Values/IntegerKeyword.php new file mode 100644 index 0000000..822b6b8 --- /dev/null +++ b/src/Values/IntegerKeyword.php @@ -0,0 +1,48 @@ +guardAgainstInvalidValues($keyword); + + $this->stringValue = $keyword; + $this->castedValue = (int) $keyword; + } + + private function guardAgainstInvalidValues(string $keyword): void + { + if (! $this->isInt($keyword)) { + throw InvalidKeyword::cannotBeCastedToInteger($keyword); + } + } + + public function __toString(): string + { + return $this->stringValue; + } + + public function castedValue(): int + { + return $this->castedValue; + } + + private function isInt(string $keyword): bool + { + return is_numeric($keyword) && is_int($keyword + 0); + } +} diff --git a/src/Values/IntegerKeywordTest.php b/src/Values/IntegerKeywordTest.php new file mode 100644 index 0000000..65173e2 --- /dev/null +++ b/src/Values/IntegerKeywordTest.php @@ -0,0 +1,105 @@ +expectException(InvalidKeyword::class); + + new IntegerKeyword($stringValue); + } + + /** + * @test + */ + public function it_implements_the_keyword_interface(): void + { + $keyword = new IntegerKeyword('20070728'); + + $this->assertInstanceOf(Keyword::class, $keyword); + } + + public function validStringValues(): array + { + return [ + [ + 'string_value' => '369', + 'expected_casted_value' => 369, + ], + [ + 'string_value' => '0', + 'expected_casted_value' => 0, + ], + ]; + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_return_its_casted_value(string $stringValue, int $expectedCastedValue): void + { + $keyword = new IntegerKeyword($stringValue); + + $this->assertSame($expectedCastedValue, $keyword->castedValue()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_can_be_casted_to_a_string(string $stringValue): void + { + $keyword = new IntegerKeyword($stringValue); + + $this->assertEquals($stringValue, (string) $keyword); + } + + /** + * @test + */ + public function it_can_be_initialized_to_accept_nullable_values(): void + { + $keyword = IntegerKeyword::nullable(NullableKeyword::VALUE); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertSame(null, $keyword->castedValue()); + $this->assertNull($keyword->deferredKeyword()); + } + + /** + * @test + * @dataProvider validStringValues + */ + public function it_returns_the_deferred_keyword_when_accepting_nullable_values_but_the_value_is_not_nullable( + string $stringValue, + $expectedCastedValue + ): void { + $keyword = IntegerKeyword::nullable($stringValue); + + $this->assertInstanceOf(NullableKeyword::class, $keyword); + $this->assertEquals($expectedCastedValue, (new IntegerKeyword($stringValue))->castedValue()); + $this->assertEquals(new IntegerKeyword($stringValue), $keyword->deferredKeyword()); + } +} diff --git a/src/Values/Keyword.php b/src/Values/Keyword.php new file mode 100644 index 0000000..7a48d49 --- /dev/null +++ b/src/Values/Keyword.php @@ -0,0 +1,12 @@ +isNullValue($keyword)) { + $this->deferredKeyword = $deferredKeyword($keyword); + } + } + + private function isNullValue(string $keyword): bool + { + return $keyword === self::VALUE; + } + + public function __toString(): string + { + if ($this->deferredKeyword) { + return (string) $this->deferredKeyword; + } + + return self::VALUE; + } + + public function castedValue() + { + if ($this->deferredKeyword) { + return $this->deferredKeyword->castedValue(); + } + + return null; + } + + public function deferredKeyword(): ?Keyword + { + return $this->deferredKeyword; + } +} diff --git a/src/Values/NullableKeywordTest.php b/src/Values/NullableKeywordTest.php new file mode 100644 index 0000000..30cc710 --- /dev/null +++ b/src/Values/NullableKeywordTest.php @@ -0,0 +1,57 @@ +expectException(TypeError::class); + + new NullableKeyword('foo', fn () => 'invalid deferred keyword'); + } + + /** + * @test + */ + public function it_implements_the_keyword_interface(): void + { + $keyword = new NullableKeyword('null', fn () => null); + + $this->assertInstanceOf(Keyword::class, $keyword); + } + + /** + * @test + */ + public function it_can_cast_the_null_keyword_to_the_null_value(): void + { + $keyword = new NullableKeyword(self::EXPECTED_VALUE, fn (string $keyword) => FakeKeyword::casted($keyword)); + + $this->assertSame(self::EXPECTED_VALUE, (string) $keyword); + $this->assertNull($keyword->castedValue()); + $this->assertNull($keyword->deferredKeyword()); + } + + /** + * @test + */ + public function it_uses_the_deferred_keyword_when_the_keyword_value_is_not_null(): void + { + $keyword = new NullableKeyword('foo', fn (string $keyword) => FakeKeyword::casted($keyword)); + + $this->assertSame(FakeKeyword::VALUE, (string) $keyword); + $this->assertSame('foo', $keyword->castedValue()); + $this->assertInstanceOf(FakeKeyword::class, $keyword->deferredKeyword()); + } +}