From 35d0084ca12e14af19bf14ea8162fe3e0ef4fcca Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Wed, 8 Jan 2025 17:31:21 -0500 Subject: [PATCH 01/31] Implement validation framework with Validator, validation rules, and exceptions --- clover.xml | 568 +++++++++++------- src/Concerns/BaseData.php | 1 - src/Concerns/ValidateableData.php | 16 + src/Contexts/ClassContext.php | 6 +- src/Contracts/ValidateableData.php | 32 + src/Exceptions/DataValidationException.php | 9 + src/Exceptions/ValidationRuleException.php | 15 + src/Validation/Rules/BackedEnumRule.php | 88 +++ src/Validation/Rules/NullableRule.php | 30 + src/Validation/Rules/RequiredRule.php | 30 + src/Validation/Rules/ValidationRule.php | 21 + src/Validation/Validator.php | 8 + tests/Unit/Validation/BackedEnumRuleTest.php | 136 +++++ tests/Unit/Validation/NullableRuleTest.php | 53 ++ tests/Unit/Validation/RequireRuleTest.php | 67 +++ .../Validation/ValidationRuleUnitCase.php | 59 ++ 16 files changed, 904 insertions(+), 235 deletions(-) create mode 100644 src/Concerns/ValidateableData.php create mode 100644 src/Contracts/ValidateableData.php create mode 100644 src/Exceptions/DataValidationException.php create mode 100644 src/Exceptions/ValidationRuleException.php create mode 100644 src/Validation/Rules/BackedEnumRule.php create mode 100644 src/Validation/Rules/NullableRule.php create mode 100644 src/Validation/Rules/RequiredRule.php create mode 100644 src/Validation/Rules/ValidationRule.php create mode 100644 src/Validation/Validator.php create mode 100644 tests/Unit/Validation/BackedEnumRuleTest.php create mode 100644 tests/Unit/Validation/NullableRuleTest.php create mode 100644 tests/Unit/Validation/RequireRuleTest.php create mode 100644 tests/Unit/Validation/ValidationRuleUnitCase.php diff --git a/clover.xml b/clover.xml index 26f192e..8625f79 100644 --- a/clover.xml +++ b/clover.xml @@ -1,6 +1,6 @@ - - + + @@ -177,67 +177,67 @@ - - + + + - - - + + + - + - - - - - - + + + + + + - - - - + + + + - - - + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - + + + + + @@ -287,6 +287,14 @@ + + + + + + + + @@ -371,46 +379,46 @@ - - + - - - - - - - - - + + + + + + + + + - - - - - - + + + + + - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -419,190 +427,190 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + - - - - - - - - - + + + + + + + + + - + + + + + + - - - - - + + + + - - - - - - - - - - - - - + + + + + + + + + + + + - - + + - - - + + + - - - + + + - - - + + + - - - - - + + + + + + - + - - - - + + + + - - - - - - + + + + + + - - - - - - + + + + + - - + + - - - - + + + + + + @@ -627,10 +635,10 @@ - - - - + + + + @@ -707,6 +715,9 @@ + + + @@ -898,6 +909,12 @@ + + + + + + @@ -1050,6 +1067,14 @@ + + + + + + + + @@ -1659,6 +1684,85 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 2e9b0d1..7d42eeb 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -9,7 +9,6 @@ use Nuxtifyts\PhpDto\Normalizers\Concerns\HasNormalizers; use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline; use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable; -use ReflectionClass; use Throwable; trait BaseData diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php new file mode 100644 index 0000000..0cd4760 --- /dev/null +++ b/src/Concerns/ValidateableData.php @@ -0,0 +1,16 @@ + + */ + public static function validationRules(): array + { + return []; + } +} diff --git a/src/Contexts/ClassContext.php b/src/Contexts/ClassContext.php index c7cde2e..ac52b55 100644 --- a/src/Contexts/ClassContext.php +++ b/src/Contexts/ClassContext.php @@ -139,7 +139,8 @@ private function syncClassAttributes(): void /** * @throws ReflectionException * - * @return T + * @return Data + * @phpstan-return T */ public function newInstanceWithoutConstructor(): mixed { @@ -149,7 +150,8 @@ public function newInstanceWithoutConstructor(): mixed /** * @throws ReflectionException * - * @return T + * @return Data + * @phpstan-return T */ public function newInstanceWithConstructorCall(mixed ...$args): mixed { diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php new file mode 100644 index 0000000..3954688 --- /dev/null +++ b/src/Contracts/ValidateableData.php @@ -0,0 +1,32 @@ + $data + * + * @throws DataValidationException + */ + public static function validate(array $data): void; + + /** + * @param array $data + * + * @throws DataValidationException + */ + public static function validateAndCreate(array $data): static; + + /** + * @return true|array> + */ + public function isValid(): true|array; + + /** + * @return array + */ + public static function validationRules(): array; +} diff --git a/src/Exceptions/DataValidationException.php b/src/Exceptions/DataValidationException.php new file mode 100644 index 0000000..269a395 --- /dev/null +++ b/src/Exceptions/DataValidationException.php @@ -0,0 +1,9 @@ + */ + public protected(set) string $backedEnumClass; + + /** + * @var ?array + */ + public protected(set) ?array $allowedValues = null; + + public string $name { + get { + return 'backed_enum'; + } + } + + public function evaluate(mixed $value): bool + { + if ($value instanceof $this->backedEnumClass) { + /** @var BackedEnum $value */ + $resolvedValue = $value; + } else if (is_string($value) || is_integer($value)) { + $resolvedValue = $this->backedEnumClass::tryFrom($value); + } else { + return false; + } + + return !!$resolvedValue + && ( + is_null($this->allowedValues) + || in_array($resolvedValue->value, array_column($this->allowedValues, 'value')) + ); + } + + /** + * @param ?array $parameters + * + * @throws ValidationRuleException + */ + public static function make(?array $parameters = null): self + { + $instance = new self(); + + $backedEnumClass = $parameters['backedEnumClass'] ?? null; + + if ( + !is_string($backedEnumClass) + || !enum_exists($backedEnumClass) + || !is_subclass_of($backedEnumClass, BackedEnum::class) + ) { + throw ValidationRuleException::invalidParameters(); + } + + $instance->backedEnumClass = $backedEnumClass; + $instance->allowedValues = array_filter( + array_map(static fn (mixed $value) => + ($value instanceof $instance->backedEnumClass) + ? $value + : null, + Arr::getArray($parameters ?? [], 'allowedValues') + ) + ) ?: null; + + return $instance; + } + + public function validationMessage(): string + { + if ($this->allowedValues) { + $allowedValues = implode( + ', ', + array_map(static fn (BackedEnum $value) => $value->value, $this->allowedValues) + ); + + return "The :attribute field must be one of the following values: $allowedValues."; + } else { + return 'The :attribute field is invalid.'; + } + } +} diff --git a/src/Validation/Rules/NullableRule.php b/src/Validation/Rules/NullableRule.php new file mode 100644 index 0000000..dc2e6ca --- /dev/null +++ b/src/Validation/Rules/NullableRule.php @@ -0,0 +1,30 @@ + $parameters + */ + public static function make(?array $parameters = null): self + { + return new self(); + } + + public function validationMessage(): string + { + return ''; + } +} diff --git a/src/Validation/Rules/RequiredRule.php b/src/Validation/Rules/RequiredRule.php new file mode 100644 index 0000000..f85e993 --- /dev/null +++ b/src/Validation/Rules/RequiredRule.php @@ -0,0 +1,30 @@ + $parameters + */ + public static function make(?array $parameters = null): self + { + return new self(); + } + + public function validationMessage(): string + { + return 'The :attribute field is required.'; + } +} diff --git a/src/Validation/Rules/ValidationRule.php b/src/Validation/Rules/ValidationRule.php new file mode 100644 index 0000000..4594e64 --- /dev/null +++ b/src/Validation/Rules/ValidationRule.php @@ -0,0 +1,21 @@ + $parameters + * + * @throws ValidationRuleException + */ + public static function make(?array $parameters = null): self; + + public function validationMessage(): string; +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php new file mode 100644 index 0000000..bd3c57d --- /dev/null +++ b/src/Validation/Validator.php @@ -0,0 +1,8 @@ + YesNoBackedEnum::class + ]); + + self::assertEquals( + 'The :attribute field is invalid.', + $rule->validationMessage() + ); + + $rule = BackedEnumRule::make([ + 'backedEnumClass' => YesNoBackedEnum::class, + 'allowedValues' => $allowedValues = [YesNoBackedEnum::YES] + ]); + + $allowedValues = implode( + ', ', + array_map(static fn (YesNoBackedEnum $value) => $value->value, $allowedValues) + ); + + self::assertEquals( + "The :attribute field must be one of the following values: $allowedValues.", + $rule->validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will_throw_an_exception_if_no_enum_class_is_passed' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => '', + 'expectedResult' => false, + ], + 'Will_throw_an_exception_if_invalid_backed_enum_class_is_passed' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => 'InvalidEnumClass' + ], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => '', + 'expectedResult' => false, + ], + 'Will_throw_an_exception_if_invalid_non_backed_enum_class_is_passed' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoEnum::class + ], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => '', + 'expectedResult' => false, + ], + 'Will return false if the value is invalid backed enum value' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoBackedEnum::class + ], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'Something', + 'expectedResult' => false, + ], + 'Will return false if the value is neither a backed enum, nor a string or an integer' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoBackedEnum::class + ], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => [], + 'expectedResult' => false, + ], + 'Will return true if the value is a valid backed enum value' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoBackedEnum::class + ], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'yes', + 'expectedResult' => true, + ], + 'Will return false if the value is not within the allowed values' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoBackedEnum::class, + 'allowedValues' => [YesNoBackedEnum::NO] + ], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'yes', + 'expectedResult' => false, + ], + 'Will return true if the value passed if an actual backed enum of the same instance' => [ + 'validationRuleClassString' => BackedEnumRule::class, + 'makeParams' => [ + 'backedEnumClass' => YesNoBackedEnum::class + ], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => YesNoBackedEnum::YES, + 'expectedResult' => true, + ] + ]; + } +} diff --git a/tests/Unit/Validation/NullableRuleTest.php b/tests/Unit/Validation/NullableRuleTest.php new file mode 100644 index 0000000..afc38aa --- /dev/null +++ b/tests/Unit/Validation/NullableRuleTest.php @@ -0,0 +1,53 @@ +validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will return true if the value is null' => [ + 'validationRuleClassString' => NullableRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => null, + 'expectedResult' => true, + ], + 'Will return true if the value exists' => [ + 'validationRuleClassString' => NullableRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'Something', + 'expectedResult' => true, + ] + ]; + } +} diff --git a/tests/Unit/Validation/RequireRuleTest.php b/tests/Unit/Validation/RequireRuleTest.php new file mode 100644 index 0000000..3c3414a --- /dev/null +++ b/tests/Unit/Validation/RequireRuleTest.php @@ -0,0 +1,67 @@ +validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will return false if the value is empty string' => [ + 'validationRuleClassString' => RequiredRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '', + 'expectedResult' => false, + ], + 'Will return false if the value is null' => [ + 'validationRuleClassString' => RequiredRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => null, + 'expectedResult' => false, + ], + 'Will return false if the value is falsy' => [ + 'validationRuleClassString' => RequiredRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => false, + 'expectedResult' => false, + ], + 'Will return true otherwise' => [ + 'validationRuleClassString' => RequiredRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'Something', + 'expectedResult' => true, + ] + ]; + } +} diff --git a/tests/Unit/Validation/ValidationRuleUnitCase.php b/tests/Unit/Validation/ValidationRuleUnitCase.php new file mode 100644 index 0000000..93d6d20 --- /dev/null +++ b/tests/Unit/Validation/ValidationRuleUnitCase.php @@ -0,0 +1,59 @@ + $validationRuleClassString + * @param ?array $makeParams + * @param ?class-string $expectedMakeException + * @param mixed $valueToBeEvaluated + * @param bool $expectedResult + * + * @throws Throwable + */ + #[Test] + #[DataProvider('data_provider')] + public function will_be_able_to_use_rule( + string $validationRuleClassString, + ?array $makeParams, + ?string $expectedMakeException, + mixed $valueToBeEvaluated, + bool $expectedResult + ): void { + if ($expectedMakeException) { + self::expectException($expectedMakeException); + $validationRuleClassString::make($makeParams); + + return; + } + + $rule = $validationRuleClassString::make($makeParams); + + self::assertEquals( + $expectedResult, + $rule->evaluate($valueToBeEvaluated) + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + abstract public static function data_provider(): array; +} From f841242470bfd619f5821f979a0659cf2585763a Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Thu, 9 Jan 2025 06:42:41 -0500 Subject: [PATCH 02/31] Refactor validation rule tests: rename RequireRuleTest to RequiredRuleTest and update imports --- tests/Unit/Validation/BackedEnumRuleTest.php | 1 + tests/Unit/Validation/NullableRuleTest.php | 2 ++ .../Validation/{RequireRuleTest.php => RequiredRuleTest.php} | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) rename tests/Unit/Validation/{RequireRuleTest.php => RequiredRuleTest.php} (92%) diff --git a/tests/Unit/Validation/BackedEnumRuleTest.php b/tests/Unit/Validation/BackedEnumRuleTest.php index f8f1dd5..6adf0f8 100644 --- a/tests/Unit/Validation/BackedEnumRuleTest.php +++ b/tests/Unit/Validation/BackedEnumRuleTest.php @@ -4,6 +4,7 @@ use Nuxtifyts\PhpDto\Validation\Rules\BackedEnumRule; use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Validation\Rules\ValidationRule; use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoBackedEnum; use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoEnum; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/tests/Unit/Validation/NullableRuleTest.php b/tests/Unit/Validation/NullableRuleTest.php index afc38aa..a28807a 100644 --- a/tests/Unit/Validation/NullableRuleTest.php +++ b/tests/Unit/Validation/NullableRuleTest.php @@ -3,6 +3,8 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Validation; use Nuxtifyts\PhpDto\Validation\Rules\NullableRule; +use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Validation\Rules\ValidationRule; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Throwable; diff --git a/tests/Unit/Validation/RequireRuleTest.php b/tests/Unit/Validation/RequiredRuleTest.php similarity index 92% rename from tests/Unit/Validation/RequireRuleTest.php rename to tests/Unit/Validation/RequiredRuleTest.php index 3c3414a..e9e1988 100644 --- a/tests/Unit/Validation/RequireRuleTest.php +++ b/tests/Unit/Validation/RequiredRuleTest.php @@ -3,12 +3,14 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Validation; use Nuxtifyts\PhpDto\Validation\Rules\RequiredRule; +use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Validation\Rules\ValidationRule; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Throwable; #[CoversClass(RequiredRule::class)] -final class RequireRuleTest extends ValidationRuleUnitCase +final class RequiredRuleTest extends ValidationRuleUnitCase { /** * @throws Throwable From e08f32f4c7e58cb4bd368f98213481b224b14dc0 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Thu, 9 Jan 2025 06:51:44 -0500 Subject: [PATCH 03/31] Add DateRule validation and corresponding tests; refactor validation test cases --- clover.xml | 18 ++++- src/Validation/Rules/DateRule.php | 30 ++++++++ tests/Unit/Validation/BackedEnumRuleTest.php | 2 +- tests/Unit/Validation/DateRuleTest.php | 69 +++++++++++++++++++ tests/Unit/Validation/NullableRuleTest.php | 2 +- tests/Unit/Validation/RequiredRuleTest.php | 2 +- ...nitCase.php => ValidationRuleTestCase.php} | 2 +- 7 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 src/Validation/Rules/DateRule.php create mode 100644 tests/Unit/Validation/DateRuleTest.php rename tests/Unit/Validation/{ValidationRuleUnitCase.php => ValidationRuleTestCase.php} (96%) diff --git a/clover.xml b/clover.xml index 8625f79..d572379 100644 --- a/clover.xml +++ b/clover.xml @@ -1,6 +1,6 @@ - - + + @@ -1727,6 +1727,18 @@ + + + + + + + + + + + + @@ -1763,6 +1775,6 @@ - + diff --git a/src/Validation/Rules/DateRule.php b/src/Validation/Rules/DateRule.php new file mode 100644 index 0000000..49ade6f --- /dev/null +++ b/src/Validation/Rules/DateRule.php @@ -0,0 +1,30 @@ + $parameters + */ + public static function make(?array $parameters = null): self + { + return new self(); + } + + public function validationMessage(): string + { + return 'The :attribute must be a valid date.'; + } +} diff --git a/tests/Unit/Validation/BackedEnumRuleTest.php b/tests/Unit/Validation/BackedEnumRuleTest.php index 6adf0f8..0257236 100644 --- a/tests/Unit/Validation/BackedEnumRuleTest.php +++ b/tests/Unit/Validation/BackedEnumRuleTest.php @@ -16,7 +16,7 @@ #[CoversClass(ValidationRuleException::class)] #[UsesClass(YesNoBackedEnum::class)] #[UsesClass(YesNoEnum::class)] -final class BackedEnumRuleTest extends ValidationRuleUnitCase +final class BackedEnumRuleTest extends ValidationRuleTestCase { /** * @throws Throwable diff --git a/tests/Unit/Validation/DateRuleTest.php b/tests/Unit/Validation/DateRuleTest.php new file mode 100644 index 0000000..78dccf5 --- /dev/null +++ b/tests/Unit/Validation/DateRuleTest.php @@ -0,0 +1,69 @@ +validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will evaluate false when value is not a valid datetime string' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'not-a-valid-datetime-string', + 'expectedResult' => false + ], + 'Will evaluate true when a valid datetime string is provided (Y-m-d)' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '2021-01-01', + 'expectedResult' => true + ], + 'Will evaluate true when a valid datetime string is provided (ATOM)' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '2021-01-01T00:00:00+00:00', + 'expectedResult' => true + ], + 'Will evaluate true when a valid datetime string is provided (Y-m-d H:m:s)' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '2021-01-01 00:00:00', + 'expectedResult' => true + ], + ]; + } +} diff --git a/tests/Unit/Validation/NullableRuleTest.php b/tests/Unit/Validation/NullableRuleTest.php index a28807a..95d2ad2 100644 --- a/tests/Unit/Validation/NullableRuleTest.php +++ b/tests/Unit/Validation/NullableRuleTest.php @@ -10,7 +10,7 @@ use Throwable; #[CoversClass(NullableRule::class)] -final class NullableRuleTest extends ValidationRuleUnitCase +final class NullableRuleTest extends ValidationRuleTestCase { /** * @throws Throwable diff --git a/tests/Unit/Validation/RequiredRuleTest.php b/tests/Unit/Validation/RequiredRuleTest.php index e9e1988..1e1f292 100644 --- a/tests/Unit/Validation/RequiredRuleTest.php +++ b/tests/Unit/Validation/RequiredRuleTest.php @@ -10,7 +10,7 @@ use Throwable; #[CoversClass(RequiredRule::class)] -final class RequiredRuleTest extends ValidationRuleUnitCase +final class RequiredRuleTest extends ValidationRuleTestCase { /** * @throws Throwable diff --git a/tests/Unit/Validation/ValidationRuleUnitCase.php b/tests/Unit/Validation/ValidationRuleTestCase.php similarity index 96% rename from tests/Unit/Validation/ValidationRuleUnitCase.php rename to tests/Unit/Validation/ValidationRuleTestCase.php index 93d6d20..5d56dda 100644 --- a/tests/Unit/Validation/ValidationRuleUnitCase.php +++ b/tests/Unit/Validation/ValidationRuleTestCase.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\Attributes\Test; use Throwable; -abstract class ValidationRuleUnitCase extends UnitCase +abstract class ValidationRuleTestCase extends UnitCase { abstract function validate_validation_message(): void; From b4242e241c87cda0baca89f409471de7742d31d2 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 15:29:53 -0500 Subject: [PATCH 04/31] Enhance DateRule to support custom date formats and improve validation logic; update tests accordingly --- src/Validation/Rules/DateRule.php | 39 +++++++++++++++++++++++--- tests/Unit/Validation/DateRuleTest.php | 34 ++++++++++++++++++---- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/Validation/Rules/DateRule.php b/src/Validation/Rules/DateRule.php index 49ade6f..35acbdc 100644 --- a/src/Validation/Rules/DateRule.php +++ b/src/Validation/Rules/DateRule.php @@ -2,8 +2,16 @@ namespace Nuxtifyts\PhpDto\Validation\Rules; +use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Support\Arr; + class DateRule implements ValidationRule { + /** + * @var list + */ + protected array $formats = []; + public string $name { get { return 'date'; @@ -12,19 +20,42 @@ class DateRule implements ValidationRule public function evaluate(mixed $value): bool { - return is_string($value) && strtotime($value) !== false; + return empty($this->formats) + ? is_string($value) && strtotime($value) !== false + : is_string($value) && array_any( + $this->formats, + static fn (string $format): bool => (bool) date_create_from_format($format, $value) + ); } /** - * @param ?array $parameters + * @param ?array $parameters + * + * @throws ValidationRuleException */ public static function make(?array $parameters = null): self { - return new self(); + $instance = new self(); + + $formats = Arr::getArray($parameters ?? [], 'formats', []); + + if (array_any( + $formats, + static fn (mixed $format): bool => !is_string($format) + )) { + throw ValidationRuleException::invalidParameters(); + } + + /** @var array $formats */ + $instance->formats = array_values($formats); + + return $instance; } public function validationMessage(): string { - return 'The :attribute must be a valid date.'; + return empty($this->formats) + ? 'The :attribute must be a valid date.' + : 'The :attribute must be a valid date in one of the following formats: ' . implode(', ', $this->formats); } } diff --git a/tests/Unit/Validation/DateRuleTest.php b/tests/Unit/Validation/DateRuleTest.php index 78dccf5..9ee310c 100644 --- a/tests/Unit/Validation/DateRuleTest.php +++ b/tests/Unit/Validation/DateRuleTest.php @@ -2,12 +2,15 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Validation; -use Nuxtifyts\PhpDto\Validation\Rules\DateRule; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; use Throwable; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\CoversClass; +use Nuxtifyts\PhpDto\Validation\Rules\DateRule; +use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Validation\Rules\ValidationRule; #[CoversClass(DateRule::class)] +#[CoversClass(ValidationRuleException::class)] final class DateRuleTest extends ValidationRuleTestCase { /** @@ -24,8 +27,8 @@ public function validate_validation_message(): void ); } - /** - * @return array, * makeParams: ?array, * expectedMakeException: ?class-string, @@ -64,6 +67,27 @@ public static function data_provider(): array 'valueToBeEvaluated' => '2021-01-01 00:00:00', 'expectedResult' => true ], + 'Will evaluate false when a custom datetime string is provided but no formats are set' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '2021/01-01 00/00/00', + 'expectedResult' => false + ], + 'Will evaluate true when a custom datetime string is provided and a format is set' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => ['formats' => ['Y/m-d H/m/s']], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => '2021/01-01 00/00/00', + 'expectedResult' => true + ], + 'Will throw an exception when an invalid non string format is passed' => [ + 'validationRuleClassString' => DateRule::class, + 'makeParams' => ['formats' => ['Y/m-d H/m/s', 123]], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => '2021/01-01 00/00/00', + 'expectedResult' => false + ], ]; } } From 7da3aefffaace42c1baac66d8686e6bfc612b85f Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 16:33:38 -0500 Subject: [PATCH 05/31] Remove ValidateableData trait as it is no longer needed in the validation framework --- src/Concerns/ValidateableData.php | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/Concerns/ValidateableData.php diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php deleted file mode 100644 index 0cd4760..0000000 --- a/src/Concerns/ValidateableData.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public static function validationRules(): array - { - return []; - } -} From 66c254416088c4608412b461241d1fbc026dd5e5 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 17:28:34 -0500 Subject: [PATCH 06/31] Add RegexRule validation with evaluation logic and corresponding tests --- src/Validation/Rules/RegexRule.php | 57 ++++++++++++++++++++ tests/Unit/Validation/RegexRuleTest.php | 72 +++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/Validation/Rules/RegexRule.php create mode 100644 tests/Unit/Validation/RegexRuleTest.php diff --git a/src/Validation/Rules/RegexRule.php b/src/Validation/Rules/RegexRule.php new file mode 100644 index 0000000..eda4c1e --- /dev/null +++ b/src/Validation/Rules/RegexRule.php @@ -0,0 +1,57 @@ +pattern, + subject: $value, + flags: $this->flags, + offset: $this->offset + ); + } + + /** + * @param ?array $parameters + * + * @throws ValidationRuleException + */ + public static function make(?array $parameters = null): self + { + $instance = new self(); + + $pattern = $parameters['pattern'] ?? null; + + if (!is_string($pattern) || @preg_match($pattern, '') === false) { + throw ValidationRuleException::invalidParameters(); + } + + $instance->pattern = $pattern; + + return $instance; + } + + public function validationMessage(): string + { + return 'The :attribute field does not match the required pattern.'; + } +} diff --git a/tests/Unit/Validation/RegexRuleTest.php b/tests/Unit/Validation/RegexRuleTest.php new file mode 100644 index 0000000..e13e045 --- /dev/null +++ b/tests/Unit/Validation/RegexRuleTest.php @@ -0,0 +1,72 @@ + '/^test$/']); + + self::assertEquals( + 'The :attribute field does not match the required pattern.', + $rule->validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will evaluate false when value is not a valid regex string' => [ + 'validationRuleClassString' => RegexRule::class, + 'makeParams' => ['pattern' => '/^test$/'], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'not-a-valid-regex-string', + 'expectedResult' => false + ], + 'Will evaluate true when a valid regex string is provided' => [ + 'validationRuleClassString' => RegexRule::class, + 'makeParams' => ['pattern' => '/^test$/'], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => true + ], + 'Will evaluate false when a non string value is provided' => [ + 'validationRuleClassString' => RegexRule::class, + 'makeParams' => ['pattern' => '/^test$/'], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123, + 'expectedResult' => false + ], + 'Will throw an exception if the pattern is not a valid regex' => [ + 'validationRuleClassString' => RegexRule::class, + 'makeParams' => ['pattern' => '/^test'], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + ]; + } +} From 44fbdb60f8c810f257594babea375be404bec171 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 17:45:38 -0500 Subject: [PATCH 07/31] Update .gitignore to include clover.xml and tests/.DS_Store --- .gitignore | 2 + clover.xml | 1780 ---------------------------------------------------- 2 files changed, 2 insertions(+), 1780 deletions(-) delete mode 100644 clover.xml diff --git a/.gitignore b/.gitignore index 783d717..3258f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ vendor/ .idea/ coverage.xml +clover.xml package.xml coverage-html/ .phpunit/ /.php-cs-fixer.cache +tests/.DS_Store diff --git a/clover.xml b/clover.xml deleted file mode 100644 index d572379..0000000 --- a/clover.xml +++ /dev/null @@ -1,1780 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5c8e7b592d1eaac1c43eb6a7974860eb9b70d024 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 18:59:41 -0500 Subject: [PATCH 08/31] Add utility methods to retrieve values from arrays with type checks and defaults --- src/Support/Arr.php | 73 ++++++++++++- tests/Unit/Support/ArrTest.php | 192 +++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 1 deletion(-) diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 0a83032..3a404b7 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -6,7 +6,6 @@ { /** * @param array $array - * @param string $key * @param array $default * * @return array @@ -18,6 +17,78 @@ public static function getArray(array $array, string $key, array $default = []): return is_array($value) ? $value : $default; } + /** + * @param array $array + */ + public static function getStringOrNull(array $array, string $key): ?string + { + $value = $array[$key] ?? null; + + return is_string($value) ? $value : null; + } + + /** + * @param array $array + */ + public static function getString(array $array, string $key, string $default = ''): string + { + return self::getStringOrNull($array, $key) ?? $default; + } + + /** + * @param array $array + */ + public static function getIntegerOrNull(array $array, string $key): ?int + { + $value = $array[$key] ?? null; + + return is_int($value) ? $value : null; + } + + /** + * @param array $array + */ + public static function getInteger(array $array, string $key, int $default = 0): int + { + return self::getIntegerOrNull($array, $key) ?? $default; + } + + /** + * @param array $array + */ + public static function getFloatOrNull(array $array, string $key): ?float + { + $value = $array[$key] ?? null; + + return is_float($value) ? $value : null; + } + + /** + * @param array $array + */ + public static function getFloat(array $array, string $key, float $default = 0.0): float + { + return self::getFloatOrNull($array, $key) ?? $default; + } + + /** + * @param array $array + */ + public static function getBooleanOrNull(array $array, string $key): ?bool + { + $value = $array[$key] ?? null; + + return is_bool($value) ? $value : null; + } + + /** + * @param array $array + */ + public static function getBoolean(array $array, string $key, bool $default = false): bool + { + return self::getBooleanOrNull($array, $key) ?? $default; + } + /** * @param array $array * @param class-string $classString diff --git a/tests/Unit/Support/ArrTest.php b/tests/Unit/Support/ArrTest.php index 633b923..b54f247 100644 --- a/tests/Unit/Support/ArrTest.php +++ b/tests/Unit/Support/ArrTest.php @@ -62,6 +62,198 @@ public static function get_arr_provider(): array ], [], ], + 'get string existing key, invalid value' => [ + 'getString', + [ + 'array' => ['key' => 1], + 'key' => 'key', + ], + '', + ], + 'get string existing key, valid value' => [ + 'getString', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + 'value', + ], + 'get string non-existing key' => [ + 'getString', + [ + 'array' => ['key' => 'value'], + 'key' => 'nonExistingKey', + ], + '', + ], + 'get string or null existing key, invalid value' => [ + 'getStringOrNull', + [ + 'array' => ['key' => 1], + 'key' => 'key', + ], + null, + ], + 'get string or null existing key, valid value' => [ + 'getStringOrNull', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + 'value', + ], + 'get string or null non-existing key' => [ + 'getStringOrNull', + [ + 'array' => ['key' => 'value'], + 'key' => 'nonExistingKey', + ], + null, + ], + 'get integer existing key, invalid value' => [ + 'getInteger', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + 0, + ], + 'get integer existing key, valid value' => [ + 'getInteger', + [ + 'array' => ['key' => 1], + 'key' => 'key', + ], + 1, + ], + 'get integer non-existing key' => [ + 'getInteger', + [ + 'array' => ['key' => 1], + 'key' => 'nonExistingKey', + ], + 0, + ], + 'get integer or null existing key, invalid value' => [ + 'getIntegerOrNull', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + null, + ], + 'get integer or null existing key, valid value' => [ + 'getIntegerOrNull', + [ + 'array' => ['key' => 1], + 'key' => 'key', + ], + 1, + ], + 'get integer or null non-existing key' => [ + 'getIntegerOrNull', + [ + 'array' => ['key' => 1], + 'key' => 'nonExistingKey', + ], + null, + ], + 'get float existing key, invalid value' => [ + 'getFloat', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + 0.0, + ], + 'get float existing key, valid value' => [ + 'getFloat', + [ + 'array' => ['key' => 1.1], + 'key' => 'key', + ], + 1.1, + ], + 'get float non-existing key' => [ + 'getFloat', + [ + 'array' => ['key' => 1.1], + 'key' => 'nonExistingKey', + ], + 0.0, + ], + 'get float or null existing key, invalid value' => [ + 'getFloatOrNull', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + null, + ], + 'get float or null existing key, valid value' => [ + 'getFloatOrNull', + [ + 'array' => ['key' => 1.1], + 'key' => 'key', + ], + 1.1, + ], + 'get float or null non-existing key' => [ + 'getFloatOrNull', + [ + 'array' => ['key' => 1.1], + 'key' => 'nonExistingKey', + ], + null, + ], + 'get boolean existing key, invalid value' => [ + 'getBoolean', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + false, + ], + 'get boolean existing key, valid value' => [ + 'getBoolean', + [ + 'array' => ['key' => true], + 'key' => 'key', + ], + true, + ], + 'get boolean non-existing key' => [ + 'getBoolean', + [ + 'array' => ['key' => true], + 'key' => 'nonExistingKey', + ], + false, + ], + 'get boolean or null existing key, invalid value' => [ + 'getBooleanOrNull', + [ + 'array' => ['key' => 'value'], + 'key' => 'key', + ], + null, + ], + 'get boolean or null existing key, valid value' => [ + 'getBooleanOrNull', + [ + 'array' => ['key' => true], + 'key' => 'key', + ], + true, + ], + 'get boolean or null non-existing key' => [ + 'getBooleanOrNull', + [ + 'array' => ['key' => true], + 'key' => 'nonExistingKey', + ], + null, + ], ]; } From 331b3a1c9afae5d2cd3af90dab89c58fdbdc375b Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 18:59:48 -0500 Subject: [PATCH 09/31] Refactor RegexRule properties to protected visibility and introduce StringRule for string validation with length constraints and corresponding tests --- src/Validation/Rules/RegexRule.php | 6 +- src/Validation/Rules/StringRule.php | 60 ++++++++++++ tests/Unit/Validation/StringRuleTest.php | 114 +++++++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/Validation/Rules/StringRule.php create mode 100644 tests/Unit/Validation/StringRuleTest.php diff --git a/src/Validation/Rules/RegexRule.php b/src/Validation/Rules/RegexRule.php index eda4c1e..29a2ccf 100644 --- a/src/Validation/Rules/RegexRule.php +++ b/src/Validation/Rules/RegexRule.php @@ -6,12 +6,12 @@ class RegexRule implements ValidationRule { - public string $pattern; + protected string $pattern; /** @var 0 | 256 | 512 | 768 */ - public int $flags = 0; + protected int $flags = 0; - public int $offset = 0; + protected int $offset = 0; public string $name { get { diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php new file mode 100644 index 0000000..7884d28 --- /dev/null +++ b/src/Validation/Rules/StringRule.php @@ -0,0 +1,60 @@ + $parameters + * + * @throws ValidationRuleException + */ + #[Override] + public static function make(?array $parameters = null): self + { + $instance = new self(); + + if ( + (!is_null($minLen = Arr::getIntegerOrNull($parameters ?? [], 'minLen')) + && $minLen < 0) + || (!is_null($maxLen = Arr::getIntegerOrNull($parameters ?? [], 'maxLen')) + && $maxLen < $minLen) + ) { + throw ValidationRuleException::invalidParameters(); + } + + $lengthPattern = ''; + if ($minLen !== null) { + $lengthPattern .= '.{' . $minLen . ','; + } else { + $lengthPattern .= '.{0,'; + } + + if ($maxLen !== null) { + $lengthPattern .= $maxLen . '}'; + } else { + $lengthPattern .= '}'; + } + + $instance->pattern = '/^' . $lengthPattern . '$/u'; + + return $instance; + } + + #[Override] + public function validationMessage(): string + { + return 'The :attribute field must be a valid string.'; + } +} diff --git a/tests/Unit/Validation/StringRuleTest.php b/tests/Unit/Validation/StringRuleTest.php new file mode 100644 index 0000000..01926ff --- /dev/null +++ b/tests/Unit/Validation/StringRuleTest.php @@ -0,0 +1,114 @@ +validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will evaluate false when value is not a string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123, + 'expectedResult' => false + ], + 'Will evaluate true when a string value is provided' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => true + ], + 'Will evaluate false when a string value is provided but minLen is greater than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => 5], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will evaluate true when a string value is provided and minLen is less than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => 3], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => true + ], + 'Will evaluate false when a string value is provided but maxLen is less than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['maxLen' => 3], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will evaluate true when a string value is provided and maxLen is greater than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['maxLen' => 5], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => true + ], + 'Will evaluate false when a string value is provided but minLen is greater than the length of the string and maxLen is less than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => 5, 'maxLen' => 10], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will evaluate true when a string value is provided and minLen is less than the length of the string and maxLen is greater than the length of the string' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => 3, 'maxLen' => 5], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => true + ], + 'Will throw an exception if minLen is less than 0' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => -1], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will throw an exception if maxLen is less than minLen' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['minLen' => 5, 'maxLen' => 3], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + ]; + } +} From 638f3fc73f0b44533fae6140da46bc8d916f7832 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 19:23:14 -0500 Subject: [PATCH 10/31] Introduce MinMaxValues trait for centralized min/max validation and integrate it into StringRule --- .../Rules/Concerns/MinMaxValues.php | 32 +++++++++++++++++++ src/Validation/Rules/StringRule.php | 25 +++------------ 2 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 src/Validation/Rules/Concerns/MinMaxValues.php diff --git a/src/Validation/Rules/Concerns/MinMaxValues.php b/src/Validation/Rules/Concerns/MinMaxValues.php new file mode 100644 index 0000000..5ad6642 --- /dev/null +++ b/src/Validation/Rules/Concerns/MinMaxValues.php @@ -0,0 +1,32 @@ + $parameters + * @return array{ 0: int|null, 1: int|null } + * + * @throws ValidationRuleException + */ + protected static function getMinMaxValues( + ?array $parameters = null, + string $minKey = 'min', + string $maxKey = 'max' + ): array { + if ( + (!is_null($min = Arr::getIntegerOrNull($parameters ?? [], $minKey)) + && $min < 0) + || (!is_null($max = Arr::getIntegerOrNull($parameters ?? [], $maxKey)) + && $max < $min) + ) { + throw ValidationRuleException::invalidParameters(); + } + + return [$min, $max]; + } +} diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index 7884d28..2fd695d 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -9,6 +9,8 @@ class StringRule extends RegexRule { + use Concerns\MinMaxValues; + public string $name { get { return 'string'; @@ -25,27 +27,10 @@ public static function make(?array $parameters = null): self { $instance = new self(); - if ( - (!is_null($minLen = Arr::getIntegerOrNull($parameters ?? [], 'minLen')) - && $minLen < 0) - || (!is_null($maxLen = Arr::getIntegerOrNull($parameters ?? [], 'maxLen')) - && $maxLen < $minLen) - ) { - throw ValidationRuleException::invalidParameters(); - } + [$minLen, $maxLen] = self::getMinMaxValues($parameters, 'minLen', 'maxLen'); - $lengthPattern = ''; - if ($minLen !== null) { - $lengthPattern .= '.{' . $minLen . ','; - } else { - $lengthPattern .= '.{0,'; - } - - if ($maxLen !== null) { - $lengthPattern .= $maxLen . '}'; - } else { - $lengthPattern .= '}'; - } + $lengthPattern = is_null($minLen) ? '.{0,' : '.{' . $minLen . ','; + $lengthPattern .= is_null($maxLen) ? '}' : $maxLen . '}'; $instance->pattern = '/^' . $lengthPattern . '$/u'; From 33a251d1f5fbf49814fb7e27ce52912b9f5350be Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 19:50:17 -0500 Subject: [PATCH 11/31] Add getBackedEnumOrNull method to Arr class for enum retrieval with validation --- src/Support/Arr.php | 37 ++++++++++++++++++++ tests/Unit/Support/ArrTest.php | 64 +++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 3a404b7..75f2788 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -2,6 +2,9 @@ namespace Nuxtifyts\PhpDto\Support; +use BackedEnum; +use InvalidArgumentException; + final readonly class Arr { /** @@ -89,6 +92,40 @@ public static function getBoolean(array $array, string $key, bool $default = fal return self::getBooleanOrNull($array, $key) ?? $default; } + /** + * @template T of BackedEnum + * + * @param array $array + * @param class-string $enumClass + * @param ?T $default + * + * @return ?T + */ + public static function getBackedEnumOrNull( + array $array, + string $key, + string $enumClass, + ?BackedEnum $default = null + ): ?BackedEnum { + $value = $array[$key] ?? null; + + if ($value instanceof $enumClass) { + return $value; + } else if ( + (is_string($value) || is_integer($value)) + && $resolvedValue = $enumClass::tryFrom($value) + ) { + return $resolvedValue; + } + + return is_null($default) + ? null + : ($default instanceof $enumClass + ? $default + : throw new InvalidArgumentException('Default value must be an instance of ' . $enumClass) + ); + } + /** * @param array $array * @param class-string $classString diff --git a/tests/Unit/Support/ArrTest.php b/tests/Unit/Support/ArrTest.php index b54f247..ce0de10 100644 --- a/tests/Unit/Support/ArrTest.php +++ b/tests/Unit/Support/ArrTest.php @@ -2,16 +2,22 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Support; -use Nuxtifyts\PhpDto\Serializers\BackedEnumSerializer; -use Nuxtifyts\PhpDto\Serializers\ScalarTypeSerializer; -use Nuxtifyts\PhpDto\Serializers\Serializer; +use InvalidArgumentException; use Nuxtifyts\PhpDto\Support\Arr; +use PHPUnit\Framework\Attributes\Test; use Nuxtifyts\PhpDto\Tests\Unit\UnitCase; +use PHPUnit\Framework\Attributes\UsesClass; +use Nuxtifyts\PhpDto\Serializers\Serializer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Test; +use Nuxtifyts\PhpDto\Serializers\BackedEnumSerializer; +use Nuxtifyts\PhpDto\Serializers\ScalarTypeSerializer; +use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoBackedEnum; +use Nuxtifyts\PhpDto\Tests\Dummies\Enums\ColorsBackedEnum; #[CoversClass(Arr::class)] +#[UsesClass(YesNoBackedEnum::class)] +#[UsesClass(ColorsBackedEnum::class)] final class ArrTest extends UnitCase { /** @@ -254,9 +260,59 @@ public static function get_arr_provider(): array ], null, ], + 'get backed enum or null, invalid value' => [ + 'getBackedEnumOrNull', + [ + 'array' => ['key' => 'invalid'], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + ], + null + ], + 'get backed enum or null, invalid value default provided' => [ + 'getBackedEnumOrNull', + [ + 'array' => ['key' => 'invalid'], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + 'default' => YesNoBackedEnum::NO, + ], + YesNoBackedEnum::NO + ], + 'get backed enum or null, valid backed enum value' => [ + 'getBackedEnumOrNull', + [ + 'array' => ['key' => YesNoBackedEnum::YES], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + ], + YesNoBackedEnum::YES + ], + 'get backed enum or null, valid string value' => [ + 'getBackedEnumOrNull', + [ + 'array' => ['key' => 'yes'], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + ], + YesNoBackedEnum::YES + ] ]; } + #[Test] + public function get_backed_enum_or_null_will_throw_an_exception_if_default_value_is_invalid(): void + { + self::expectException(InvalidArgumentException::class); + + Arr::getBackedEnumOrNull( + ['key' => 'invalid'], + 'key', + YesNoBackedEnum::class, + ColorsBackedEnum::RED + ); + } + /** * @return array */ From 4e7d63787c5ca6e245ead837de471993712ec2ba Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 19:52:18 -0500 Subject: [PATCH 12/31] Add getBackedEnum method to Arr class for enum retrieval with default fallback --- src/Support/Arr.php | 18 ++++++++++++++++++ tests/Unit/Support/ArrTest.php | 32 +++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 75f2788..cf9547d 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -126,6 +126,24 @@ public static function getBackedEnumOrNull( ); } + /** + * @template T of BackedEnum + * + * @param array $array + * @param class-string $enumClass + * @param T $default + * + * @return T + */ + public static function getBackedEnum( + array $array, + string $key, + string $enumClass, + BackedEnum $default + ): BackedEnum { + return self::getBackedEnumOrNull($array, $key, $enumClass, $default) ?? $default; + } + /** * @param array $array * @param class-string $classString diff --git a/tests/Unit/Support/ArrTest.php b/tests/Unit/Support/ArrTest.php index ce0de10..587fb4c 100644 --- a/tests/Unit/Support/ArrTest.php +++ b/tests/Unit/Support/ArrTest.php @@ -296,7 +296,37 @@ public static function get_arr_provider(): array 'enumClass' => YesNoBackedEnum::class, ], YesNoBackedEnum::YES - ] + ], + 'get backed enum, invalid value' => [ + 'getBackedEnum', + [ + 'array' => ['key' => 'invalid'], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + 'default' => YesNoBackedEnum::NO, + ], + YesNoBackedEnum::NO + ], + 'get backed enum, valid backed enum value' => [ + 'getBackedEnum', + [ + 'array' => ['key' => YesNoBackedEnum::YES], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + 'default' => YesNoBackedEnum::NO, + ], + YesNoBackedEnum::YES + ], + 'get backed enum, valid string value' => [ + 'getBackedEnum', + [ + 'array' => ['key' => 'yes'], + 'key' => 'key', + 'enumClass' => YesNoBackedEnum::class, + 'default' => YesNoBackedEnum::NO, + ], + YesNoBackedEnum::YES + ], ]; } From 8d33d3bd453c8e439b2d70e110cef827a4649249 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 10 Jan 2025 20:49:07 -0500 Subject: [PATCH 13/31] Add NumericRule class for numeric validation with min/max constraints and corresponding tests --- src/Enums/Property/Type.php | 6 + .../Rules/Concerns/MinMaxValues.php | 33 ++++-- src/Validation/Rules/NumericRule.php | 79 +++++++++++++ tests/Unit/Validation/NumericRuleTest.php | 105 ++++++++++++++++++ 4 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 src/Validation/Rules/NumericRule.php create mode 100644 tests/Unit/Validation/NumericRuleTest.php diff --git a/src/Enums/Property/Type.php b/src/Enums/Property/Type.php index 3953d28..ff39a8a 100644 --- a/src/Enums/Property/Type.php +++ b/src/Enums/Property/Type.php @@ -20,4 +20,10 @@ enum Type: string self::BOOLEAN, self::STRING, ]; + + /** @var list */ + public const array NUMERIC_TYPES = [ + self::FLOAT, + self::INT, + ]; } diff --git a/src/Validation/Rules/Concerns/MinMaxValues.php b/src/Validation/Rules/Concerns/MinMaxValues.php index 5ad6642..effcc5f 100644 --- a/src/Validation/Rules/Concerns/MinMaxValues.php +++ b/src/Validation/Rules/Concerns/MinMaxValues.php @@ -4,25 +4,42 @@ use Nuxtifyts\PhpDto\Support\Arr; use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; +use Nuxtifyts\PhpDto\Enums\Property\Type; trait MinMaxValues { /** - * @param ?array $parameters - * @return array{ 0: int|null, 1: int|null } + * @param ?array $parameters + * @param Type::INT | Type::FLOAT $type * - * @throws ValidationRuleException + * @return array{ 0: int|null|float, 1: int|null|float } + * + * @throws ValidationRuleException */ protected static function getMinMaxValues( ?array $parameters = null, string $minKey = 'min', - string $maxKey = 'max' + string $maxKey = 'max', + Type $type = Type::INT ): array { + $arrFunc = match ($type) { + Type::INT => 'getIntegerOrNull', + Type::FLOAT => 'getFloatOrNull', + }; + + $min = match ($type) { + Type::INT => Arr::getIntegerOrNull($parameters ?? [], $minKey), + Type::FLOAT => Arr::getFloatOrNull($parameters ?? [], $minKey), + }; + + $max = match ($type) { + Type::INT => Arr::getIntegerOrNull($parameters ?? [], $maxKey), + Type::FLOAT => Arr::getFloatOrNull($parameters ?? [], $maxKey), + }; + if ( - (!is_null($min = Arr::getIntegerOrNull($parameters ?? [], $minKey)) - && $min < 0) - || (!is_null($max = Arr::getIntegerOrNull($parameters ?? [], $maxKey)) - && $max < $min) + (!is_null($min) && $min < 0) + || (!is_null($max) && ($max <= 0 || $min > $max)) ) { throw ValidationRuleException::invalidParameters(); } diff --git a/src/Validation/Rules/NumericRule.php b/src/Validation/Rules/NumericRule.php new file mode 100644 index 0000000..07f42d3 --- /dev/null +++ b/src/Validation/Rules/NumericRule.php @@ -0,0 +1,79 @@ +type === Type::INT && !is_int($value)) + || ($this->type === Type::FLOAT && !is_float($value)) + || (!is_null($this->min) && $value < $this->min) + || (!is_null($this->max) && $value > $this->max) + ) { + return false; + } + + return true; + } + + /** + * @param ?array $parameters + * + * @throws ValidationRuleException + */ + public static function make(?array $parameters = null): self + { + $instance = new self(); + + $numericType = Arr::getBackedEnum( + $parameters ?? [], + 'type', + Type::class, + Type::INT + ); + + if (!in_array( + $numericType->value, + array_column(Type::NUMERIC_TYPES, 'value')) + ) { + throw ValidationRuleException::invalidParameters(); + } + + /** @var Type::INT | Type::FLOAT $numericType */ + $instance->type = $numericType; + + [$instance->min, $instance->max] = self::getMinMaxValues($parameters, type: $numericType); + + return $instance; + } + + public function validationMessage(): string + { + return match($this->type) { + Type::INT => 'The :attribute field must be a valid integer.', + Type::FLOAT => 'The :attribute field must be a valid float.', + }; + } +} diff --git a/tests/Unit/Validation/NumericRuleTest.php b/tests/Unit/Validation/NumericRuleTest.php new file mode 100644 index 0000000..93f33dd --- /dev/null +++ b/tests/Unit/Validation/NumericRuleTest.php @@ -0,0 +1,105 @@ +validationMessage() + ); + + self::assertEquals( + 'The :attribute field must be a valid float.', + NumericRule::make(['type' => Type::FLOAT])->validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will evaluate false when value is not a number' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will evaluate true when an integer value is provided' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::INT], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123, + 'expectedResult' => true + ], + 'Will evaluate false when an integer value is provided but min is greater than the value' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::INT, 'min' => 124], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123, + 'expectedResult' => false + ], + 'Will evaluate false when an integer value is provided but max is less than the value' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::INT, 'max' => 122], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123, + 'expectedResult' => false + ], + 'Will evaluate true when a float value is provided' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::FLOAT], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123.45, + 'expectedResult' => true + ], + 'Will evaluate false when a float value is provided but min is greater than the value' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::FLOAT, 'min' => 123.46], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123.45, + 'expectedResult' => false + ], + 'Will evaluate false when a float value is provided but max is less than the value' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::FLOAT, 'max' => 123.44], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 123.45, + 'expectedResult' => false + ], + 'Will throw an exception if an invalid type is provided' => [ + 'validationRuleClassString' => NumericRule::class, + 'makeParams' => ['type' => Type::BOOLEAN], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => 123, + 'expectedResult' => false + ], + ]; + } +} From 00ae95bf362a01d19830be0fb887828c49233c5f Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sat, 11 Jan 2025 12:07:34 -0500 Subject: [PATCH 14/31] Enhance StringRule to support alpha numeric validation and improve RegexRule to accept numeric values --- src/Validation/Rules/RegexRule.php | 4 ++-- src/Validation/Rules/StringRule.php | 25 ++++++++++++++++++++++-- tests/Unit/Validation/StringRuleTest.php | 16 ++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Validation/Rules/RegexRule.php b/src/Validation/Rules/RegexRule.php index 29a2ccf..c3038c8 100644 --- a/src/Validation/Rules/RegexRule.php +++ b/src/Validation/Rules/RegexRule.php @@ -21,10 +21,10 @@ class RegexRule implements ValidationRule public function evaluate(mixed $value): bool { - return is_string($value) + return (is_string($value) || is_numeric($value)) && preg_match( pattern: $this->pattern, - subject: $value, + subject: (string) $value, flags: $this->flags, offset: $this->offset ); diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index 2fd695d..43dac17 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -11,6 +11,15 @@ class StringRule extends RegexRule { use Concerns\MinMaxValues; + protected const TYPE_STRING = 'string'; + protected const TYPE_ALPHA = 'alpha'; + + /** @var list */ + protected const TYPES = [ + self::TYPE_STRING, + self::TYPE_ALPHA, + ]; + public string $name { get { return 'string'; @@ -27,12 +36,24 @@ public static function make(?array $parameters = null): self { $instance = new self(); + $strType = Arr::getString($parameters ?? [], 'type', self::TYPE_STRING); + + if (!in_array($strType, self::TYPES)) { + throw ValidationRuleException::invalidParameters(); + } + + /** @var 'string' | 'alpha' $strType */ + $strPattern = match ($strType) { + self::TYPE_STRING => '[a-zA-Z]', + self::TYPE_ALPHA => '[a-zA-Z0-9]' + }; + [$minLen, $maxLen] = self::getMinMaxValues($parameters, 'minLen', 'maxLen'); - $lengthPattern = is_null($minLen) ? '.{0,' : '.{' . $minLen . ','; + $lengthPattern = is_null($minLen) ? '{0,' : '{' . $minLen . ','; $lengthPattern .= is_null($maxLen) ? '}' : $maxLen . '}'; - $instance->pattern = '/^' . $lengthPattern . '$/u'; + $instance->pattern = '/^' . $strPattern . $lengthPattern . '$/'; return $instance; } diff --git a/tests/Unit/Validation/StringRuleTest.php b/tests/Unit/Validation/StringRuleTest.php index 01926ff..6c46103 100644 --- a/tests/Unit/Validation/StringRuleTest.php +++ b/tests/Unit/Validation/StringRuleTest.php @@ -43,7 +43,7 @@ public static function data_provider(): array 'validationRuleClassString' => StringRule::class, 'makeParams' => null, 'expectedMakeException' => null, - 'valueToBeEvaluated' => 123, + 'valueToBeEvaluated' => 'test1234', 'expectedResult' => false ], 'Will evaluate true when a string value is provided' => [ @@ -109,6 +109,20 @@ public static function data_provider(): array 'valueToBeEvaluated' => 'test', 'expectedResult' => false ], + 'Will evaluate alpha numeric strings as true when a param is passed' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['type' => 'alpha'], + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test123', + 'expectedResult' => true + ], + 'Will throw a validation exception when an invalid type is passed' => [ + 'validationRuleClassString' => StringRule::class, + 'makeParams' => ['type' => 'invalid'], + 'expectedMakeException' => ValidationRuleException::class, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], ]; } } From 8da677c7b01026ff27506345d998edd699dddde5 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sat, 11 Jan 2025 12:59:57 -0500 Subject: [PATCH 15/31] Use `string` type for class constants --- src/Validation/Rules/StringRule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index 43dac17..fd2e727 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -11,8 +11,8 @@ class StringRule extends RegexRule { use Concerns\MinMaxValues; - protected const TYPE_STRING = 'string'; - protected const TYPE_ALPHA = 'alpha'; + protected const string TYPE_STRING = 'string'; + protected const string TYPE_ALPHA = 'alpha'; /** @var list */ protected const TYPES = [ From 6a2d0acf6066e7691508791efc9ee0da61afc064 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sat, 11 Jan 2025 13:10:47 -0500 Subject: [PATCH 16/31] Refactor test structure by organizing Validation rules Moved unit tests for validation rules into a dedicated "Rules" subdirectory, updating namespaces accordingly. This improves organization and readability of test files, making it clearer which rules are being tested. --- .../Validation/{ => Rules}/BackedEnumRuleTest.php | 6 +++--- .../Unit/Validation/{ => Rules}/DateRuleTest.php | 10 +++++----- .../Validation/{ => Rules}/NullableRuleTest.php | 4 ++-- .../Validation/{ => Rules}/NumericRuleTest.php | 8 ++++---- .../Unit/Validation/{ => Rules}/RegexRuleTest.php | 10 +++++----- .../Validation/{ => Rules}/RequiredRuleTest.php | 4 ++-- .../Validation/{ => Rules}/StringRuleTest.php | 15 ++++++--------- .../{ => Rules}/ValidationRuleTestCase.php | 4 ++-- 8 files changed, 29 insertions(+), 32 deletions(-) rename tests/Unit/Validation/{ => Rules}/BackedEnumRuleTest.php (98%) rename tests/Unit/Validation/{ => Rules}/DateRuleTest.php (98%) rename tests/Unit/Validation/{ => Rules}/NullableRuleTest.php (96%) rename tests/Unit/Validation/{ => Rules}/NumericRuleTest.php (98%) rename tests/Unit/Validation/{ => Rules}/RegexRuleTest.php (97%) rename tests/Unit/Validation/{ => Rules}/RequiredRuleTest.php (97%) rename tests/Unit/Validation/{ => Rules}/StringRuleTest.php (96%) rename tests/Unit/Validation/{ => Rules}/ValidationRuleTestCase.php (96%) diff --git a/tests/Unit/Validation/BackedEnumRuleTest.php b/tests/Unit/Validation/Rules/BackedEnumRuleTest.php similarity index 98% rename from tests/Unit/Validation/BackedEnumRuleTest.php rename to tests/Unit/Validation/Rules/BackedEnumRuleTest.php index 0257236..ee313b7 100644 --- a/tests/Unit/Validation/BackedEnumRuleTest.php +++ b/tests/Unit/Validation/Rules/BackedEnumRuleTest.php @@ -1,12 +1,12 @@ validationMessage() + StringRule::make()->validationMessage() ); } diff --git a/tests/Unit/Validation/ValidationRuleTestCase.php b/tests/Unit/Validation/Rules/ValidationRuleTestCase.php similarity index 96% rename from tests/Unit/Validation/ValidationRuleTestCase.php rename to tests/Unit/Validation/Rules/ValidationRuleTestCase.php index 5d56dda..c207995 100644 --- a/tests/Unit/Validation/ValidationRuleTestCase.php +++ b/tests/Unit/Validation/Rules/ValidationRuleTestCase.php @@ -1,10 +1,10 @@ Date: Sat, 11 Jan 2025 13:14:33 -0500 Subject: [PATCH 17/31] Add EmailRule with validation and tests, update DateRule Introduce EmailRule for validating email addresses using regex, along with comprehensive unit tests. Update DateRule's validation messages for consistency and adjust corresponding test assertions. --- src/Validation/Rules/DateRule.php | 4 +- src/Validation/Rules/EmailRule.php | 34 ++++++++++ tests/Unit/Validation/Rules/DateRuleTest.php | 9 ++- tests/Unit/Validation/Rules/EmailRuleTest.php | 62 +++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/Validation/Rules/EmailRule.php create mode 100644 tests/Unit/Validation/Rules/EmailRuleTest.php diff --git a/src/Validation/Rules/DateRule.php b/src/Validation/Rules/DateRule.php index 35acbdc..4897457 100644 --- a/src/Validation/Rules/DateRule.php +++ b/src/Validation/Rules/DateRule.php @@ -55,7 +55,7 @@ public static function make(?array $parameters = null): self public function validationMessage(): string { return empty($this->formats) - ? 'The :attribute must be a valid date.' - : 'The :attribute must be a valid date in one of the following formats: ' . implode(', ', $this->formats); + ? 'The :attribute field must be a valid date.' + : 'The :attribute field must be a valid date in one of the following formats: ' . implode(', ', $this->formats); } } diff --git a/src/Validation/Rules/EmailRule.php b/src/Validation/Rules/EmailRule.php new file mode 100644 index 0000000..d9c811f --- /dev/null +++ b/src/Validation/Rules/EmailRule.php @@ -0,0 +1,34 @@ + $parameters + */ + #[Override] + public static function make(?array $parameters = null): self + { + $instance = new self(); + + $instance->pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; + + return $instance; + } + + #[Override] + public function validationMessage(): string + { + return 'The :attribute field must be a valid email address.'; + } +} diff --git a/tests/Unit/Validation/Rules/DateRuleTest.php b/tests/Unit/Validation/Rules/DateRuleTest.php index 64f34a9..e49f103 100644 --- a/tests/Unit/Validation/Rules/DateRuleTest.php +++ b/tests/Unit/Validation/Rules/DateRuleTest.php @@ -19,11 +19,14 @@ final class DateRuleTest extends ValidationRuleTestCase #[Test] public function validate_validation_message(): void { - $rule = DateRule::make(); + self::assertEquals( + 'The :attribute field must be a valid date.', + DateRule::make()->validationMessage() + ); self::assertEquals( - 'The :attribute must be a valid date.', - $rule->validationMessage() + 'The :attribute field must be a valid date in one of the following formats: Y/m-d H/m/s', + DateRule::make(['formats' => ['Y/m-d H/m/s']])->validationMessage() ); } diff --git a/tests/Unit/Validation/Rules/EmailRuleTest.php b/tests/Unit/Validation/Rules/EmailRuleTest.php new file mode 100644 index 0000000..839b926 --- /dev/null +++ b/tests/Unit/Validation/Rules/EmailRuleTest.php @@ -0,0 +1,62 @@ +validationMessage() + ); + } + + /** + * @return array, + * makeParams: ?array, + * expectedMakeException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool + * }> + */ + public static function data_provider(): array + { + return [ + 'Will evaluate false when value is not a string' => [ + 'validationRuleClassString' => EmailRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test1234', + 'expectedResult' => false + ], + 'Will evaluate false when a string value is provided but it is not a valid email address' => [ + 'validationRuleClassString' => EmailRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'test', + 'expectedResult' => false + ], + 'Will evaluate true when a valid email address is provided' => [ + 'validationRuleClassString' => EmailRule::class, + 'makeParams' => null, + 'expectedMakeException' => null, + 'valueToBeEvaluated' => 'johndoe@example.com', + 'expectedResult' => true, + ], + ]; + } +} From 350f7b72c3ee115ca2932ed84a2f7e6c43079776 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 11:24:54 -0500 Subject: [PATCH 18/31] Added Logical rules Added SingularRule, AndRule, OrRule --- src/Exceptions/LogicalRuleException.php | 15 ++++ src/Support/Collection.php | 82 ++++++++++++++++++++++ src/Validation/Contracts/RuleEvaluator.php | 8 +++ src/Validation/Contracts/RuleGroup.php | 17 +++++ src/Validation/Logic/AndRule.php | 15 ++++ src/Validation/Logic/LogicalRule.php | 26 +++++++ src/Validation/Logic/OrRule.php | 15 ++++ src/Validation/Logic/SingularRule.php | 29 ++++++++ src/Validation/RuleContext.php | 8 +++ src/Validation/Rules/ValidationRule.php | 5 +- 10 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/Exceptions/LogicalRuleException.php create mode 100644 src/Support/Collection.php create mode 100644 src/Validation/Contracts/RuleEvaluator.php create mode 100644 src/Validation/Contracts/RuleGroup.php create mode 100644 src/Validation/Logic/AndRule.php create mode 100644 src/Validation/Logic/LogicalRule.php create mode 100644 src/Validation/Logic/OrRule.php create mode 100644 src/Validation/Logic/SingularRule.php create mode 100644 src/Validation/RuleContext.php diff --git a/src/Exceptions/LogicalRuleException.php b/src/Exceptions/LogicalRuleException.php new file mode 100644 index 0000000..8f9be7b --- /dev/null +++ b/src/Exceptions/LogicalRuleException.php @@ -0,0 +1,15 @@ + */ + protected array $items = []; + + /** + * @param array $items + */ + public function __construct(array $items = []) + { + $this->items = $items; + } + + /** + * @param TValue $item + * + * @return self + */ + public function push(mixed $item): self + { + $this->items[] = $item; + return $this; + } + + /** + * @param TKey $key + * @param TValue $value + * + * @return self + */ + public function put(mixed $key, mixed $value): self + { + $this->items[$key] = $value; + return $this; + } + + /** + * @param ?callable(TValue $item): bool $callable + * + * @return ?TValue + */ + public function first(?callable $callable = null): mixed + { + return is_null($callable) + ? reset($this->items) ?: null + : array_find($this->items, $callable); + } + + public function isNotEmpty(): bool + { + return !empty($this->items); + } + + public function isEmpty(): bool + { + return !$this->isNotEmpty(); + } + + /** + * @param callable(TValue $item): bool $callable + */ + public function every(callable $callable): bool + { + return array_all($this->items, $callable); + } + + /** + * @param callable(TValue $item): bool $callable + */ + public function some(callable $callable): bool + { + return array_any($this->items, $callable); + } +} diff --git a/src/Validation/Contracts/RuleEvaluator.php b/src/Validation/Contracts/RuleEvaluator.php new file mode 100644 index 0000000..df678b6 --- /dev/null +++ b/src/Validation/Contracts/RuleEvaluator.php @@ -0,0 +1,8 @@ + */ + public Collection $rules { get; } + + /** + * @throws ValidationRuleException + */ + public function addRule(RuleEvaluator $rule): static; +} diff --git a/src/Validation/Logic/AndRule.php b/src/Validation/Logic/AndRule.php new file mode 100644 index 0000000..bebd43e --- /dev/null +++ b/src/Validation/Logic/AndRule.php @@ -0,0 +1,15 @@ +rules->every( + static fn (RuleEvaluator $rule) => $rule->evaluate($value) + ); + } +} diff --git a/src/Validation/Logic/LogicalRule.php b/src/Validation/Logic/LogicalRule.php new file mode 100644 index 0000000..9aac457 --- /dev/null +++ b/src/Validation/Logic/LogicalRule.php @@ -0,0 +1,26 @@ + */ + protected ?Collection $_rules = null; + + /** @var Collection */ + public Collection $rules { + get { + return $this->_rules ??= new Collection(); + } + } + + public function addRule(RuleEvaluator $rule): static + { + $this->rules->push($rule); + return $this; + } +} diff --git a/src/Validation/Logic/OrRule.php b/src/Validation/Logic/OrRule.php new file mode 100644 index 0000000..7babbb4 --- /dev/null +++ b/src/Validation/Logic/OrRule.php @@ -0,0 +1,15 @@ +rules->some( + static fn (RuleEvaluator $rule) => $rule->evaluate($value) + ); + } +} diff --git a/src/Validation/Logic/SingularRule.php b/src/Validation/Logic/SingularRule.php new file mode 100644 index 0000000..d9ded31 --- /dev/null +++ b/src/Validation/Logic/SingularRule.php @@ -0,0 +1,29 @@ +rules->isNotEmpty()) { + throw LogicalRuleException::unableToCreateRule('SingularRule can only have one rule'); + } + + $this->rules->push($rule); + return $this; + } + + public function evaluate(mixed $value): bool + { + return (bool) $this->rules->first()?->evaluate($value); + } +} diff --git a/src/Validation/RuleContext.php b/src/Validation/RuleContext.php new file mode 100644 index 0000000..79cee09 --- /dev/null +++ b/src/Validation/RuleContext.php @@ -0,0 +1,8 @@ + $parameters * From ccd10064123c349dafefb19a13d725dea7e59e09 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 12:40:25 -0500 Subject: [PATCH 19/31] Adjusting LogicalRule to create a new collection when constructed --- src/Validation/Logic/LogicalRule.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Validation/Logic/LogicalRule.php b/src/Validation/Logic/LogicalRule.php index 9aac457..6475638 100644 --- a/src/Validation/Logic/LogicalRule.php +++ b/src/Validation/Logic/LogicalRule.php @@ -8,16 +8,21 @@ abstract class LogicalRule implements RuleGroup, RuleEvaluator { - /** @var ?Collection */ - protected ?Collection $_rules = null; + /** @var Collection */ + protected Collection $_rules; /** @var Collection */ public Collection $rules { get { - return $this->_rules ??= new Collection(); + return $this->_rules; } } + public function __construct() + { + $this->_rules = new Collection(); + } + public function addRule(RuleEvaluator $rule): static { $this->rules->push($rule); From b00916dd40fe423dc0b31e1ca5025118269d1046 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 15:35:36 -0500 Subject: [PATCH 20/31] Adjusted logical rules Added validation messages tree --- src/Support/Collection.php | 19 +++++++++++++++++++ src/Validation/Logic/AndRule.php | 9 +++++++++ src/Validation/Logic/LogicalRule.php | 20 ++++++++++++++++++++ src/Validation/Logic/OrRule.php | 9 +++++++++ src/Validation/Logic/SingularRule.php | 7 +++++++ 5 files changed, 64 insertions(+) diff --git a/src/Support/Collection.php b/src/Support/Collection.php index e44337f..1bb8020 100644 --- a/src/Support/Collection.php +++ b/src/Support/Collection.php @@ -54,6 +54,17 @@ public function first(?callable $callable = null): mixed : array_find($this->items, $callable); } + /** + * @template TNewValue of mixed + * @param callable(TValue $item): TNewValue $callable + * + * @return self + */ + public function map(callable $callable): self + { + return new self(array_map($callable, $this->items)); + } + public function isNotEmpty(): bool { return !empty($this->items); @@ -79,4 +90,12 @@ public function some(callable $callable): bool { return array_any($this->items, $callable); } + + /** + * @return array + */ + public function all(): array + { + return $this->items; + } } diff --git a/src/Validation/Logic/AndRule.php b/src/Validation/Logic/AndRule.php index bebd43e..b672213 100644 --- a/src/Validation/Logic/AndRule.php +++ b/src/Validation/Logic/AndRule.php @@ -12,4 +12,13 @@ public function evaluate(mixed $value): bool static fn (RuleEvaluator $rule) => $rule->evaluate($value) ); } + + public function validationMessages(): array + { + return [ + 'and' => $this->rules + ->map(self::resolveValidationMessages(...)) + ->all() + ]; + } } diff --git a/src/Validation/Logic/LogicalRule.php b/src/Validation/Logic/LogicalRule.php index 6475638..310cbeb 100644 --- a/src/Validation/Logic/LogicalRule.php +++ b/src/Validation/Logic/LogicalRule.php @@ -5,6 +5,7 @@ use Nuxtifyts\PhpDto\Support\Collection; use Nuxtifyts\PhpDto\Validation\Contracts\RuleEvaluator; use Nuxtifyts\PhpDto\Validation\Contracts\RuleGroup; +use Nuxtifyts\PhpDto\Validation\Rules\ValidationRule; abstract class LogicalRule implements RuleGroup, RuleEvaluator { @@ -28,4 +29,23 @@ public function addRule(RuleEvaluator $rule): static $this->rules->push($rule); return $this; } + + /** + * @return array + */ + abstract public function validationMessages(): array; + + /** + * @return ?array + */ + protected static function resolveValidationMessages(?RuleEvaluator $rule): ?array + { + return match (true) { + $rule instanceof LogicalRule => $rule->validationMessages(), + $rule instanceof ValidationRule => [ + $rule->name => $rule->validationMessage() + ], + default => null + }; + } } diff --git a/src/Validation/Logic/OrRule.php b/src/Validation/Logic/OrRule.php index 7babbb4..079e17a 100644 --- a/src/Validation/Logic/OrRule.php +++ b/src/Validation/Logic/OrRule.php @@ -12,4 +12,13 @@ public function evaluate(mixed $value): bool static fn (RuleEvaluator $rule) => $rule->evaluate($value) ); } + + public function validationMessages(): array + { + return [ + 'or' => $this->rules + ->map(self::resolveValidationMessages(...)) + ->all() + ]; + } } diff --git a/src/Validation/Logic/SingularRule.php b/src/Validation/Logic/SingularRule.php index d9ded31..ccd8e34 100644 --- a/src/Validation/Logic/SingularRule.php +++ b/src/Validation/Logic/SingularRule.php @@ -26,4 +26,11 @@ public function evaluate(mixed $value): bool { return (bool) $this->rules->first()?->evaluate($value); } + + public function validationMessages(): array + { + return [ + 'singular' => self::resolveValidationMessages($this->rules->first()) + ]; + } } From eb94ced72307f35198c15bdccc710cd8020b189b Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 15:36:17 -0500 Subject: [PATCH 21/31] Refactor on validation messages name Renamed validation messages to validation message tree --- src/Validation/Logic/AndRule.php | 2 +- src/Validation/Logic/LogicalRule.php | 4 ++-- src/Validation/Logic/OrRule.php | 2 +- src/Validation/Logic/SingularRule.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Validation/Logic/AndRule.php b/src/Validation/Logic/AndRule.php index b672213..437a620 100644 --- a/src/Validation/Logic/AndRule.php +++ b/src/Validation/Logic/AndRule.php @@ -13,7 +13,7 @@ public function evaluate(mixed $value): bool ); } - public function validationMessages(): array + public function validationMessageTree(): array { return [ 'and' => $this->rules diff --git a/src/Validation/Logic/LogicalRule.php b/src/Validation/Logic/LogicalRule.php index 310cbeb..e770080 100644 --- a/src/Validation/Logic/LogicalRule.php +++ b/src/Validation/Logic/LogicalRule.php @@ -33,7 +33,7 @@ public function addRule(RuleEvaluator $rule): static /** * @return array */ - abstract public function validationMessages(): array; + abstract public function validationMessageTree(): array; /** * @return ?array @@ -41,7 +41,7 @@ abstract public function validationMessages(): array; protected static function resolveValidationMessages(?RuleEvaluator $rule): ?array { return match (true) { - $rule instanceof LogicalRule => $rule->validationMessages(), + $rule instanceof LogicalRule => $rule->validationMessageTree(), $rule instanceof ValidationRule => [ $rule->name => $rule->validationMessage() ], diff --git a/src/Validation/Logic/OrRule.php b/src/Validation/Logic/OrRule.php index 079e17a..6323ef3 100644 --- a/src/Validation/Logic/OrRule.php +++ b/src/Validation/Logic/OrRule.php @@ -13,7 +13,7 @@ public function evaluate(mixed $value): bool ); } - public function validationMessages(): array + public function validationMessageTree(): array { return [ 'or' => $this->rules diff --git a/src/Validation/Logic/SingularRule.php b/src/Validation/Logic/SingularRule.php index ccd8e34..b0da119 100644 --- a/src/Validation/Logic/SingularRule.php +++ b/src/Validation/Logic/SingularRule.php @@ -27,7 +27,7 @@ public function evaluate(mixed $value): bool return (bool) $this->rules->first()?->evaluate($value); } - public function validationMessages(): array + public function validationMessageTree(): array { return [ 'singular' => self::resolveValidationMessages($this->rules->first()) From aacb5a08841e33a0e1cc1c1978e039e41d08bf41 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 16:17:38 -0500 Subject: [PATCH 22/31] Adjusted NullableRule Will return true only if value is null Validation message is updated to specify that the attribute has to be nullable --- src/Validation/Rules/NullableRule.php | 4 ++-- tests/Unit/Validation/Rules/NullableRuleTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Validation/Rules/NullableRule.php b/src/Validation/Rules/NullableRule.php index dc2e6ca..0f5f543 100644 --- a/src/Validation/Rules/NullableRule.php +++ b/src/Validation/Rules/NullableRule.php @@ -12,7 +12,7 @@ class NullableRule implements ValidationRule public function evaluate(mixed $value): bool { - return true; + return is_null($value); } /** @@ -25,6 +25,6 @@ public static function make(?array $parameters = null): self public function validationMessage(): string { - return ''; + return 'The :attribute must be nullable.'; } } diff --git a/tests/Unit/Validation/Rules/NullableRuleTest.php b/tests/Unit/Validation/Rules/NullableRuleTest.php index f0b4537..6c9fef0 100644 --- a/tests/Unit/Validation/Rules/NullableRuleTest.php +++ b/tests/Unit/Validation/Rules/NullableRuleTest.php @@ -19,7 +19,7 @@ final class NullableRuleTest extends ValidationRuleTestCase public function validate_validation_message(): void { self::assertEquals( - '', + 'The :attribute must be nullable.', NullableRule::make()->validationMessage() ); } @@ -43,12 +43,12 @@ public static function data_provider(): array 'valueToBeEvaluated' => null, 'expectedResult' => true, ], - 'Will return true if the value exists' => [ + 'Will return false if the value is different then null' => [ 'validationRuleClassString' => NullableRule::class, 'makeParams' => null, 'expectedMakeException' => null, 'valueToBeEvaluated' => 'Something', - 'expectedResult' => true, + 'expectedResult' => false, ] ]; } From 38be46c42df216af63175c9aae2ec4277e332299 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 16:18:40 -0500 Subject: [PATCH 23/31] Specifying type of const in StringRule --- src/Validation/Rules/StringRule.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index fd2e727..a91843b 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -5,7 +5,6 @@ use Nuxtifyts\PhpDto\Exceptions\ValidationRuleException; use Nuxtifyts\PhpDto\Support\Arr; use Override; -use Nuxtifyts\PhpDto\Validation\Rules\RegexRule; class StringRule extends RegexRule { @@ -15,7 +14,7 @@ class StringRule extends RegexRule protected const string TYPE_ALPHA = 'alpha'; /** @var list */ - protected const TYPES = [ + protected const array TYPES = [ self::TYPE_STRING, self::TYPE_ALPHA, ]; From 22de1a0cb0fa87ba27dbed804beb97a3b37298e6 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 16:48:25 -0500 Subject: [PATCH 24/31] Added tests for SingularRule Tested that SingularRule is functioning as expected: carrying out a ValidationRule or LogicalRule Updated Arr and Collection and added flatten function (Not yet tested) --- src/Support/Arr.php | 36 ++++++ src/Support/Collection.php | 16 +++ src/Validation/Logic/AndRule.php | 1 + src/Validation/Logic/OrRule.php | 1 + src/Validation/Rules/NullableRule.php | 2 +- .../Logical/LogicalRuleTestCase.php | 69 +++++++++++ .../Logical/SingularLogicalRuleTest.php | 112 ++++++++++++++++++ .../Validation/Rules/NullableRuleTest.php | 2 +- 8 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Validation/Logical/LogicalRuleTestCase.php create mode 100644 tests/Unit/Validation/Logical/SingularLogicalRuleTest.php diff --git a/src/Support/Arr.php b/src/Support/Arr.php index cf9547d..c6cb3e8 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -156,4 +156,40 @@ public static function isArrayOfClassStrings(array $array, string $classString): && is_subclass_of($value, $classString) ); } + + /** + * @param array $array + * + * @return ($preserveKeys is true ? array : list) + */ + public static function flatten(array $array, float $depth = INF, bool $preserveKeys = true): array + { + $result = []; + + foreach ($array as $key => $item) { + $item = $item instanceof Collection ? $item->all() : $item; + + if (! is_array($item)) { + if ($preserveKeys) { + $result[$key] = $item; + } else { + $result[] = $item; + } + } else { + $values = $depth === 1.0 + ? $item + : self::flatten($item, $depth - 1, $preserveKeys); + + foreach ($values as $subKey => $value) { + if ($preserveKeys) { + $result[$subKey] = $value; + } else { + $result[] = $value; + } + } + } + } + + return $result; + } } diff --git a/src/Support/Collection.php b/src/Support/Collection.php index 1bb8020..0268953 100644 --- a/src/Support/Collection.php +++ b/src/Support/Collection.php @@ -65,6 +65,22 @@ public function map(callable $callable): self return new self(array_map($callable, $this->items)); } + /** + * @return ($preserveKeys is true ? Collection : Collection) + */ + public function collapse(bool $preserveKeys = true): self + { + return $this->flatten(1, $preserveKeys); + } + + /** + * @return ($preserveKeys is true ? Collection : Collection) + */ + public function flatten(float $depth = INF, bool $preserveKeys = true): self + { + return new self(Arr::flatten($this->items, $depth, $preserveKeys)); + } + public function isNotEmpty(): bool { return !empty($this->items); diff --git a/src/Validation/Logic/AndRule.php b/src/Validation/Logic/AndRule.php index 437a620..4690a23 100644 --- a/src/Validation/Logic/AndRule.php +++ b/src/Validation/Logic/AndRule.php @@ -18,6 +18,7 @@ public function validationMessageTree(): array return [ 'and' => $this->rules ->map(self::resolveValidationMessages(...)) + ->collapse() ->all() ]; } diff --git a/src/Validation/Logic/OrRule.php b/src/Validation/Logic/OrRule.php index 6323ef3..3c303e9 100644 --- a/src/Validation/Logic/OrRule.php +++ b/src/Validation/Logic/OrRule.php @@ -18,6 +18,7 @@ public function validationMessageTree(): array return [ 'or' => $this->rules ->map(self::resolveValidationMessages(...)) + ->collapse() ->all() ]; } diff --git a/src/Validation/Rules/NullableRule.php b/src/Validation/Rules/NullableRule.php index 0f5f543..b0b5e20 100644 --- a/src/Validation/Rules/NullableRule.php +++ b/src/Validation/Rules/NullableRule.php @@ -25,6 +25,6 @@ public static function make(?array $parameters = null): self public function validationMessage(): string { - return 'The :attribute must be nullable.'; + return 'The :attribute field must be nullable.'; } } diff --git a/tests/Unit/Validation/Logical/LogicalRuleTestCase.php b/tests/Unit/Validation/Logical/LogicalRuleTestCase.php new file mode 100644 index 0000000..0c04c17 --- /dev/null +++ b/tests/Unit/Validation/Logical/LogicalRuleTestCase.php @@ -0,0 +1,69 @@ + $logicalRuleClassString + * @param list $ruleEvaluators + * @param ?class-string $expectedCreateException + * @param array $expectedValidationMessageTree + * + * @throws Throwable + */ + #[Test] + #[DataProvider('data_provider')] + public function will_be_able_to_use_logical_rules( + string $logicalRuleClassString, + array $ruleEvaluators, + ?string $expectedCreateException, + mixed $valueToBeEvaluated, + bool $expectedResult, + array $expectedValidationMessageTree + ): void { + if ($expectedCreateException) { + self::expectException($expectedCreateException); + } + + $logicalRule = new $logicalRuleClassString(); + + foreach ($ruleEvaluators as $ruleEvaluator) { + $logicalRule->addRule($ruleEvaluator); + } + + if ($expectedCreateException) { + return; + } + + self::assertEquals( + $expectedResult, + $logicalRule->evaluate($valueToBeEvaluated) + ); + + self::assertEquals( + $expectedValidationMessageTree, + $logicalRule->validationMessageTree() + ); + } + + /** + * @return array, + * ruleEvaluators: list, + * expectedCreateException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool, + * expectedValidationMessageTree: array + * }> + */ + abstract public static function data_provider(): array; +} diff --git a/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php b/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php new file mode 100644 index 0000000..20dc77e --- /dev/null +++ b/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php @@ -0,0 +1,112 @@ +, + * ruleEvaluators: list, + * expectedCreateException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool, + * expectedValidationMessageTree: array + * }> + * + * @throws Throwable + */ + public static function data_provider(): array + { + return [ + 'Will throw an exception when trying to add more than one rule' => [ + 'logicalRuleClassString' => SingularRule::class, + 'ruleEvaluators' => [ + RequiredRule::make(), + RequiredRule::make() + ], + 'expectedCreateException' => LogicalRuleException::class, + 'valueToBeEvaluated' => 'value', + 'expectedResult' => false, + 'expectedValidationMessageTree' => [] + ], + 'Will be able to use validation rules' => [ + 'logicalRuleClassString' => SingularRule::class, + 'ruleEvaluators' => [ + $ruleA = RequiredRule::make() + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'value', + 'expectedResult' => true, + 'expectedValidationMessageTree' => [ + 'singular' => [ + $ruleA->name => $ruleA->validationMessage() + ] + ] + ], + 'Will be able to resolve complex validations using OrRule and AndRule' => [ + 'logicalRuleClassString' => SingularRule::class, + 'ruleEvaluators' => $ruleEvaluators = [ + $orRule = new OrRule() + ->addRule( + $andRule = new AndRule() + ->addRule($requiredRule = RequiredRule::make()) + ->addRule($emailRule = EmailRule::make()) + ) + ->addRule( + $nullableRule = NullableRule::make() + ) + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'johndoe@example.test', + 'expectedResult' => true, + 'expectedValidationMessageTree' => $validationTree = [ + 'singular' => [ + 'or' => [ + 'and' => [ + 'required' => $requiredRule->validationMessage(), + 'email' => $emailRule->validationMessage() + ], + 'nullable' => $nullableRule->validationMessage() + ] + ] + ] + ], + 'Will be able to resolve complex validations using OrRule and AndRule 2' => [ + 'logicalRuleClassString' => SingularRule::class, + 'ruleEvaluators' => $ruleEvaluators, + 'expectedCreateException' => null, + 'valueToBeEvaluated' => null, + 'expectedResult' => true, + 'expectedValidationMessageTree' => $validationTree, + ], + 'Will be able to resolve complex validations using OrRule and AndRule 3' => [ + 'logicalRuleClassString' => SingularRule::class, + 'ruleEvaluators' => $ruleEvaluators, + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 1234.5, + 'expectedResult' => false, + 'expectedValidationMessageTree' => $validationTree, + ] + ]; + } +} diff --git a/tests/Unit/Validation/Rules/NullableRuleTest.php b/tests/Unit/Validation/Rules/NullableRuleTest.php index 6c9fef0..106df6f 100644 --- a/tests/Unit/Validation/Rules/NullableRuleTest.php +++ b/tests/Unit/Validation/Rules/NullableRuleTest.php @@ -19,7 +19,7 @@ final class NullableRuleTest extends ValidationRuleTestCase public function validate_validation_message(): void { self::assertEquals( - 'The :attribute must be nullable.', + 'The :attribute field must be nullable.', NullableRule::make()->validationMessage() ); } From ef482c9f911f25c294e9988527b53d499b399775 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 16:53:12 -0500 Subject: [PATCH 25/31] Fixing PHPStan --- .../Logical/SingularLogicalRuleTest.php | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php b/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php index 20dc77e..ba781ae 100644 --- a/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php +++ b/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php @@ -37,6 +37,32 @@ final class SingularLogicalRuleTest extends LogicalRuleTestCase */ public static function data_provider(): array { + /** @var list $ruleEvaluators */ + $ruleEvaluators = [ + $orRule = new OrRule() + ->addRule( + $andRule = new AndRule() + ->addRule($requiredRule = RequiredRule::make()) + ->addRule($emailRule = EmailRule::make()) + ) + ->addRule( + $nullableRule = NullableRule::make() + ) + ]; + + /** @var array $validationTree */ + $validationTree = [ + 'singular' => [ + 'or' => [ + 'and' => [ + 'required' => $requiredRule->validationMessage(), + 'email' => $emailRule->validationMessage() + ], + 'nullable' => $nullableRule->validationMessage() + ] + ] + ]; + return [ 'Will throw an exception when trying to add more than one rule' => [ 'logicalRuleClassString' => SingularRule::class, @@ -65,31 +91,11 @@ public static function data_provider(): array ], 'Will be able to resolve complex validations using OrRule and AndRule' => [ 'logicalRuleClassString' => SingularRule::class, - 'ruleEvaluators' => $ruleEvaluators = [ - $orRule = new OrRule() - ->addRule( - $andRule = new AndRule() - ->addRule($requiredRule = RequiredRule::make()) - ->addRule($emailRule = EmailRule::make()) - ) - ->addRule( - $nullableRule = NullableRule::make() - ) - ], + 'ruleEvaluators' => $ruleEvaluators, 'expectedCreateException' => null, 'valueToBeEvaluated' => 'johndoe@example.test', 'expectedResult' => true, - 'expectedValidationMessageTree' => $validationTree = [ - 'singular' => [ - 'or' => [ - 'and' => [ - 'required' => $requiredRule->validationMessage(), - 'email' => $emailRule->validationMessage() - ], - 'nullable' => $nullableRule->validationMessage() - ] - ] - ] + 'expectedValidationMessageTree' => $validationTree ], 'Will be able to resolve complex validations using OrRule and AndRule 2' => [ 'logicalRuleClassString' => SingularRule::class, From 942ddc9c9944e54ac58d8e6d92f700a2fc351936 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 17:53:29 -0500 Subject: [PATCH 26/31] Added tests for AndRule and OrRule --- src/Validation/Rules/StringRule.php | 2 +- tests/Unit/Validation/Logical/AndRuleTest.php | 70 ++++++++++++++++ tests/Unit/Validation/Logical/OrRuleTest.php | 83 +++++++++++++++++++ ...gicalRuleTest.php => SingularRuleTest.php} | 2 +- .../Unit/Validation/Rules/StringRuleTest.php | 2 +- 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Validation/Logical/AndRuleTest.php create mode 100644 tests/Unit/Validation/Logical/OrRuleTest.php rename tests/Unit/Validation/Logical/{SingularLogicalRuleTest.php => SingularRuleTest.php} (98%) diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index a91843b..a7114eb 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -43,7 +43,7 @@ public static function make(?array $parameters = null): self /** @var 'string' | 'alpha' $strType */ $strPattern = match ($strType) { - self::TYPE_STRING => '[a-zA-Z]', + self::TYPE_STRING => '.', self::TYPE_ALPHA => '[a-zA-Z0-9]' }; diff --git a/tests/Unit/Validation/Logical/AndRuleTest.php b/tests/Unit/Validation/Logical/AndRuleTest.php new file mode 100644 index 0000000..ca703df --- /dev/null +++ b/tests/Unit/Validation/Logical/AndRuleTest.php @@ -0,0 +1,70 @@ +, + * ruleEvaluators: list, + * expectedCreateException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool, + * expectedValidationMessageTree: array + * }> + * + * @throws Throwable + */ + public static function data_provider(): array + { + return [ + 'Will be able to use validation rules' => [ + 'logicalRuleClassString' => AndRule::class, + 'ruleEvaluators' => [ + $stringRule = StringRule::make(), + $emailRule = EmailRule::make() + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'string', + 'expectedResult' => false, + 'expectedValidationMessageTree' => [ + 'and' => [ + $stringRule->name => $stringRule->validationMessage(), + $emailRule->name => $emailRule->validationMessage() + ] + ] + ], + 'Will be able to use validation rules 2' => [ + 'logicalRuleClassString' => AndRule::class, + 'ruleEvaluators' => [ + $stringRule = StringRule::make(), + $emailRule = EmailRule::make() + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'johndoe@example.test', + 'expectedResult' => true, + 'expectedValidationMessageTree' => [ + 'and' => [ + $stringRule->name => $stringRule->validationMessage(), + $emailRule->name => $emailRule->validationMessage() + ] + ] + ] + ]; + } +} diff --git a/tests/Unit/Validation/Logical/OrRuleTest.php b/tests/Unit/Validation/Logical/OrRuleTest.php new file mode 100644 index 0000000..2cc627d --- /dev/null +++ b/tests/Unit/Validation/Logical/OrRuleTest.php @@ -0,0 +1,83 @@ +, + * ruleEvaluators: list, + * expectedCreateException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool, + * expectedValidationMessageTree: array + * }> + * + * @throws Throwable + */ + public static function data_provider(): array + { + return [ + 'Will be able to use validation rules' => [ + 'logicalRuleClassString' => OrRule::class, + 'ruleEvaluators' => [ + $stringRule = StringRule::make(), + $numericRule = NumericRule::make() + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'string', + 'expectedResult' => true, + 'expectedValidationMessageTree' => [ + 'or' => [ + $stringRule->name => $stringRule->validationMessage(), + $numericRule->name => $numericRule->validationMessage() + ] + ] + ], + 'Will be able to use validation rules 2' => [ + 'logicalRuleClassString' => OrRule::class, + 'ruleEvaluators' => [ + $stringRule, + $numericRule + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 1234, + 'expectedResult' => true, + 'expectedValidationMessageTree' => [ + 'or' => [ + $stringRule->name => $stringRule->validationMessage(), + $numericRule->name => $numericRule->validationMessage() + ] + ] + ], + 'Will be able to use validation rule 3' => [ + 'logicalRuleClassString' => OrRule::class, + 'ruleEvaluators' => [ + $numericRule + ], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 1234.45, + 'expectedResult' => false, + 'expectedValidationMessageTree' => [ + 'or' => [ + $numericRule->name => $numericRule->validationMessage() + ] + ] + ] + ]; + } +} diff --git a/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php b/tests/Unit/Validation/Logical/SingularRuleTest.php similarity index 98% rename from tests/Unit/Validation/Logical/SingularLogicalRuleTest.php rename to tests/Unit/Validation/Logical/SingularRuleTest.php index ba781ae..44fabe6 100644 --- a/tests/Unit/Validation/Logical/SingularLogicalRuleTest.php +++ b/tests/Unit/Validation/Logical/SingularRuleTest.php @@ -21,7 +21,7 @@ #[UsesClass(OrRule::class)] #[UsesClass(AndRule::class)] #[UsesClass(OrRule::class)] -final class SingularLogicalRuleTest extends LogicalRuleTestCase +final class SingularRuleTest extends LogicalRuleTestCase { /** * @return array StringRule::class, 'makeParams' => null, 'expectedMakeException' => null, - 'valueToBeEvaluated' => 'test1234', + 'valueToBeEvaluated' => null, 'expectedResult' => false ], 'Will evaluate true when a string value is provided' => [ From a3c610e5aa9a99a70a5fcb093fe5a43d49e622b4 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 18:05:17 -0500 Subject: [PATCH 27/31] Added Array test for flatten function --- tests/Unit/Support/ArrTest.php | 121 +++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/Unit/Support/ArrTest.php b/tests/Unit/Support/ArrTest.php index 587fb4c..59f77d6 100644 --- a/tests/Unit/Support/ArrTest.php +++ b/tests/Unit/Support/ArrTest.php @@ -26,6 +26,7 @@ final class ArrTest extends UnitCase #[Test] #[DataProvider('get_arr_provider')] #[DataProvider('is_array_of_class_strings_provider')] + #[DataProvider('flatten_provider')] public function arr_helper_functions( string $functionName, array $parameters, @@ -374,4 +375,124 @@ public static function is_array_of_class_strings_provider(): array ], ]; } + + /** + * @return array + */ + public static function flatten_provider(): array + { + return [ + 'flatten, empty array' => [ + 'flatten', + [ + 'array' => [], + ], + [] + ], + 'flatten array, one depth' => [ + 'flatten', + [ + 'array' => [ + 'a' => [ + 'a1' => 1.1, + 'a2' => 1.2 + ], + 'b' => 2, + ], + ], + [ + 'a1' => 1.1, + 'a2' => 1.2, + 'b' => 2 + ] + ], + 'flatten array, multiple depths' => [ + 'flatten', + [ + 'array' => [ + 'a' => [ + 'a1' => [ + 'a1.1' => 1.1, + 'a1.2' => 1.2 + ], + 'a2' => 2 + ], + 'b' => 3, + ], + ], + [ + 'a1.1' => 1.1, + 'a1.2' => 1.2, + 'a2' => 2, + 'b' => 3 + ] + ], + 'flatten array, and resets array keys' => [ + 'flatten', + [ + 'array' => [ + 'a' => [ + 'a1' => 1.1, + 'a2' => 1.2 + ], + 'b' => 2, + ], + 'depth' => 1.0, + 'preserveKeys' => false, + ], + [ + 1.1, + 1.2, + 2 + ] + ], + 'flatten array, and resets array keys, multiple depths' => [ + 'flatten', + [ + 'array' => [ + 'a' => [ + 'a1' => [ + 'a1.1' => 1.1, + 'a1.2' => 1.2 + ], + 'a2' => 2 + ], + 'b' => 3, + ], + 'preserveKeys' => false, + ], + [ + 1.1, + 1.2, + 2, + 3 + ] + ], + 'flatten array, one depth, not enough' => [ + 'flatten', + [ + 'array' => [ + 'a' => [ + 'a1' => [ + 'a1.1' => 1.1, + 'a1.2' => 1.2 + ], + 'a2' => 2 + ], + 'b' => 3, + ], + 'depth' => 1, + 'preserveKeys' => false, + ], + [ + [ + 'a1.1' => 1.1, + 'a1.2' => 1.2 + ], + 2, + 3 + ] + ] + ]; + } } From 13df455961aa341ca54a834c26813fc62666d82e Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 18:46:45 -0500 Subject: [PATCH 28/31] Added Collection test Switched default value for preserved keys in collapse function in Collection to false --- src/Support/Collection.php | 2 +- src/Validation/Logic/AndRule.php | 2 +- src/Validation/Logic/OrRule.php | 2 +- tests/Unit/Support/CollectionTest.php | 238 ++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Support/CollectionTest.php diff --git a/src/Support/Collection.php b/src/Support/Collection.php index 0268953..47369c1 100644 --- a/src/Support/Collection.php +++ b/src/Support/Collection.php @@ -68,7 +68,7 @@ public function map(callable $callable): self /** * @return ($preserveKeys is true ? Collection : Collection) */ - public function collapse(bool $preserveKeys = true): self + public function collapse(bool $preserveKeys = false): self { return $this->flatten(1, $preserveKeys); } diff --git a/src/Validation/Logic/AndRule.php b/src/Validation/Logic/AndRule.php index 4690a23..f0cf592 100644 --- a/src/Validation/Logic/AndRule.php +++ b/src/Validation/Logic/AndRule.php @@ -18,7 +18,7 @@ public function validationMessageTree(): array return [ 'and' => $this->rules ->map(self::resolveValidationMessages(...)) - ->collapse() + ->collapse(preserveKeys: true) ->all() ]; } diff --git a/src/Validation/Logic/OrRule.php b/src/Validation/Logic/OrRule.php index 3c303e9..420dd73 100644 --- a/src/Validation/Logic/OrRule.php +++ b/src/Validation/Logic/OrRule.php @@ -18,7 +18,7 @@ public function validationMessageTree(): array return [ 'or' => $this->rules ->map(self::resolveValidationMessages(...)) - ->collapse() + ->collapse(preserveKeys: true) ->all() ]; } diff --git a/tests/Unit/Support/CollectionTest.php b/tests/Unit/Support/CollectionTest.php new file mode 100644 index 0000000..9039364 --- /dev/null +++ b/tests/Unit/Support/CollectionTest.php @@ -0,0 +1,238 @@ + $collection + * @param array $functionParams + */ + #[Test] + #[DataProvider('push_function_provider')] + #[DataProvider('put_function_provider')] + #[DataProvider('first_function_provider')] + #[DataProvider('map_function_provider')] + #[DataProvider('collapse_function_provider')] + #[DataProvider('flatten_function_provider')] + #[DataProvider('all_function_provider')] + #[DataProvider('validation_functions_provider')] + public function will_be_able_to_perform_functions( + Collection $collection, + string $functionName, + array $functionParams, + mixed $expected + ): void { + $result = $collection->{$functionName}(...$functionParams); + + if ($expected instanceof Collection) { + self::assertInstanceOf(Collection::class, $result); + self::assertCollection($result, $expected); + } else { + self::assertEquals($expected, $result); + } + } + + /** + * @return array + */ + public static function push_function_provider(): array + { + return [ + 'push' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'push', + 'functionParams' => [ 'item' => 4 ], + 'expected' => new Collection([1, 2, 3, 4]) + ] + ]; + } + + /** + * @return array + */ + public static function put_function_provider(): array + { + return [ + 'put in new key' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'put', + 'functionParams' => [ 'key' => 3, 'value' => 4 ], + 'expected' => new Collection([1, 2, 3, 4]) + ], + 'put in existing key will override value' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'put', + 'functionParams' => [ 'key' => 0, 'value' => 4 ], + 'expected' => new Collection([4, 2, 3]) + ] + ]; + } + + /** + * @return array + */ + public static function first_function_provider(): array + { + return [ + 'first without callable and non empty collection' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'first', + 'functionParams' => [], + 'expected' => 1 + ], + 'first without callable and empty collection' => [ + 'collection' => new Collection([]), + 'functionName' => 'first', + 'functionParams' => [], + 'expected' => null + ], + 'first with callable and existing item that will meet requirements' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'first', + 'functionParams' => [ 'callable' => static fn (int $item) => $item === 2 ], + 'expected' => 2 + ], + 'first with callable and no item that will meet requirements' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'first', + 'functionParams' => [ 'callable' => static fn (int $item) => $item === 4 ], + 'expected' => null + ] + ]; + } + + /** + * @return array + */ + public static function map_function_provider(): array + { + return [ + 'map' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'map', + 'functionParams' => [ 'callable' => static fn (int $item) => $item * 2 ], + 'expected' => new Collection([2, 4, 6]) + ] + ]; + } + + /** + * @return array + */ + public static function collapse_function_provider(): array + { + return [ + 'collapse' => [ + 'collection' => new Collection([ + new Collection([ 'a' => 1, 2, 3]), + new Collection([4, 5, 6]), + new Collection([7, 8, 9]) + ]), + 'functionName' => 'collapse', + 'functionParams' => [], + 'expected' => new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9]) + ], + + ]; + } + + /** + * @return array + */ + public static function flatten_function_provider(): array + { + return [ + 'flatten' => [ + 'collection' => new Collection([ + 'a1' => new Collection([ + 'a1.1' => 1.1, + 'a1.2' => new Collection([ + 'a1.2.1' => 1.21, + 'a1.2.2' => 1.22, + 'a1.2.3' => new Collection([ + 'a1.2.3.1' => 1.231, + 'a1.2.3.2' => 1.232, + 'a1.2.3.3' => 1.233 + ]) + ]) + ]) + ]), + 'functionName' => 'flatten', + 'functionParams' => [], + 'expected' => new Collection([ + 'a1.1' => 1.1, + 'a1.2.1' => 1.21, + 'a1.2.2' => 1.22, + 'a1.2.3.1' => 1.231, + 'a1.2.3.2' => 1.232, + 'a1.2.3.3' => 1.233 + ]) + ] + ]; + } + + /** + * @return array + */ + public static function all_function_provider(): array + { + return [ + 'all' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'all', + 'functionParams' => [], + 'expected' => [1, 2, 3] + ] + ]; + } + + /** + * @return array + */ + public static function validation_functions_provider(): array + { + return [ + 'isEmpty' => [ + 'collection' => new Collection([]), + 'functionName' => 'isEmpty', + 'functionParams' => [], + 'expected' => true + ], + 'isNotEmpty' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'isNotEmpty', + 'functionParams' => [], + 'expected' => true + ], + 'every' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'every', + 'functionParams' => [ 'callable' => static fn (int $item) => $item > 0 ], + 'expected' => true + ], + 'some' => [ + 'collection' => new Collection([1, 2, 3]), + 'functionName' => 'some', + 'functionParams' => [ 'callable' => static fn (int $item) => $item === 2 ], + 'expected' => true + ], + ]; + } + + /** + * @param Collection $collection + * @param Collection $expected + */ + private static function assertCollection(Collection $collection, Collection $expected): void + { + self::assertEquals($expected->all(), $collection->all()); + } +} From 604f2a09d743fad5fa8b0715218375bbe5f2a1dd Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Sun, 19 Jan 2025 18:47:47 -0500 Subject: [PATCH 29/31] PHPStan fixes --- tests/Unit/Validation/Logical/OrRuleTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Unit/Validation/Logical/OrRuleTest.php b/tests/Unit/Validation/Logical/OrRuleTest.php index 2cc627d..c5c3e78 100644 --- a/tests/Unit/Validation/Logical/OrRuleTest.php +++ b/tests/Unit/Validation/Logical/OrRuleTest.php @@ -51,8 +51,8 @@ public static function data_provider(): array 'Will be able to use validation rules 2' => [ 'logicalRuleClassString' => OrRule::class, 'ruleEvaluators' => [ - $stringRule, - $numericRule + $stringRule = StringRule::make(), + $numericRule = NumericRule::make() ], 'expectedCreateException' => null, 'valueToBeEvaluated' => 1234, @@ -67,7 +67,7 @@ public static function data_provider(): array 'Will be able to use validation rule 3' => [ 'logicalRuleClassString' => OrRule::class, 'ruleEvaluators' => [ - $numericRule + $numericRule = NumericRule::make() ], 'expectedCreateException' => null, 'valueToBeEvaluated' => 1234.45, From c88db7fa7d22f35425641346dbbd510a7ebed599 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Mon, 20 Jan 2025 06:42:59 -0500 Subject: [PATCH 30/31] Added unit test and ci test separately --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a09f12f..0a5b607 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ } }, "scripts": { - "ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=ci --configuration phpunit.xml", + "ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --testsuite=ci --configuration phpunit.xml", + "unit-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --configuration phpunit.xml", "phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon --memory-limit=256M" } } From e0bfd779b6f601dee1afc74561de0da4b6a3cfe7 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Mon, 27 Jan 2025 18:12:39 -0500 Subject: [PATCH 31/31] Added truthy rule --- src/Validation/Logic/TruthyRule.php | 29 ++++++++++ .../Validation/Logical/TruthyRuleTest.php | 54 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/Validation/Logic/TruthyRule.php create mode 100644 tests/Unit/Validation/Logical/TruthyRuleTest.php diff --git a/src/Validation/Logic/TruthyRule.php b/src/Validation/Logic/TruthyRule.php new file mode 100644 index 0000000..4d1d07d --- /dev/null +++ b/src/Validation/Logic/TruthyRule.php @@ -0,0 +1,29 @@ +, + * ruleEvaluators: list, + * expectedCreateException: ?class-string, + * valueToBeEvaluated: mixed, + * expectedResult: bool, + * expectedValidationMessageTree: array + * }> + * + * @throws Throwable + */ + public static function data_provider(): array + { + return [ + 'Will always return true when validating' => [ + 'logicalRuleClassString' => TruthyRule::class, + 'ruleEvaluators' => [], + 'expectedCreateException' => null, + 'valueToBeEvaluated' => 'string', + 'expectedResult' => true, + 'expectedValidationMessageTree' => [] + ], + 'Will throw an exception if trying to add a nested rule' => [ + 'logicalRuleClassString' => TruthyRule::class, + 'ruleEvaluators' => [ + StringRule::make(), + ], + 'expectedCreateException' => LogicalRuleException::class, + 'valueToBeEvaluated' => 1234, + 'expectedResult' => true, + 'expectedValidationMessageTree' => [] + ] + ]; + } +}