diff --git a/composer.json b/composer.json index 0d6ac51..bd5e782 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,8 @@ "php": "^8.3", "kariricode/data-structure": "^1.1", "kariricode/contract": "^2.7", - "kariricode/property-inspector": "^1.0", - "kariricode/exception": "^1.2" + "kariricode/exception": "^1.2", + "kariricode/property-inspector": "^1.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 6a381a7..f90abd9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2a02a3529fa7de8a9bfe2c155aebcb29", + "content-hash": "6522d2cfeebe2bdb8cdc93c110a5a646", "packages": [ { "name": "kariricode/contract", @@ -201,19 +201,20 @@ }, { "name": "kariricode/property-inspector", - "version": "v1.0.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-property-inspector.git", - "reference": "17910e63e0db9e8e59310462c36f0cd1b0fe5159" + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/17910e63e0db9e8e59310462c36f0cd1b0fe5159", - "reference": "17910e63e0db9e8e59310462c36f0cd1b0fe5159", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/5faa6ca584ee80fbfc8de456377020703a88ab80", + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80", "shasum": "" }, "require": { + "kariricode/contract": "^2.7", "php": "^8.3" }, "require-dev": { @@ -259,7 +260,7 @@ "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" + "time": "2024-10-25T19:50:19+00:00" } ], "packages-dev": [ diff --git a/src/Contract/ProcessorConfigBuilder.php b/src/Contract/ProcessorConfigBuilder.php new file mode 100644 index 0000000..d0c94a7 --- /dev/null +++ b/src/Contract/ProcessorConfigBuilder.php @@ -0,0 +1,19 @@ +processAttribute($propertyName, $attribute, $value); + } catch (\Exception $e) { + $this->processingResultErrors[$propertyName][] = $e->getMessage(); + + return $value; + } + } + + private function processAttribute(string $propertyName, ProcessableAttribute $attribute, mixed $value): mixed + { + $config = $this->configBuilder->build($attribute); + $messages = []; + + if ($attribute instanceof CustomizableMessageAttribute) { + foreach ($config as $processorName => &$processorConfig) { + if ($message = $attribute->getMessage($processorName)) { + $processorConfig['customMessage'] = $message; + $messages[$processorName] = $message; + } + } + } + + $processedValue = $this->processValue($value, $config); + + if ($errors = $this->validateProcessors($config, $messages)) { + $this->processingResultErrors[$propertyName] = $errors; + } + + $this->processedPropertyValues[$propertyName] = [ + 'value' => $processedValue, + 'messages' => $messages, + ]; + + $this->processingResultMessages[$propertyName] = $messages; + + return $processedValue; + } + + private function validateProcessors(array $processorsConfig, array $messages): array + { + $errors = []; + foreach ($processorsConfig as $processorName => $config) { + // Simplify cache key to processor name + if (!isset($this->processorCache[$processorName])) { + $this->processorCache[$processorName] = $this->builder->build( + $this->processorType, + $processorName, + $config + ); + } + + $processor = $this->processorCache[$processorName]; + + if ($error = $this->validator->validate($processor, $processorName, $messages)) { + $errors[$processorName] = $error; + } + } + + return $errors; + } + + private function processValue(mixed $value, array $config): mixed + { + return $this->builder + ->buildPipeline($this->processorType, $config) + ->process($value); + } + + public function applyChanges(object $entity): void + { + foreach ($this->processedPropertyValues as $propertyName => $data) { + (new PropertyAccessor($entity, $propertyName))->setValue($data['value']); + } + } + + public function getProcessedPropertyValues(): array + { + return $this->processedPropertyValues; + } + + public function getProcessingResultErrors(): array + { + return $this->processingResultErrors; + } + + public function getProcessingResultMessages(): array + { + return $this->processingResultMessages; + } +} diff --git a/src/Handler/ProcessorAttributeHandler.php b/src/Handler/ProcessorAttributeHandler.php index c5f3770..2bd1349 100644 --- a/src/Handler/ProcessorAttributeHandler.php +++ b/src/Handler/ProcessorAttributeHandler.php @@ -5,19 +5,17 @@ namespace KaririCode\ProcessorPipeline\Handler; use KaririCode\Contract\Processor\ProcessorBuilder; -use KaririCode\Contract\Processor\ValidatableProcessor; -use KaririCode\ProcessorPipeline\Result\ProcessedData; -use KaririCode\ProcessorPipeline\Result\ProcessingError; +use KaririCode\ProcessorPipeline\AttributeHandler; +use KaririCode\ProcessorPipeline\Exception\ProcessorRuntimeException; use KaririCode\ProcessorPipeline\Result\ProcessingResultCollection; -use KaririCode\PropertyInspector\AttributeHandler; -class ProcessorAttributeHandler extends AttributeHandler +final class ProcessorAttributeHandler extends AttributeHandler { - protected ProcessingResultCollection $results; + private ProcessingResultCollection $results; public function __construct( - private readonly string $identifier, - private readonly ProcessorBuilder $builder + string $identifier, + ProcessorBuilder $builder ) { parent::__construct($identifier, $builder); $this->results = new ProcessingResultCollection(); @@ -25,59 +23,43 @@ public function __construct( public function processPropertyValue(string $property, mixed $value): mixed { - $pipeline = $this->builder->buildPipeline( - $this->identifier, - $this->getPropertyProcessors($property) - ); + $processorSpecs = $this->getPropertyProcessors($property); + + if (empty($processorSpecs)) { + return $value; + } try { - $processedValue = $pipeline->process($value); - $this->storeProcessedValue($property, $processedValue); + $pipeline = $this->builder->buildPipeline( + $this->identifier, + $processorSpecs + ); - // Verifica se há erros de validação - $this->checkValidationErrors($property, $pipeline); + $processedValue = $pipeline->process($value); + $this->results->setProcessedData($property, $processedValue); 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() - ); - } + throw ProcessorRuntimeException::processingFailed($property, $e); } } - protected function storeProcessedValue(string $property, mixed $value): void + public function getProcessedPropertyValues(): array { - $processedData = new ProcessedData($property, $value); - $this->results->addProcessedData($processedData); + return [ + 'values' => $this->results->getProcessedData(), + 'timestamp' => time(), + ]; } - protected function storeProcessingError(string $property, \Exception $exception): void + public function getProcessingResultErrors(): array { - $error = new ProcessingError( - $property, - 'processingError', - $exception->getMessage() - ); - $this->results->addError($error); + return $this->results->getErrors(); } - protected function storeValidationError(string $property, string $errorKey, string $message): void + public function hasErrors(): bool { - $error = new ProcessingError($property, $errorKey, $message); - $this->results->addError($error); + return $this->results->hasErrors(); } public function getProcessingResults(): ProcessingResultCollection @@ -85,16 +67,6 @@ 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/Processor/ProcessorConfigBuilder.php b/src/Processor/ProcessorConfigBuilder.php new file mode 100644 index 0000000..84909e6 --- /dev/null +++ b/src/Processor/ProcessorConfigBuilder.php @@ -0,0 +1,55 @@ +getProcessors(); + $processorsConfig = []; + + foreach ($processors as $key => $processor) { + if ($this->isSimpleProcessor($processor)) { + $processorsConfig[$processor] = $this->getDefaultProcessorConfig(); + } elseif ($this->isConfigurableProcessor($processor)) { + $processorName = $this->determineProcessorName($key, $processor); + $processorsConfig[$processorName] = $this->getProcessorConfig($processor); + } + } + + return $processorsConfig; + } + + private function isSimpleProcessor(mixed $processor): bool + { + return is_string($processor); + } + + private function isConfigurableProcessor(mixed $processor): bool + { + return is_array($processor); + } + + private function getDefaultProcessorConfig(): array + { + return []; + } + + private function determineProcessorName(string|int $key, array $processor): string + { + $nameNormalizer = new ProcessorNameNormalizer(); + + return $nameNormalizer->normalize($key, $processor); + } + + private function getProcessorConfig(array $processor): array + { + return $processor; + } +} diff --git a/src/Processor/ProcessorNameNormalizer.php b/src/Processor/ProcessorNameNormalizer.php new file mode 100644 index 0000000..c5fde05 --- /dev/null +++ b/src/Processor/ProcessorNameNormalizer.php @@ -0,0 +1,25 @@ +isNamedProcessor($key) ? (string) $key : $this->extractProcessorName($processor); + } + + private function isNamedProcessor(string|int $key): bool + { + return is_string($key); + } + + private function extractProcessorName(array $processor): string + { + $firstKey = array_key_first($processor); + + return is_string($firstKey) ? $firstKey : ''; + } +} diff --git a/src/Processor/ProcessorValidator.php b/src/Processor/ProcessorValidator.php new file mode 100644 index 0000000..b868ba4 --- /dev/null +++ b/src/Processor/ProcessorValidator.php @@ -0,0 +1,26 @@ +isValid()) { + $errorKey = $processor->getErrorKey(); + + return [ + 'errorKey' => $errorKey, + 'message' => $messages[$processorName] ?? "Validation failed for $processorName", + ]; + } + + return null; + } +} diff --git a/tests/AttributeHandlerTest.php b/tests/AttributeHandlerTest.php new file mode 100644 index 0000000..07a9203 --- /dev/null +++ b/tests/AttributeHandlerTest.php @@ -0,0 +1,236 @@ +processorBuilder = $this->createMock(ProcessorBuilder::class); + $this->processorValidator = $this->createMock(ProcessorValidator::class); + $this->configBuilder = $this->createMock(ProcessorConfigBuilder::class); + $this->attributeHandler = new AttributeHandler( + 'testProcessor', + $this->processorBuilder, + $this->processorValidator, + $this->configBuilder + ); + } + + public function testHandleAttributeProcessesValue(): void + { + $mockAttribute = $this->createMock(ProcessableAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => []]); + + $mockPipeline->expects($this->once()) + ->method('process') + ->with('initialValue') + ->willReturn('processedValue'); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->with($this->equalTo('testProcessor'), $this->equalTo(['processor1' => []])) + ->willReturn($mockPipeline); + + $this->processorValidator->expects($this->once()) + ->method('validate') + ->willReturn(null); + + $result = $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + + $this->assertSame('processedValue', $result); + } + + public function testHandleAttributeWithValidationError(): void + { + $mockAttribute = $this->createMock(ProcessableAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => []]); + + $mockPipeline->expects($this->once()) + ->method('process') + ->willReturn('processedValue'); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->willReturn($mockPipeline); + + $this->processorValidator->expects($this->once()) + ->method('validate') + ->willReturn(['errorKey' => 'testError', 'message' => 'Test error message']); + + $result = $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + + $this->assertSame('processedValue', $result); + + $errors = $this->attributeHandler->getProcessingResultErrors(); + $this->assertArrayHasKey('testProperty', $errors); + $this->assertArrayHasKey('processor1', $errors['testProperty']); + $this->assertEquals('testError', $errors['testProperty']['processor1']['errorKey']); + $this->assertEquals('Test error message', $errors['testProperty']['processor1']['message']); + } + + public function testHandleAttributeReturnsNullWhenAttributeNotProcessable(): void + { + $nonProcessableAttribute = new \stdClass(); + $result = $this->attributeHandler->handleAttribute('testProperty', $nonProcessableAttribute, 'initialValue'); + $this->assertNull($result); + } + + public function testApplyChangesSetsProcessedValues(): void + { + $mockEntity = new class { + public string $testProperty = 'originalValue'; + }; + + $mockAttribute = $this->createMock(ProcessableAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => []]); + + $mockPipeline->expects($this->once()) + ->method('process') + ->with('initialValue') + ->willReturn('processedValue'); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->willReturn($mockPipeline); + + $this->processorValidator->expects($this->once()) + ->method('validate') + ->willReturn(null); + + $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + $this->attributeHandler->applyChanges($mockEntity); + + $this->assertSame('processedValue', $mockEntity->testProperty); + } + + public function testGetProcessedPropertyValuesReturnsProcessedData(): void + { + $mockAttribute = $this->createMock(ProcessableAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => []]); + + $mockPipeline->expects($this->once()) + ->method('process') + ->with('initialValue') + ->willReturn('processedValue'); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->willReturn($mockPipeline); + + $this->processorValidator->expects($this->once()) + ->method('validate') + ->willReturn(null); + + $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + $processedValues = $this->attributeHandler->getProcessedPropertyValues(); + + $this->assertArrayHasKey('testProperty', $processedValues); + $this->assertIsArray($processedValues['testProperty']); + $this->assertArrayHasKey('value', $processedValues['testProperty']); + $this->assertArrayHasKey('messages', $processedValues['testProperty']); + $this->assertSame('processedValue', $processedValues['testProperty']['value']); + $this->assertIsArray($processedValues['testProperty']['messages']); + } + + public function testHandleAttributeWithCustomizableMessageAttribute(): void + { + $mockAttribute = $this->createMock(CombinedAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => ['option' => 'value']]); + + $mockAttribute->expects($this->once()) + ->method('getMessage') + ->with('processor1') + ->willReturn('Custom message'); + + $mockPipeline->method('process')->willReturn('processedValue'); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->willReturn($mockPipeline); + + $this->processorValidator->expects($this->once()) + ->method('validate') + ->willReturn(null); + + $result = $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + $this->assertSame('processedValue', $result); + + $processedValues = $this->attributeHandler->getProcessedPropertyValues(); + $this->assertArrayHasKey('testProperty', $processedValues); + $this->assertArrayHasKey('messages', $processedValues['testProperty']); + $this->assertArrayHasKey('processor1', $processedValues['testProperty']['messages']); + $this->assertEquals('Custom message', $processedValues['testProperty']['messages']['processor1']); + } + + public function testHandleAttributeWithProcessingException(): void + { + $mockAttribute = $this->createMock(ProcessableAttribute::class); + $mockPipeline = $this->createMock(Pipeline::class); + + $this->configBuilder->expects($this->once()) + ->method('build') + ->willReturn(['processor1' => []]); + + $mockPipeline->expects($this->once()) + ->method('process') + ->willThrowException( + ProcessorRuntimeException::contextNotFound('payment') + ); + + $this->processorBuilder->expects($this->once()) + ->method('buildPipeline') + ->willReturn($mockPipeline); + + $result = $this->attributeHandler->handleAttribute('testProperty', $mockAttribute, 'initialValue'); + $this->assertSame('initialValue', $result); + + $errors = $this->attributeHandler->getProcessingResultErrors(); + + $this->assertStringContainsString("Processor context 'payment' not found", $errors['testProperty'][0]); + } +} diff --git a/tests/Processor/ProcessorConfigBuilderTest.php b/tests/Processor/ProcessorConfigBuilderTest.php new file mode 100644 index 0000000..8a63801 --- /dev/null +++ b/tests/Processor/ProcessorConfigBuilderTest.php @@ -0,0 +1,63 @@ +configBuilder = new ProcessorConfigBuilder(); + } + + public function testBuildWithSimpleProcessors(): void + { + $attribute = $this->createMock(ProcessableAttribute::class); + $attribute->method('getProcessors')->willReturn(['processor1', 'processor2']); + + $result = $this->configBuilder->build($attribute); + + $this->assertEquals(['processor1' => [], 'processor2' => []], $result); + } + + public function testBuildWithConfigurableProcessors(): void + { + $attribute = $this->createMock(ProcessableAttribute::class); + $attribute->method('getProcessors')->willReturn([ + 'processor1' => ['option' => 'value'], + 'processor2' => ['another_option' => 'another_value'], + ]); + + $result = $this->configBuilder->build($attribute); + + $this->assertEquals([ + 'processor1' => ['option' => 'value'], + 'processor2' => ['another_option' => 'another_value'], + ], $result); + } + + public function testBuildWithMixedProcessors(): void + { + $attribute = $this->createMock(ProcessableAttribute::class); + $attribute->method('getProcessors')->willReturn([ + 'processor1', + 'processor2' => ['option' => 'value'], + ['unnamed_processor' => []], + ]); + + $result = $this->configBuilder->build($attribute); + + $this->assertEquals([ + 'processor1' => [], + 'processor2' => ['option' => 'value'], + 'unnamed_processor' => ['unnamed_processor' => []], + ], $result); + } +} diff --git a/tests/Processor/ProcessorNameNormalizerTest.php b/tests/Processor/ProcessorNameNormalizerTest.php new file mode 100644 index 0000000..cf500b9 --- /dev/null +++ b/tests/Processor/ProcessorNameNormalizerTest.php @@ -0,0 +1,39 @@ +normalizer = new ProcessorNameNormalizer(); + } + + public function testNormalizeWithStringKey(): void + { + $result = $this->normalizer->normalize('processor_name', []); + + $this->assertEquals('processor_name', $result); + } + + public function testNormalizeWithIntegerKey(): void + { + $result = $this->normalizer->normalize(0, ['processor_name' => []]); + + $this->assertEquals('processor_name', $result); + } + + public function testNormalizeWithEmptyProcessor(): void + { + $result = $this->normalizer->normalize(0, []); + + $this->assertEquals('', $result); + } +} diff --git a/tests/Processor/ProcessorValidatorTest.php b/tests/Processor/ProcessorValidatorTest.php new file mode 100644 index 0000000..1255246 --- /dev/null +++ b/tests/Processor/ProcessorValidatorTest.php @@ -0,0 +1,71 @@ +processorValidator = new ProcessorValidator(); + } + + public function testValidateWithNonValidatableProcessor(): void + { + $processor = $this->createMock(Processor::class); + + $result = $this->processorValidator->validate($processor, 'testProcessor', []); + + $this->assertNull($result); + } + + public function testValidateWithValidValidatableProcessor(): void + { + $processor = $this->createMock(ValidatableProcessor::class); + $processor->method('isValid')->willReturn(true); + + $result = $this->processorValidator->validate($processor, 'testProcessor', []); + + $this->assertNull($result); + } + + public function testValidateWithInvalidValidatableProcessor(): void + { + $processor = $this->createMock(ValidatableProcessor::class); + $processor->method('isValid')->willReturn(false); + $processor->method('getErrorKey')->willReturn('testError'); + + $result = $this->processorValidator->validate($processor, 'testProcessor', []); + + $this->assertIsArray($result); + $this->assertArrayHasKey('errorKey', $result); + $this->assertArrayHasKey('message', $result); + $this->assertEquals('testError', $result['errorKey']); + $this->assertEquals('Validation failed for testProcessor', $result['message']); + } + + public function testValidateWithInvalidValidatableProcessorAndCustomMessage(): void + { + $processor = $this->createMock(ValidatableProcessor::class); + $processor->method('isValid')->willReturn(false); + $processor->method('getErrorKey')->willReturn('testError'); + + $messages = ['testProcessor' => 'Custom error message']; + + $result = $this->processorValidator->validate($processor, 'testProcessor', $messages); + + $this->assertIsArray($result); + $this->assertArrayHasKey('errorKey', $result); + $this->assertArrayHasKey('message', $result); + $this->assertEquals('testError', $result['errorKey']); + $this->assertEquals('Custom error message', $result['message']); + } +} diff --git a/tests/ProcessorBuilderTest.php b/tests/ProcessorBuilderTest.php index 22dd95f..862c98c 100644 --- a/tests/ProcessorBuilderTest.php +++ b/tests/ProcessorBuilderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace KaririCode\Tests\ProcessorPipeline; +namespace KaririCode\ProcessorPipeline\Tests; use KaririCode\Contract\Processor\ConfigurableProcessor; use KaririCode\Contract\Processor\Pipeline; diff --git a/tests/benchmark_attribute_handler.php b/tests/benchmark_attribute_handler.php new file mode 100644 index 0000000..7512592 --- /dev/null +++ b/tests/benchmark_attribute_handler.php @@ -0,0 +1,536 @@ +builder = new MockProcessorBuilder(); + } + + public function run(): void + { + echo self::ANSI_BOLD . "\nATTRIBUTE HANDLER PERFORMANCE BENCHMARK\n" . self::ANSI_RESET; + echo str_repeat('=', 60) . "\n"; + echo self::ANSI_BLUE . 'Running benchmark with ' . self::ITERATIONS . ' iterations...' . self::ANSI_RESET . "\n\n"; + + // Warm up phase + $this->warmUp(); + + // Test original handler + $originalStats = $this->benchmarkOriginalHandler(); + + // Test optimized handler + $optimizedStats = $this->benchmarkOptimizedHandler(); + + // Display results + $this->displayResults($originalStats, $optimizedStats); + } + + private function warmUp(): void + { + echo self::ANSI_YELLOW . 'Warming up JIT compiler...' . self::ANSI_RESET . "\n"; + + for ($i = 0; $i < 1000; ++$i) { + $handler = new AttributeHandler('validator', $this->builder); + $this->runTestCase($handler); + + $handler = new AttributeHandlerOtimized('validator', $this->builder); + $this->runTestCase($handler); + } + + // Clear any accumulated memory + gc_collect_cycles(); + echo self::ANSI_GREEN . "Warm-up complete!\n\n" . self::ANSI_RESET; + } + + private function benchmarkOriginalHandler(): array + { + // Reset memory state + gc_collect_cycles(); + $startMemory = memory_get_usage(true); + + $handler = new AttributeHandler('validator', $this->builder); + $start = hrtime(true); + + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $this->runTestCase($handler); + } + + $time = (hrtime(true) - $start) / 1e+9; + $memoryUsed = memory_get_usage(true) - $startMemory; + + return [ + 'time' => $time, + 'memory' => $memoryUsed, + 'peak' => memory_get_peak_usage(true), + ]; + } + + private function benchmarkOptimizedHandler(): array + { + // Reset memory state + gc_collect_cycles(); + $startMemory = memory_get_usage(true); + + $handler = new AttributeHandlerOtimized('validator', $this->builder); + $start = hrtime(true); + + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $this->runTestCase($handler); + } + + $time = (hrtime(true) - $start) / 1e+9; + $memoryUsed = memory_get_usage(true) - $startMemory; + + return [ + 'time' => $time, + 'memory' => $memoryUsed, + 'peak' => memory_get_peak_usage(true), + ]; + } + + private function runTestCase($handler): void + { + $attribute = new class implements ProcessableAttribute { + public function getProcessors(): array + { + return [ + 'required', + 'email' => ['pattern' => '/.+@.+/'], + 'length' => ['min' => 5, 'max' => 50], + 'trim' => true, + 'lowercase' => true, + ]; + } + }; + + $testCases = [ + ['email', 'test@example.com'], + ['name', 'John Doe'], + ['age', 25], + ['description', str_repeat('a', 100)], + ['date', new \DateTime()], + ['empty', null], + ['whitespace', ' trimmed '], + ['special', '!@#$%^&*()'], + ['unicode', 'αβγδε'], + ['number_string', '12345'], + ]; + + foreach ($testCases as [$property, $value]) { + $handler->handleAttribute($property, $attribute, $value); + } + + $handler->getProcessingResultMessages(); + $handler->getProcessedPropertyValues(); + $handler->getProcessingResultErrors(); + } + + private function displayResults(array $originalStats, array $optimizedStats): void + { + echo self::ANSI_BOLD . "Performance Results\n" . self::ANSI_RESET; + echo str_repeat('=', 60) . "\n"; + + // Time Performance + $timeDiff = $this->calculatePercentageDiff($originalStats['time'], $optimizedStats['time']); + $timeColor = $timeDiff > 0 ? self::ANSI_GREEN : self::ANSI_RED; + + echo self::ANSI_BOLD . "Execution Time\n" . self::ANSI_RESET; + echo str_repeat('-', 40) . "\n"; + echo sprintf("%sOriginal Handler: %.6f seconds%s\n", self::ANSI_YELLOW, $originalStats['time'], self::ANSI_RESET); + echo sprintf("%sOptimized Handler: %.6f seconds%s\n", self::ANSI_YELLOW, $optimizedStats['time'], self::ANSI_RESET); + echo sprintf( + "%sTime Difference: %.2f%% %s%s\n\n", + $timeColor, + abs($timeDiff), + $timeDiff > 0 ? 'faster' : 'slower', + self::ANSI_RESET + ); + + // Memory Usage + echo self::ANSI_BOLD . "Memory Usage\n" . self::ANSI_RESET; + echo str_repeat('-', 40) . "\n"; + + $originalMemoryMB = $originalStats['memory'] / 1024 / 1024; + $optimizedMemoryMB = $optimizedStats['memory'] / 1024 / 1024; + $memoryDiff = $this->calculatePercentageDiff($originalStats['memory'], $optimizedStats['memory']); + $memoryColor = $memoryDiff > 0 ? self::ANSI_GREEN : self::ANSI_RED; + + echo sprintf("%sOriginal Handler: %.2f MB%s\n", self::ANSI_YELLOW, $originalMemoryMB, self::ANSI_RESET); + echo sprintf("%sOptimized Handler: %.2f MB%s\n", self::ANSI_YELLOW, $optimizedMemoryMB, self::ANSI_RESET); + echo sprintf( + "%sMemory Difference: %.2f%% %s%s\n\n", + $memoryColor, + abs($memoryDiff), + $memoryDiff > 0 ? 'less' : 'more', + self::ANSI_RESET + ); + + // Peak Memory + echo self::ANSI_BOLD . "Peak Memory Usage\n" . self::ANSI_RESET; + echo str_repeat('-', 40) . "\n"; + + $originalPeakMB = $originalStats['peak'] / 1024 / 1024; + $optimizedPeakMB = $optimizedStats['peak'] / 1024 / 1024; + $peakDiff = $this->calculatePercentageDiff($originalStats['peak'], $optimizedStats['peak']); + $peakColor = $peakDiff > 0 ? self::ANSI_GREEN : self::ANSI_RED; + + echo sprintf("%sOriginal Peak: %.2f MB%s\n", self::ANSI_YELLOW, $originalPeakMB, self::ANSI_RESET); + echo sprintf("%sOptimized Peak: %.2f MB%s\n", self::ANSI_YELLOW, $optimizedPeakMB, self::ANSI_RESET); + echo sprintf( + "%sPeak Difference: %.2f%% %s%s\n\n", + $peakColor, + abs($peakDiff), + $peakDiff > 0 ? 'less' : 'more', + self::ANSI_RESET + ); + + // Per Iteration Stats + echo self::ANSI_BOLD . "Per Iteration Stats\n" . self::ANSI_RESET; + echo str_repeat('-', 40) . "\n"; + + $originalTimePerIteration = ($originalStats['time'] * 1000) / self::ITERATIONS; + $optimizedTimePerIteration = ($optimizedStats['time'] * 1000) / self::ITERATIONS; + + echo sprintf( + "%sOriginal Time per Iteration: %.6f ms%s\n", + self::ANSI_YELLOW, + $originalTimePerIteration, + self::ANSI_RESET + ); + echo sprintf( + "%sOptimized Time per Iteration: %.6f ms%s\n", + self::ANSI_YELLOW, + $optimizedTimePerIteration, + self::ANSI_RESET + ); + + echo "\n" . str_repeat('=', 60) . "\n"; + } + + private function calculatePercentageDiff(float $original, float $optimized): float + { + if ($original <= 0) { + return 0; + } + + return (($original - $optimized) / $original) * 100; + } +} + +final class AttributeHandlerOtimized implements PropertyAttributeHandler, PropertyChangeApplier +{ + private array $processedPropertyValues = []; + private array $processingResultErrors = []; + private array $processingResultMessages = []; + private array $processorCache = []; + + public function __construct( + private readonly string $processorType, + private readonly ProcessorBuilder $builder, + private readonly ProcessorProcessorContract $validator = new ProcessorValidator(), + private readonly ProcessorConfigBuilderContract $configBuilder = new ProcessorConfigBuilder() + ) { + } + + public function handleAttribute(string $propertyName, object $attribute, mixed $value): mixed + { + if (!$attribute instanceof ProcessableAttribute) { + return null; + } + + try { + return $this->processAttribute($propertyName, $attribute, $value); + } catch (\Exception $e) { + $this->processingResultErrors[$propertyName][] = $e->getMessage(); + + return $value; + } + } + + private function processAttribute(string $propertyName, ProcessableAttribute $attribute, mixed $value): mixed + { + $config = $this->configBuilder->build($attribute); + $messages = []; + + if ($attribute instanceof CustomizableMessageAttribute) { + foreach ($config as $processorName => &$processorConfig) { + if ($message = $attribute->getMessage($processorName)) { + $processorConfig['customMessage'] = $message; + $messages[$processorName] = $message; + } + } + } + + $processedValue = $this->processValue($value, $config); + + if ($errors = $this->validateProcessors($config, $messages)) { + $this->processingResultErrors[$propertyName] = $errors; + } + + $this->processedPropertyValues[$propertyName] = [ + 'value' => $processedValue, + 'messages' => $messages, + ]; + + $this->processingResultMessages[$propertyName] = $messages; + + return $processedValue; + } + + private function validateProcessors(array $processorsConfig, array $messages): array + { + $errors = []; + foreach ($processorsConfig as $processorName => $config) { + // Simplify cache key to processor name + if (!isset($this->processorCache[$processorName])) { + $this->processorCache[$processorName] = $this->builder->build( + $this->processorType, + $processorName, + $config + ); + } + + $processor = $this->processorCache[$processorName]; + + if ($error = $this->validator->validate($processor, $processorName, $messages)) { + $errors[$processorName] = $error; + } + } + + return $errors; + } + + private function processValue(mixed $value, array $config): mixed + { + return $this->builder + ->buildPipeline($this->processorType, $config) + ->process($value); + } + + public function applyChanges(object $entity): void + { + foreach ($this->processedPropertyValues as $propertyName => $data) { + (new PropertyAccessor($entity, $propertyName))->setValue($data['value']); + } + } + + public function getProcessedPropertyValues(): array + { + return $this->processedPropertyValues; + } + + public function getProcessingResultErrors(): array + { + return $this->processingResultErrors; + } + + public function getProcessingResultMessages(): array + { + return $this->processingResultMessages; + } +} + +// Before +// class AttributeHandler implements PropertyAttributeHandler, PropertyChangeApplier +// { +// private array $processedPropertyValues = []; +// private array $processingResultErrors = []; +// private array $processingResultMessages = []; + +// public function __construct( +// private readonly string $processorType, +// private readonly ProcessorBuilder $builder, +// private readonly ProcessorProcessorContract $validator = new ProcessorValidator(), +// private readonly ProcessorConfigBuilderContract $configBuilder = new ProcessorConfigBuilder() +// ) { +// } + +// public function handleAttribute(string $propertyName, object $attribute, mixed $value): mixed +// { +// if (!$attribute instanceof ProcessableAttribute) { +// return null; +// } + +// $processorsConfig = $this->configBuilder->build($attribute); +// $messages = $this->extractCustomMessages($attribute, $processorsConfig); + +// try { +// $processedValue = $this->processValue($value, $processorsConfig); +// $errors = $this->validateProcessors($processorsConfig, $messages); + +// $this->storeProcessedPropertyValue($propertyName, $processedValue, $messages); + +// if (!empty($errors)) { +// $this->storeProcessingResultErrors($propertyName, $errors); +// } + +// return $processedValue; +// } catch (\Exception $e) { +// $this->storeProcessingResultError($propertyName, $e->getMessage()); + +// return $value; +// } +// } + +// private function validateProcessors(array $processorsConfig, array $messages): array +// { +// $errors = []; +// foreach ($processorsConfig as $processorName => $config) { +// $processor = $this->builder->build($this->processorType, $processorName, $config); +// $validationError = $this->validator->validate( +// $processor, +// $processorName, +// $messages +// ); + +// if ($this->shouldAddValidationError($validationError, $errors, $processorName)) { +// $errors[$processorName] = $validationError; +// } +// } + +// return $errors; +// } + +// private function shouldAddValidationError(?array $validationError, array $errors, string $processorName): bool +// { +// return null !== $validationError && !isset($errors[$processorName]); +// } + +// private function storeProcessingResultErrors(string $propertyName, array $errors): void +// { +// $this->processingResultErrors[$propertyName] = $errors; +// } + +// private function extractCustomMessages(ProcessableAttribute $attribute, array &$processorsConfig): array +// { +// $messages = []; +// if ($attribute instanceof CustomizableMessageAttribute) { +// foreach ($processorsConfig as $processorName => &$config) { +// $customMessage = $attribute->getMessage($processorName); +// if (null !== $customMessage) { +// $config['customMessage'] = $customMessage; +// $messages[$processorName] = $customMessage; +// } +// } +// } + +// return $messages; +// } + +// private function processValue(mixed $value, array $processorsConfig): mixed +// { +// $pipeline = $this->builder->buildPipeline( +// $this->processorType, +// $processorsConfig +// ); + +// return $pipeline->process($value); +// } + +// private function storeProcessedPropertyValue(string $propertyName, mixed $processedValue, array $messages): void +// { +// $this->processedPropertyValues[$propertyName] = [ +// 'value' => $processedValue, +// 'messages' => $messages, +// ]; +// $this->processingResultMessages[$propertyName] = $messages; +// } + +// private function storeProcessingResultError(string $propertyName, string $errorMessage): void +// { +// $this->processingResultErrors[$propertyName][] = $errorMessage; +// } + +// public function applyChanges(object $entity): void +// { +// foreach ($this->processedPropertyValues as $propertyName => $data) { +// (new PropertyAccessor($entity, $propertyName))->setValue($data['value']); +// } +// } + +// public function getProcessedPropertyValues(): array +// { +// return $this->processedPropertyValues; +// } + +// public function getProcessingResultErrors(): array +// { +// return $this->processingResultErrors; +// } + +// public function getProcessingResultMessages(): array +// { +// return $this->processingResultMessages; +// } +// } + +$benchmark = new BenchmarkRunner(); +$benchmark->run();