diff --git a/composer.json b/composer.json index 14c9109..0d6ac51 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,9 @@ "require": { "php": "^8.3", "kariricode/data-structure": "^1.1", - "kariricode/contract": "^2.7" + "kariricode/contract": "^2.7", + "kariricode/property-inspector": "^1.0", + "kariricode/exception": "^1.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 3248a4d..6a381a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "385a3ecdf968aedf7f9b9cf5a6451366", + "content-hash": "2a02a3529fa7de8a9bfe2c155aebcb29", "packages": [ { "name": "kariricode/contract", - "version": "v2.7.11", + "version": "v2.8.0", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b" + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/72c834a3afe2dbded8f6a7f96005635424636d4b", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/ee489bbcb44339a246af01058e00b3f94891f66c", + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c", "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-24T18:51:39+00:00" + "time": "2024-10-25T17:45:25+00:00" }, { "name": "kariricode/data-structure", @@ -141,6 +141,125 @@ "source": "https://github.com/KaririCode-Framework/kariricode-data-structure" }, "time": "2024-10-10T22:37:23+00:00" + }, + { + "name": "kariricode/exception", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-exception.git", + "reference": "ec5d9be5bda95e7d35ff3a230ec9afbf6f53c44d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-exception/zipball/ec5d9be5bda95e7d35ff3a230ec9afbf6f53c44d", + "reference": "ec5d9be5bda95e7d35ff3a230ec9afbf6f53c44d", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "enlightn/security-checker": "^2.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "KaririCode\\Exception\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "KaririCode Exception provides a robust and modular exception handling system for the KaririCode Framework, enabling seamless error management across various application domains.", + "homepage": "https://kariricode.org", + "keywords": [ + "error-management", + "exception-handling", + "framework", + "kariri-code", + "modular-exceptions", + "php-exceptions", + "php-framework" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-exception/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-exception" + }, + "time": "2024-10-25T18:13:01+00:00" + }, + { + "name": "kariricode/property-inspector", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-property-inspector.git", + "reference": "17910e63e0db9e8e59310462c36f0cd1b0fe5159" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/17910e63e0db9e8e59310462c36f0cd1b0fe5159", + "reference": "17910e63e0db9e8e59310462c36f0cd1b0fe5159", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "enlightn/security-checker": "^2.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "KaririCode\\PropertyInspector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "A robust and flexible data sanitization component for PHP, part of the KaririCode Framework, utilizing configurable processors and native functions.", + "homepage": "https://kariricode.org", + "keywords": [ + "attribute", + "dynamic-analysis", + "framework", + "inspection", + "kariri-code", + "metadata", + "normalization", + "object-properties", + "php8", + "property-inspector", + "reflection", + "validation" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-property-inspector" + }, + "time": "2024-10-14T15:18:05+00:00" } ], "packages-dev": [ diff --git a/src/Exception/ProcessingException.php b/src/Exception/ProcessingException.php index de45097..5b9512e 100644 --- a/src/Exception/ProcessingException.php +++ b/src/Exception/ProcessingException.php @@ -4,6 +4,30 @@ namespace KaririCode\ProcessorPipeline\Exception; -final class ProcessingException extends \RuntimeException +use KaririCode\Exception\AbstractException; + +final class ProcessingException extends AbstractException { + private const CODE_PIPELINE_FAILED = 3001; + private const CODE_PROCESSOR_FAILED = 3002; + + public static function pipelineExecutionFailed(): self + { + return self::createException( + self::CODE_PIPELINE_FAILED, + 'PIPELINE_FAILED', + 'Pipeline processing failed' + ); + } + + public static function processorExecutionFailed(string $processorClass): self + { + $message = sprintf('Processor %s execution failed', $processorClass); + + return self::createException( + self::CODE_PROCESSOR_FAILED, + 'PROCESSOR_FAILED', + $message + ); + } } diff --git a/src/Exception/ProcessorRuntimeException.php b/src/Exception/ProcessorRuntimeException.php new file mode 100644 index 0000000..7034206 --- /dev/null +++ b/src/Exception/ProcessorRuntimeException.php @@ -0,0 +1,62 @@ +results = new ProcessingResultCollection(); + } + + public function processPropertyValue(string $property, mixed $value): mixed + { + $pipeline = $this->builder->buildPipeline( + $this->identifier, + $this->getPropertyProcessors($property) + ); + + try { + $processedValue = $pipeline->process($value); + $this->storeProcessedValue($property, $processedValue); + + // Verifica se há erros de validação + $this->checkValidationErrors($property, $pipeline); + + return $processedValue; + } catch (\Exception $e) { + $this->storeProcessingError($property, $e); + + return $value; + } + } + + protected function checkValidationErrors(string $property, $pipeline): void + { + foreach ($pipeline->getProcessors() as $processor) { + if ($processor instanceof ValidatableProcessor && !$processor->isValid()) { + $this->storeValidationError( + $property, + $processor->getErrorKey(), + $processor->getErrorMessage() + ); + } + } + } + + protected function storeProcessedValue(string $property, mixed $value): void + { + $processedData = new ProcessedData($property, $value); + $this->results->addProcessedData($processedData); + } + + protected function storeProcessingError(string $property, \Exception $exception): void + { + $error = new ProcessingError( + $property, + 'processingError', + $exception->getMessage() + ); + $this->results->addError($error); + } + + protected function storeValidationError(string $property, string $errorKey, string $message): void + { + $error = new ProcessingError($property, $errorKey, $message); + $this->results->addError($error); + } + + public function getProcessingResults(): ProcessingResultCollection + { + return $this->results; + } + + public function getProcessedPropertyValues(): array + { + return $this->results->getProcessedDataAsArray(); + } + + public function getProcessingResultErrors(): array + { + return $this->results->getErrorsAsArray(); + } + + public function reset(): void + { + $this->results = new ProcessingResultCollection(); + } +} diff --git a/src/ProcessorBuilder.php b/src/ProcessorBuilder.php index ecab3da..385e195 100644 --- a/src/ProcessorBuilder.php +++ b/src/ProcessorBuilder.php @@ -1,5 +1,7 @@ $processorSpecs - */ public function buildPipeline(string $context, array $processorSpecs): Pipeline { $pipeline = new ProcessorPipeline(); diff --git a/src/ProcessorPipeline.php b/src/ProcessorPipeline.php index feb3b43..c459a6a 100644 --- a/src/ProcessorPipeline.php +++ b/src/ProcessorPipeline.php @@ -7,6 +7,7 @@ use KaririCode\Contract\Processor\Pipeline; use KaririCode\Contract\Processor\Processor; use KaririCode\Contract\Processor\ValidatableProcessor; +use KaririCode\ProcessorPipeline\Exception\ProcessingException; class ProcessorPipeline implements Pipeline { @@ -21,17 +22,48 @@ public function addProcessor(Processor $processor): self public function process(mixed $input): mixed { - return array_reduce( - $this->processors, - static function ($carry, Processor $processor): mixed { - // Reset the processor's state if it's a ValidatableProcessor - if ($processor instanceof ValidatableProcessor) { - $processor->reset(); - } - - return $processor->process($carry); - }, - $input - ); + try { + return array_reduce( + $this->processors, + $this->executeProcessor(...), + $input + ); + } catch (\Exception $e) { + throw ProcessingException::pipelineExecutionFailed(); + } + } + + public function getProcessors(): array + { + return $this->processors; + } + + public function hasProcessors(): bool + { + return !empty($this->processors); + } + + public function clear(): void + { + $this->processors = []; + } + + public function count(): int + { + return count($this->processors); + } + + private function executeProcessor(mixed $carry, Processor $processor): mixed + { + try { + // Reset the processor state if it's validatable + if ($processor instanceof ValidatableProcessor) { + $processor->reset(); + } + + return $processor->process($carry); + } catch (\Exception $e) { + throw ProcessingException::processorExecutionFailed($processor::class); + } } } diff --git a/src/ProcessorRegistry.php b/src/ProcessorRegistry.php index d8f2cea..02c09b7 100644 --- a/src/ProcessorRegistry.php +++ b/src/ProcessorRegistry.php @@ -8,11 +8,13 @@ use KaririCode\Contract\Processor\Processor; use KaririCode\Contract\Processor\ProcessorRegistry as ProcessorRegistryContract; use KaririCode\DataStructure\Map\HashMap; +use KaririCode\ProcessorPipeline\Exception\ProcessorRuntimeException; class ProcessorRegistry implements ProcessorRegistryContract { - public function __construct(private Map $processors = new HashMap()) - { + public function __construct( + private readonly Map $processors = new HashMap() + ) { } public function register(string $context, string $name, Processor $processor): void @@ -27,11 +29,12 @@ public function register(string $context, string $name, Processor $processor): v public function get(string $context, string $name): Processor { if (!$this->processors->containsKey($context)) { - throw new \RuntimeException("Context '$context' not found."); + throw ProcessorRuntimeException::contextNotFound($context); } + $contextMap = $this->processors->get($context); if (!$contextMap->containsKey($name)) { - throw new \RuntimeException("Processor '$name' not found in context '$context'."); + throw ProcessorRuntimeException::processorNotFound($name, $context); } return $contextMap->get($name); @@ -40,7 +43,7 @@ public function get(string $context, string $name): Processor public function getContextProcessors(string $context): Map { if (!$this->processors->containsKey($context)) { - throw new \RuntimeException("Context '$context' not found."); + throw ProcessorRuntimeException::contextNotFound($context); } return $this->processors->get($context); diff --git a/src/Result/ProcessedData.php b/src/Result/ProcessedData.php new file mode 100644 index 0000000..8061efd --- /dev/null +++ b/src/Result/ProcessedData.php @@ -0,0 +1,35 @@ +timestamp = time(); + } + + public function getProperty(): string + { + return $this->property; + } + + public function getValue(): mixed + { + return $this->value; + } + + public function toArray(): array + { + return [ + 'value' => $this->value, + 'timestamp' => $this->timestamp, + ]; + } +} diff --git a/src/Result/ProcessingError.php b/src/Result/ProcessingError.php new file mode 100644 index 0000000..e867917 --- /dev/null +++ b/src/Result/ProcessingError.php @@ -0,0 +1,54 @@ +hash = $this->generateHash(); + $this->timestamp = time(); + } + + private function generateHash(): string + { + return md5($this->property . $this->errorKey . $this->message); + } + + public function getHash(): string + { + return $this->hash; + } + + public function getProperty(): string + { + return $this->property; + } + + public function getErrorKey(): string + { + return $this->errorKey; + } + + public function getMessage(): string + { + return $this->message; + } + + public function toArray(): array + { + return [ + 'errorKey' => $this->errorKey, + 'message' => $this->message, + 'timestamp' => $this->timestamp, + ]; + } +} diff --git a/src/Result/ProcessingResultCollection.php b/src/Result/ProcessingResultCollection.php new file mode 100644 index 0000000..6275bf1 --- /dev/null +++ b/src/Result/ProcessingResultCollection.php @@ -0,0 +1,89 @@ +errors[$property])) { + $this->errors[$property] = []; + } + + $this->errors[$property][$error->getHash()] = $error; + } + + public function setProcessedData(string $property, mixed $value): void + { + $this->processedData[$property] = new ProcessedData($property, $value); + } + + public function hasErrors(): bool + { + return !empty($this->errors); + } + + public function getErrors(): array + { + $result = []; + foreach ($this->errors as $property => $propertyErrors) { + $result[$property] = array_values(array_map( + fn (ProcessingError $error) => [ + 'errorKey' => $error->getErrorKey(), + 'message' => $error->getMessage(), + ], + $propertyErrors + )); + } + + return $result; + } + + public function getProcessedData(): array + { + $result = []; + foreach ($this->processedData as $property => $data) { + $result[$property] = $data->getValue(); + } + + return $result; + } + + public function toArray(): array + { + return [ + 'isValid' => !$this->hasErrors(), + 'errors' => $this->getErrors(), + 'processedData' => $this->getProcessedData(), + ]; + } + + public function clear(): void + { + $this->processedData = []; + $this->errors = []; + } + + public function addProcessedData(ProcessedData $data): void + { + $this->processedData[$data->getProperty()] = $data; + } + + public function addProcessingError(ProcessingError $error): void + { + if (!isset($this->errors[$error->getProperty()])) { + $this->errors[$error->getProperty()] = []; + } + + $this->errors[$error->getProperty()][$error->getHash()] = $error; + } +} diff --git a/tests/ProcessorRegistryTest.php b/tests/ProcessorRegistryTest.php index 4f3742f..bf84409 100644 --- a/tests/ProcessorRegistryTest.php +++ b/tests/ProcessorRegistryTest.php @@ -6,13 +6,15 @@ use KaririCode\Contract\Processor\Processor; use KaririCode\DataStructure\Map\HashMap; +use KaririCode\ProcessorPipeline\Exception\ProcessorRuntimeException; use KaririCode\ProcessorPipeline\ProcessorRegistry; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; final class ProcessorRegistryTest extends TestCase { private ProcessorRegistry $registry; - private HashMap $mockHashMap; + private HashMap|MockObject $mockHashMap; protected function setUp(): void { @@ -27,23 +29,23 @@ public function testRegister(): void $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(false); $this->mockHashMap->expects($this->once()) ->method('put') - ->with('context', $this->isInstanceOf(HashMap::class)); + ->with('payment', $this->isInstanceOf(HashMap::class)); $this->mockHashMap->expects($this->once()) ->method('get') - ->with('context') + ->with('payment') ->willReturn($contextMap); $contextMap->expects($this->once()) ->method('put') - ->with('name', $processor); + ->with('validate', $processor); - $this->registry->register('context', 'name', $processor); + $this->registry->register('payment', 'validate', $processor); } public function testGet(): void @@ -53,25 +55,25 @@ public function testGet(): void $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(true); $this->mockHashMap->expects($this->once()) ->method('get') - ->with('context') + ->with('payment') ->willReturn($contextMap); $contextMap->expects($this->once()) ->method('containsKey') - ->with('name') + ->with('validate') ->willReturn(true); $contextMap->expects($this->once()) ->method('get') - ->with('name') + ->with('validate') ->willReturn($processor); - $result = $this->registry->get('context', 'name'); + $result = $this->registry->get('payment', 'validate'); $this->assertSame($processor, $result); } @@ -79,13 +81,13 @@ public function testGetContextNotFound(): void { $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(false); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Context 'context' not found."); + $this->expectException(ProcessorRuntimeException::class); + $this->expectExceptionMessage("Processor context 'payment' not found"); - $this->registry->get('context', 'name'); + $this->registry->get('payment', 'validate'); } public function testGetProcessorNotFound(): void @@ -94,23 +96,23 @@ public function testGetProcessorNotFound(): void $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(true); $this->mockHashMap->expects($this->once()) ->method('get') - ->with('context') + ->with('payment') ->willReturn($contextMap); $contextMap->expects($this->once()) ->method('containsKey') - ->with('name') + ->with('validate') ->willReturn(false); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Processor 'name' not found in context 'context'."); + $this->expectException(ProcessorRuntimeException::class); + $this->expectExceptionMessage("Processor 'validate' not found in context 'payment'"); - $this->registry->get('context', 'name'); + $this->registry->get('payment', 'validate'); } public function testGetContextProcessors(): void @@ -119,15 +121,15 @@ public function testGetContextProcessors(): void $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(true); $this->mockHashMap->expects($this->once()) ->method('get') - ->with('context') + ->with('payment') ->willReturn($contextMap); - $result = $this->registry->getContextProcessors('context'); + $result = $this->registry->getContextProcessors('payment'); $this->assertSame($contextMap, $result); } @@ -135,12 +137,12 @@ public function testGetContextProcessorsNotFound(): void { $this->mockHashMap->expects($this->once()) ->method('containsKey') - ->with('context') + ->with('payment') ->willReturn(false); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Context 'context' not found."); + $this->expectException(ProcessorRuntimeException::class); + $this->expectExceptionMessage("Processor context 'payment' not found"); - $this->registry->getContextProcessors('context'); + $this->registry->getContextProcessors('payment'); } }