From 5b2bedd1684368a2752b212791f3eeedabf46fb7 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Thu, 24 Oct 2024 18:16:38 -0300 Subject: [PATCH] refactor(validator): improve SOLID principles implementation and add tests - fix: state controle for message errors - Extract interfaces for better dependency inversion - Implement proper validation result processing - Add unit tests for validation workflow - Improve email validation logic - Update composer dependencies BREAKING CHANGE: Validator constructor signature has changed to support dependency injection --- composer.lock | 20 +- src/Processor/AbstractValidatorProcessor.php | 9 + .../DefaultValidationResultProcessor.php | 13 +- src/Processor/Input/EmailValidator.php | 2 + src/ValidationResult.php | 28 ++- src/Validator.php | 17 +- tests/Attribute/ValidateTest.php | 11 +- tests/ValidatorTest.php | 34 +--- tests/application2.php | 181 ++++++++++++++++++ 9 files changed, 246 insertions(+), 69 deletions(-) create mode 100644 tests/application2.php diff --git a/composer.lock b/composer.lock index 86f1731..b6882b8 100755 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "kariricode/contract", - "version": "v2.7.10", + "version": "v2.7.11", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "84db138e9e03e7173ee1a37d75fa21d756a6d060" + "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/84db138e9e03e7173ee1a37d75fa21d756a6d060", - "reference": "84db138e9e03e7173ee1a37d75fa21d756a6d060", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/72c834a3afe2dbded8f6a7f96005635424636d4b", + "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b", "shasum": "" }, "require": { @@ -66,7 +66,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-contract/issues", "source": "https://github.com/KaririCode-Framework/kariricode-contract" }, - "time": "2024-10-21T18:32:50+00:00" + "time": "2024-10-24T18:51:39+00:00" }, { "name": "kariricode/data-structure", @@ -201,16 +201,16 @@ }, { "name": "kariricode/processor-pipeline", - "version": "v1.1.5", + "version": "v1.1.6", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline.git", - "reference": "6bc3e747f254c56b5fc0cbdf22b1d3ce1497a7d0" + "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/6bc3e747f254c56b5fc0cbdf22b1d3ce1497a7d0", - "reference": "6bc3e747f254c56b5fc0cbdf22b1d3ce1497a7d0", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/58a25f345d066c7d7b69331bdbe1d468513964bf", + "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf", "shasum": "" }, "require": { @@ -256,7 +256,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline/issues", "source": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline" }, - "time": "2024-10-18T18:33:05+00:00" + "time": "2024-10-24T18:55:45+00:00" }, { "name": "kariricode/property-inspector", diff --git a/src/Processor/AbstractValidatorProcessor.php b/src/Processor/AbstractValidatorProcessor.php index 95b2013..1da58dc 100644 --- a/src/Processor/AbstractValidatorProcessor.php +++ b/src/Processor/AbstractValidatorProcessor.php @@ -12,6 +12,15 @@ abstract class AbstractValidatorProcessor implements Processor, ValidatableProce protected bool $isValid = true; protected string $errorKey = ''; + /** + * Reset the processor's state back to its initial values. + */ + public function reset(): void + { + $this->isValid = true; + $this->errorKey = ''; + } + protected function setInvalid(string $errorKey): void { $this->isValid = false; diff --git a/src/Processor/DefaultValidationResultProcessor.php b/src/Processor/DefaultValidationResultProcessor.php index 75ab6ec..deac17a 100644 --- a/src/Processor/DefaultValidationResultProcessor.php +++ b/src/Processor/DefaultValidationResultProcessor.php @@ -5,31 +5,26 @@ namespace KaririCode\Validator\Processor; use KaririCode\PropertyInspector\AttributeHandler; -use KaririCode\Validator\Contract\ValidationResult as ValidationResultContract; use KaririCode\Validator\Contract\ValidationResultProcessor; use KaririCode\Validator\ValidationResult; class DefaultValidationResultProcessor implements ValidationResultProcessor { - public function __construct( - private ValidationResultContract $result = new ValidationResult() - ) { - } - public function process(AttributeHandler $handler): ValidationResult { + $result = new ValidationResult(); $processedValues = $handler->getProcessedPropertyValues(); $errors = $handler->getProcessingResultErrors(); foreach ($processedValues as $property => $data) { - $this->result->setValidatedData($property, $data['value']); + $result->setValidatedData($property, $data['value']); if (isset($errors[$property])) { - $this->addPropertyErrors($this->result, $property, $errors[$property]); + $this->addPropertyErrors($result, $property, $errors[$property]); } } - return $this->result; + return $result; } private function addPropertyErrors( diff --git a/src/Processor/Input/EmailValidator.php b/src/Processor/Input/EmailValidator.php index e295e3b..537b852 100644 --- a/src/Processor/Input/EmailValidator.php +++ b/src/Processor/Input/EmailValidator.php @@ -16,6 +16,8 @@ public function process(mixed $input): mixed return $input; } + $input = trim($input); + if (false === filter_var($input, FILTER_VALIDATE_EMAIL)) { $this->setInvalid('invalidFormat'); } diff --git a/src/ValidationResult.php b/src/ValidationResult.php index 3936aa0..64ebd86 100644 --- a/src/ValidationResult.php +++ b/src/ValidationResult.php @@ -10,20 +10,35 @@ class ValidationResult implements ValidationResultContract { private array $errors = []; private array $validatedData = []; + private array $errorHashes = []; + + /** + * Reset all validation state. + * + * Clears all errors, validation data, and error hashes, + * returning the ValidationResult to its initial state. + */ + public function reset(): void + { + $this->errors = []; + $this->validatedData = []; + $this->errorHashes = []; + } public function addError(string $property, string $errorKey, string $message): void { if (!isset($this->errors[$property])) { $this->errors[$property] = []; + $this->errorHashes[$property] = []; } // Avoid adding duplicate errors - foreach ($this->errors[$property] as $error) { - if ($error['errorKey'] === $errorKey) { - return; - } + $hash = md5($errorKey . $message); + if (isset($this->errorHashes[$property][$hash])) { + return; } + $this->errorHashes[$property][$hash] = true; $this->errors[$property][] = [ 'errorKey' => $errorKey, 'message' => $message, @@ -58,4 +73,9 @@ public function toArray(): array 'validatedData' => $this->validatedData, ]; } + + public function __destruct() + { + $this->reset(); + } } diff --git a/src/Validator.php b/src/Validator.php index 5eb8285..3fcad00 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -11,7 +11,6 @@ use KaririCode\PropertyInspector\AttributeHandler; use KaririCode\PropertyInspector\Utility\PropertyInspector; use KaririCode\Validator\Attribute\Validate; -use KaririCode\Validator\Contract\ValidationResultProcessor; use KaririCode\Validator\Processor\DefaultValidationResultProcessor; class Validator implements ValidatorContract @@ -19,24 +18,22 @@ class Validator implements ValidatorContract private const IDENTIFIER = 'validator'; private ProcessorBuilder $builder; - private PropertyInspector $propertyInspector; - private AttributeHandler $attributeHandler; public function __construct( private readonly ProcessorRegistry $registry, - private readonly ValidationResultProcessor $resultProcessor = new DefaultValidationResultProcessor() ) { $this->builder = new ProcessorBuilder($this->registry); - $this->attributeHandler = new AttributeHandler(self::IDENTIFIER, $this->builder); - $this->propertyInspector = new PropertyInspector( - new AttributeAnalyzer(Validate::class) - ); } public function validate(mixed $object): ValidationResult { - $handler = $this->propertyInspector->inspect($object, $this->attributeHandler); + $propertyInspector = new PropertyInspector( + new AttributeAnalyzer(Validate::class) + ); + $attributeHandler = new AttributeHandler(self::IDENTIFIER, $this->builder); + $resultProcessor = new DefaultValidationResultProcessor(); + $handler = $propertyInspector->inspect($object, $attributeHandler); - return $this->resultProcessor->process($handler); + return $resultProcessor->process($handler); } } diff --git a/tests/Attribute/ValidateTest.php b/tests/Attribute/ValidateTest.php index 5109729..0d3819d 100644 --- a/tests/Attribute/ValidateTest.php +++ b/tests/Attribute/ValidateTest.php @@ -48,7 +48,7 @@ public function testConstructorFiltersInvalidProcessors(): void $expectedProcessors = ['required', 'email']; $validate = new Validate($processors); - $this->assertEquals($expectedProcessors, $validate->getProcessors()); + $this->assertEquals($expectedProcessors, array_values($validate->getProcessors())); } public function testConstructorWithEmptyProcessors(): void @@ -89,14 +89,19 @@ public static function validProcessorsProvider(): array [ 'length' => ['minLength' => 3, 'maxLength' => 20], ], - ['length'], + [ + 'length' => ['minLength' => 3, 'maxLength' => 20], + ], ], 'mixed processors' => [ [ 'required', 'email' => ['message' => 'Invalid email'], ], - ['required', 'email'], + [ + 'required', + 'email' => ['message' => 'Invalid email'], + ], ], ]; } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 5ac0876..2164aa2 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -6,7 +6,6 @@ use KaririCode\Contract\Processor\ProcessorRegistry; use KaririCode\Validator\Attribute\Validate; -use KaririCode\Validator\Contract\ValidationResultProcessor; use KaririCode\Validator\Processor\Input\EmailValidator; use KaririCode\Validator\Processor\Logic\RequiredValidator; use KaririCode\Validator\ValidationResult; @@ -17,13 +16,11 @@ class ValidatorTest extends TestCase { private ProcessorRegistry|MockObject $registry; - private ValidationResultProcessor|MockObject $resultProcessor; private Validator $validator; protected function setUp(): void { $this->registry = $this->createMock(ProcessorRegistry::class); - $this->resultProcessor = $this->createMock(ValidationResultProcessor::class); $this->registry->method('get') ->willReturnMap([ @@ -31,7 +28,7 @@ protected function setUp(): void ['validator', 'email', new EmailValidator()], ]); - $this->validator = new Validator($this->registry, $this->resultProcessor); + $this->validator = new Validator($this->registry); } public function testValidateWithValidObject(): void @@ -44,11 +41,6 @@ public function testValidateWithValidObject(): void $expectedResult = new ValidationResult(); $expectedResult->setValidatedData('email', 'walmir.silva@example.com'); - $this->resultProcessor - ->expects($this->once()) - ->method('process') - ->willReturn($expectedResult); - $result = $this->validator->validate($testObject); $this->assertFalse($result->hasErrors()); @@ -65,11 +57,6 @@ public function testValidateWithInvalidObject(): void $resultWithErrors = new ValidationResult(); $resultWithErrors->addError('email', 'invalidFormat', 'Invalid email format'); - $this->resultProcessor - ->expects($this->once()) - ->method('process') - ->willReturn($resultWithErrors); - $result = $this->validator->validate($testObject); $this->assertTrue($result->hasErrors()); @@ -82,13 +69,6 @@ public function testValidateWithNoAttributes(): void public string $name = 'Test'; }; - $emptyResult = new ValidationResult(); - - $this->resultProcessor - ->expects($this->once()) - ->method('process') - ->willReturn($emptyResult); - $result = $this->validator->validate($testObject); $this->assertFalse($result->hasErrors()); @@ -99,13 +79,6 @@ public function testValidateWithNullObject(): void { $testObject = new \stdClass(); - $emptyResult = new ValidationResult(); - - $this->resultProcessor - ->expects($this->once()) - ->method('process') - ->willReturn($emptyResult); - $result = $this->validator->validate($testObject); $this->assertFalse($result->hasErrors()); @@ -128,11 +101,6 @@ public function testValidateWithMultipleProperties(): void $multiPropertyResult->setValidatedData('name', 'Walmir'); $multiPropertyResult->setValidatedData('email', 'walmir.silva@example.com'); - $this->resultProcessor - ->expects($this->once()) - ->method('process') - ->willReturn($multiPropertyResult); - $result = $this->validator->validate($testObject); $this->assertFalse($result->hasErrors()); diff --git a/tests/application2.php b/tests/application2.php new file mode 100644 index 0000000..41d9adb --- /dev/null +++ b/tests/application2.php @@ -0,0 +1,181 @@ + ['minLength' => 3, 'maxLength' => 50], + ], + messages: [ + 'required' => 'Name is required', + 'length' => 'Name must be between 3 and 50 characters', + ] + )] + private string $name = '', + #[Validate( + processors: ['required', 'email'], + messages: [ + 'required' => 'Email is required', + 'email' => 'Invalid email format', + ] + )] + private string $email = '', + #[Validate( + processors: [ + 'required', + 'integer', + 'range' => ['min' => 18, 'max' => 120], + ], + messages: [ + 'required' => 'Age is required', + 'integer' => 'Age must be a whole number', + 'range' => 'Age must be between 18 and 120', + ] + )] + private int $age = 0 + ) { + } + + // Getters and setters + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function getAge(): int + { + return $this->age; + } + + public function setAge(int $age): void + { + $this->age = $age; + } +} + +// 2. Set up the validator registry +function setupValidatorRegistry(): ProcessorRegistry +{ + $registry = new ProcessorRegistry(); + + // Register all required validators + $registry->register('validator', 'required', new RequiredValidator()); + $registry->register('validator', 'email', new EmailValidator()); + $registry->register('validator', 'length', new LengthValidator()); + $registry->register('validator', 'integer', new IntegerValidator()); + $registry->register('validator', 'range', new RangeValidator()); + + return $registry; +} + +// 3. Helper function to display validation results +function displayValidationResults(array $errors): void +{ + if (empty($errors)) { + echo "\033[32mValidation passed successfully!\033[0m\n"; + + return; + } + + echo "\033[31mValidation failed:\033[0m\n"; + foreach ($errors as $property => $propertyErrors) { + foreach ($propertyErrors as $error) { + echo "\033[31m- {$property}: {$error['message']}\033[0m\n"; + } + } +} + +// 4. Test cases function +function runTestCases(Validator $validator): void +{ + // Test Case 1: Valid User + echo "\n\033[1mTest Case 1: Valid User\033[0m\n"; + $validUser = new User(); + $validUser->setName('Walmir Silva'); + $validUser->setEmail('walmir.silva@example.com'); + $validUser->setAge(25); + + $result = $validator->validate($validUser); + displayValidationResults($result->getErrors()); + + // Test Case 2: Invalid User (Short name, invalid email, underage) + echo "\n\033[1mTest Case 2: Invalid User\033[0m\n"; + $invalidUser = new User(); + $invalidUser->setName('Wa'); + $invalidUser->setEmail('walmir.silva.invalid'); + $invalidUser->setAge(16); + + $result = $validator->validate($invalidUser); + displayValidationResults($result->getErrors()); + + // Test Case 3: Empty User + echo "\n\033[1mTest Case 3: Empty User\033[0m\n"; + $emptyUser = new User(); + + $result = $validator->validate($emptyUser); + displayValidationResults($result->getErrors()); + + // Test Case 4: User with Extra Whitespace + echo "\n\033[1mTest Case 4: User with Extra Whitespace\033[0m\n"; + $whitespaceUser = new User(); + $whitespaceUser->setName(' Walmir Silva '); + $whitespaceUser->setEmail(' WALMIR.SILVA@EXAMPLE.COM '); + $whitespaceUser->setAge(30); + + $result = $validator->validate($whitespaceUser); + displayValidationResults($result->getErrors()); +} + +// 5. Main application execution +function main(): void +{ + try { + echo "\033[1mKaririCode Validator Demo\033[0m\n"; + echo "================================\n"; + + // Setup + $registry = setupValidatorRegistry(); + $validator = new Validator($registry); + + // Run test cases + runTestCases($validator); + } catch (Exception $e) { + echo "\033[31mError: {$e->getMessage()}\033[0m\n"; + echo "\033[33mStack trace:\033[0m\n"; + echo $e->getTraceAsString() . "\n"; + } +} + +// Run the application +main();