diff --git a/composer.json b/composer.json index 0f9b5b0..abc4779 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,8 @@ "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^11.0", "squizlabs/php_codesniffer": "^3.9", - "enlightn/security-checker": "^2.0" + "enlightn/security-checker": "^2.0", + "kariricode/validator": "^1.0" }, "support": { "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues", diff --git a/composer.lock b/composer.lock index f93be26..cfd5af6 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": "fc6bb777fb8ffdc7815f7962441ad228", + "content-hash": "789260e11a472e31aeb935652229ca59", "packages": [ { "name": "kariricode/contract", @@ -1095,6 +1095,126 @@ ], "time": "2024-07-18T11:15:46+00:00" }, + { + "name": "kariricode/exception", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-exception.git", + "reference": "65c8eb72c581eb8c33c168e5df104ed260843303" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-exception/zipball/65c8eb72c581eb8c33c168e5df104ed260843303", + "reference": "65c8eb72c581eb8c33c168e5df104ed260843303", + "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-17T22:43:32+00:00" + }, + { + "name": "kariricode/validator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-validator.git", + "reference": "885b4b157983bf601f99efda85ddd4052256db97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-validator/zipball/885b4b157983bf601f99efda85ddd4052256db97", + "reference": "885b4b157983bf601f99efda85ddd4052256db97", + "shasum": "" + }, + "require": { + "kariricode/contract": "^2.7", + "kariricode/exception": "^1.0", + "kariricode/processor-pipeline": "^1.1", + "kariricode/property-inspector": "^1.0", + "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\\Validator\\": "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": [ + "KaririCode", + "attribute", + "data", + "entity", + "php", + "pipeline", + "processor", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-validator/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-validator" + }, + "time": "2024-10-23T22:10:44+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.12.0", diff --git a/src/AttributeAnalyzer.php b/src/AttributeAnalyzer.php index 47f4974..6f7af95 100644 --- a/src/AttributeAnalyzer.php +++ b/src/AttributeAnalyzer.php @@ -9,6 +9,8 @@ final class AttributeAnalyzer implements AttributeAnalyzerContract { + private array $cache = []; + public function __construct(private readonly string $attributeClass) { } @@ -16,17 +18,14 @@ public function __construct(private readonly string $attributeClass) public function analyzeObject(object $object): array { try { - $results = []; - $reflection = new \ReflectionClass($object); - - foreach ($reflection->getProperties() as $property) { - $propertyResult = $this->analyzeProperty($object, $property); - if (null !== $propertyResult) { - $results[$property->getName()] = $propertyResult; - } + $className = $object::class; + + // Usar cache se disponível + if (!isset($this->cache[$className])) { + $this->cacheObjectMetadata($object); } - return $results; + return $this->extractValues($object); } catch (\ReflectionException $e) { throw new PropertyInspectionException('Failed to analyze object: ' . $e->getMessage(), 0, $e); } catch (\Error $e) { @@ -34,24 +33,49 @@ public function analyzeObject(object $object): array } } - private function analyzeProperty(object $object, \ReflectionProperty $property): ?array + private function cacheObjectMetadata(object $object): void { - $attributes = $property->getAttributes($this->attributeClass, \ReflectionAttribute::IS_INSTANCEOF); - if (empty($attributes)) { - return null; + $className = $object::class; + $reflection = new \ReflectionClass($object); + $cachedProperties = []; + + foreach ($reflection->getProperties() as $property) { + $attributes = $property->getAttributes($this->attributeClass, \ReflectionAttribute::IS_INSTANCEOF); + + if (!empty($attributes)) { + $property->setAccessible(true); + $attributeInstances = array_map( + static fn (\ReflectionAttribute $attr): object => $attr->newInstance(), + $attributes + ); + + $cachedProperties[$property->getName()] = [ + 'attributes' => $attributeInstances, + 'property' => $property, + ]; + } } - $property->setAccessible(true); - $propertyValue = $property->getValue($object); + $this->cache[$className] = $cachedProperties; + } - $attributeInstances = array_map( - static fn (\ReflectionAttribute $attr): object => $attr->newInstance(), - $attributes - ); + private function extractValues(object $object): array + { + $results = []; + $className = $object::class; + + foreach ($this->cache[$className] as $propertyName => $data) { + $results[$propertyName] = [ + 'value' => $data['property']->getValue($object), + 'attributes' => $data['attributes'], + ]; + } - return [ - 'value' => $propertyValue, - 'attributes' => $attributeInstances, - ]; + return $results; + } + + public function clearCache(): void + { + $this->cache = []; } } diff --git a/src/AttributeHandler.php b/src/AttributeHandler.php index 7ed6f77..297e101 100644 --- a/src/AttributeHandler.php +++ b/src/AttributeHandler.php @@ -7,6 +7,8 @@ use KaririCode\Contract\Processor\Attribute\CustomizableMessageAttribute; use KaririCode\Contract\Processor\Attribute\ProcessableAttribute; use KaririCode\Contract\Processor\ProcessorBuilder; +use KaririCode\Contract\Processor\ProcessorValidator as ProcessorProcessorContract; +use KaririCode\PropertyInspector\Contract\ProcessorConfigBuilder as ProcessorConfigBuilderContract; use KaririCode\PropertyInspector\Contract\PropertyAttributeHandler; use KaririCode\PropertyInspector\Contract\PropertyChangeApplier; use KaririCode\PropertyInspector\Processor\ProcessorConfigBuilder; @@ -18,12 +20,13 @@ class AttributeHandler implements PropertyAttributeHandler, PropertyChangeApplie 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 ProcessorValidator $validator = new ProcessorValidator(), - private readonly ProcessorConfigBuilder $configBuilder = new ProcessorConfigBuilder() + private readonly ProcessorProcessorContract $validator = new ProcessorValidator(), + private readonly ProcessorConfigBuilderContract $configBuilder = new ProcessorConfigBuilder() ) { } @@ -33,101 +36,79 @@ public function handleAttribute(string $propertyName, object $attribute, mixed $ 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; + return $this->processAttribute($propertyName, $attribute, $value); } catch (\Exception $e) { - $this->storeProcessingResultError($propertyName, $e->getMessage()); + $this->processingResultErrors[$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 + private function processAttribute(string $propertyName, ProcessableAttribute $attribute, mixed $value): mixed { + $config = $this->configBuilder->build($attribute); $messages = []; + if ($attribute instanceof CustomizableMessageAttribute) { - foreach ($processorsConfig as $processorName => &$config) { - $customMessage = $attribute->getMessage($processorName); - if (null !== $customMessage) { - $config['customMessage'] = $customMessage; - $messages[$processorName] = $customMessage; + foreach ($config as $processorName => &$processorConfig) { + if ($message = $attribute->getMessage($processorName)) { + $processorConfig['customMessage'] = $message; + $messages[$processorName] = $message; } } } - return $messages; - } - - private function processValue(mixed $value, array $processorsConfig): mixed - { - $pipeline = $this->builder->buildPipeline( - $this->processorType, - $processorsConfig - ); + $processedValue = $this->processValue($value, $config); - return $pipeline->process($value); - } + if ($errors = $this->validateProcessors($config, $messages)) { + $this->processingResultErrors[$propertyName] = $errors; + } - private function storeProcessedPropertyValue(string $propertyName, mixed $processedValue, array $messages): void - { $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 storeProcessingResultError(string $propertyName, string $errorMessage): void + private function processValue(mixed $value, array $config): mixed { - $this->processingResultErrors[$propertyName][] = $errorMessage; + return $this->builder + ->buildPipeline($this->processorType, $config) + ->process($value); } public function applyChanges(object $entity): void { foreach ($this->processedPropertyValues as $propertyName => $data) { - $accessor = new PropertyAccessor($entity, $propertyName); - $accessor->setValue($data['value']); + (new PropertyAccessor($entity, $propertyName))->setValue($data['value']); } } diff --git a/src/Contract/ProcessorConfigBuilder.php b/src/Contract/ProcessorConfigBuilder.php new file mode 100644 index 0000000..cc61196 --- /dev/null +++ b/src/Contract/ProcessorConfigBuilder.php @@ -0,0 +1,19 @@ + ['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 ) { } -} -// Custom Attribute Handler -final class CustomAttributeHandler implements PropertyAttributeHandler -{ - public function handleAttribute(object $object, string $propertyName, object $attribute, mixed $value): ?string + // Getters and setters + public function getName(): string { - return match (true) { - $attribute instanceof Validate => $this->validate($propertyName, $value, $attribute->rules), - $attribute instanceof Sanitize => $this->sanitize($propertyName, $value, $attribute->method), - default => null, - }; + return $this->name; } - private function validate(string $propertyName, mixed $value, array $rules): ?string + public function setName(string $name): void { - $errors = array_filter(array_map( - fn ($rule) => $this->applyValidationRule($propertyName, $value, $rule), - $rules - )); - - return empty($errors) ? null : implode(' ', $errors); + $this->name = $name; } - private function applyValidationRule(string $propertyName, mixed $value, string $rule): ?string + public function getEmail(): string { - return match (true) { - 'required' === $rule && empty($value) => "$propertyName is required.", - 'string' === $rule && !is_string($value) => "$propertyName must be a string.", - str_starts_with($rule, 'min:') => $this->validateMinRule($propertyName, $value, $rule), - 'email' === $rule && !filter_var($value, FILTER_VALIDATE_EMAIL) => "$propertyName must be a valid email address.", - 'integer' === $rule && !is_int($value) => "$propertyName must be an integer.", - default => null, - }; + return $this->email; } - private function validateMinRule(string $propertyName, mixed $value, string $rule): ?string + public function setEmail(string $email): void { - $minValue = (int) substr($rule, 4); - - return match (true) { - is_string($value) && strlen($value) < $minValue => "$propertyName must be at least $minValue characters long.", - is_int($value) && $value < $minValue => "$propertyName must be at least $minValue.", - default => null, - }; + $this->email = $email; } - private function sanitize(string $propertyName, mixed $value, string $method): string + public function getAge(): int { - return match ($method) { - 'trim' => trim($value), - 'lowercase' => strtolower($value), - default => (string) $value, - }; + return $this->age; } -} - -function runApplication(): void -{ - $attributeAnalyzer = new AttributeAnalyzer(Validate::class); - $propertyInspector = new PropertyInspector($attributeAnalyzer); - $handler = new CustomAttributeHandler(); - - // Scenario 1: Valid User - $validUser = new User(' WaLmir Silva ', 'WALMIR.SILVA@EXAMPLE.COM', 25); - processUser($propertyInspector, $handler, $validUser, 'Scenario 1: Valid User'); - // Scenario 2: Invalid User (Age below 18) - $underageUser = new User('Walmir Silva', 'walmir@example.com', 16); - processUser($propertyInspector, $handler, $underageUser, 'Scenario 2: Underage User'); - - // Scenario 3: Invalid User (Empty name and invalid email) - $invalidUser = new User('', 'invalid-email', 30); - processUser($propertyInspector, $handler, $invalidUser, 'Scenario 3: Invalid User Data'); - - // Scenario 4: Non-existent Attribute (to trigger an exception) - try { - $invalidAttributeAnalyzer = new AttributeAnalyzer('NonExistentAttribute'); - $invalidPropertyInspector = new PropertyInspector($invalidAttributeAnalyzer); - $invalidPropertyInspector->inspect($validUser, $handler); - } catch (PropertyInspectionException $e) { - echo "\nScenario 4: Non-existent Attribute\n"; - echo 'Error: ' . $e->getMessage() . "\n"; + public function setAge(int $age): void + { + $this->age = $age; } } -function processUser(PropertyInspector $inspector, PropertyAttributeHandler $handler, User $user, string $scenario): void +// 2. Set up the validator registry +function setupValidatorRegistry(): ProcessorRegistry { - echo "\n$scenario\n"; - echo 'Original User: ' . json_encode($user) . "\n"; + $registry = new ProcessorRegistry(); - try { - $results = $inspector->inspect($user, $handler); - displayResults($results); - - if (empty($results)) { - sanitizeUser($user); - displaySanitizedUser($user); - } else { - echo "Validation failed. User was not sanitized.\n"; - } - } catch (PropertyInspectionException $e) { - echo "An error occurred during property inspection: {$e->getMessage()}\n"; - } + // 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; } -function displayResults(array $results): void +// 3. Helper function to display validation results +function displayValidationResults(array $errors): void { - if (empty($results)) { - echo "All properties are valid.\n"; + if (empty($errors)) { + echo "\033[32mValidation passed successfully!\033[0m\n"; return; } - echo "Validation Results:\n"; - foreach ($results as $propertyName => $propertyResults) { - echo "Property: $propertyName\n"; - foreach ($propertyResults as $result) { - if (null !== $result) { - echo " - $result\n"; - } + 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"; } } } -function sanitizeUser(User $user): void +// 4. Test cases function +function runTestCases(Validator $validator): void { - $user->name = trim($user->name); - $user->email = strtolower($user->email); + // 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()); } -function displaySanitizedUser(User $user): void +// 5. Main application execution +function main(): void { - echo "Sanitized User:\n"; - echo json_encode($user) . "\n"; + 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 -runApplication(); +main(); diff --git a/tests/benchmark_attribute_analyzerphp b/tests/benchmark_attribute_analyzerphp new file mode 100644 index 0000000..39020de --- /dev/null +++ b/tests/benchmark_attribute_analyzerphp @@ -0,0 +1,351 @@ +originalAnalyzer = new AttributeAnalyzer(TestAttribute::class); + $this->optimizedAnalyzer = new OptimizedAttributeAnalyzer(TestAttribute::class); + $this->testObjects = array_fill(0, self::ITERATIONS, new TestClass()); + } + + public function runBenchmark(): void + { + $results = [ + 'Memory Usage' => $this->benchmarkMemoryUsage(), + 'Processing Time' => $this->benchmarkProcessingTime(), + 'Property Access' => $this->benchmarkPropertyAccess(), + ]; + + $this->printResults($results); + } + + private function calculateDifference(float $original, float $optimized): float + { + if ($original === 0.0) { + return $optimized === 0.0 ? 0.0 : 100.0; + } + + return (($original - $optimized) / $original) * 100; + } + + private function benchmarkMemoryUsage(): array + { + // Limpar cache e garbage collector antes de começar + gc_collect_cycles(); + + // Original Version + $startMemory = memory_get_usage(true); + foreach ($this->testObjects as $object) { + $this->originalAnalyzer->analyzeObject($object); + } + $originalMemory = memory_get_usage(true) - $startMemory; + + // Limpar entre testes + gc_collect_cycles(); + + // Optimized Version + $startMemory = memory_get_usage(true); + foreach ($this->testObjects as $object) { + $this->optimizedAnalyzer->analyzeObject($object); + } + $optimizedMemory = memory_get_usage(true) - $startMemory; + + $difference = $this->calculateDifference($originalMemory, $optimizedMemory); + + return [ + 'Original' => number_format($originalMemory / 1024, 2), + 'Optimized' => number_format($optimizedMemory / 1024, 2), + 'Difference' => number_format(abs($difference), 2), + 'Improvement' => $difference > 0 ? 'Yes' : 'No' + ]; + } + + private function benchmarkProcessingTime(): array + { + // Aquecimento + foreach ($this->testObjects as $object) { + $this->originalAnalyzer->analyzeObject($object); + $this->optimizedAnalyzer->analyzeObject($object); + } + + // Original Version + $start = microtime(true); + foreach ($this->testObjects as $object) { + $this->originalAnalyzer->analyzeObject($object); + } + $originalTime = microtime(true) - $start; + + // Optimized Version + $start = microtime(true); + foreach ($this->testObjects as $object) { + $this->optimizedAnalyzer->analyzeObject($object); + } + $optimizedTime = microtime(true) - $start; + + $difference = $this->calculateDifference($originalTime, $optimizedTime); + + return [ + 'Original' => number_format($originalTime * 1000, 4), + 'Optimized' => number_format($optimizedTime * 1000, 4), + 'Difference' => number_format(abs($difference), 2), + 'Improvement' => $difference > 0 ? 'Yes' : 'No' + ]; + } + + private function benchmarkPropertyAccess(): array + { + $object = new TestClass(); + + // Aquecimento + for ($i = 0; $i < 1000; $i++) { + $this->originalAnalyzer->analyzeObject($object); + $this->optimizedAnalyzer->analyzeObject($object); + } + + // Original Version + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + $this->originalAnalyzer->analyzeObject($object); + } + $originalTime = microtime(true) - $start; + + // Optimized Version + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + $this->optimizedAnalyzer->analyzeObject($object); + } + $optimizedTime = microtime(true) - $start; + + $difference = $this->calculateDifference($originalTime, $optimizedTime); + + return [ + 'Original' => number_format($originalTime * 1000, 4), + 'Optimized' => number_format($optimizedTime * 1000, 4), + 'Difference' => number_format(abs($difference), 2), + 'Improvement' => $difference > 0 ? 'Yes' : 'No' + ]; + } + + private function printResults(array $results): void + { + echo self::ANSI_BOLD . "\nATTRIBUTE ANALYZER BENCHMARK\n" . self::ANSI_RESET; + echo str_repeat('=', 50) . "\n\n"; + + foreach ($results as $testName => $data) { + echo self::ANSI_BOLD . "$testName\n" . self::ANSI_RESET; + echo str_repeat('-', 30) . "\n"; + + foreach ($data as $metric => $value) { + if ($metric === 'Improvement') { + continue; + } + + $unit = $metric === 'Difference' ? '%' : ($testName === 'Memory Usage' ? ' KB' : ' ms'); + $color = $this->getMetricColor($metric, $data); + + echo sprintf( + "%s%-15s: %s%s%s\n", + $color, + $metric, + $value, + $unit, + self::ANSI_RESET + ); + } + + echo sprintf( + "\n%s%s%s\n\n", + $data['Improvement'] === 'Yes' ? self::ANSI_GREEN : self::ANSI_RED, + "Improvement: " . ($data['Improvement'] === 'Yes' ? 'Yes' : 'No'), + self::ANSI_RESET + ); + } + + echo str_repeat('=', 50) . "\n"; + } + + private function getMetricColor(string $metric, array $data): string + { + if ($metric === 'Difference') { + return self::ANSI_YELLOW; + } + + if ($data['Improvement'] === 'Yes') { + return $metric === 'Optimized' ? self::ANSI_GREEN : self::ANSI_RED; + } + + return $metric === 'Original' ? self::ANSI_GREEN : self::ANSI_RED; + } +} + + +final class OptimizedAttributeAnalyzer implements AttributeAnalyzerContract +{ + private array $cache = []; + + public function __construct(private readonly string $attributeClass) + { + } + + public function analyzeObject(object $object): array + { + try { + $className = $object::class; + + // Usar cache se disponível + if (!isset($this->cache[$className])) { + $this->cacheObjectMetadata($object); + } + + return $this->extractValues($object); + } catch (\ReflectionException $e) { + throw new PropertyInspectionException('Failed to analyze object: ' . $e->getMessage(), 0, $e); + } catch (\Error $e) { + throw new PropertyInspectionException('An error occurred during object analysis: ' . $e->getMessage(), 0, $e); + } + } + + private function cacheObjectMetadata(object $object): void + { + $className = $object::class; + $reflection = new \ReflectionClass($object); + $cachedProperties = []; + + foreach ($reflection->getProperties() as $property) { + $attributes = $property->getAttributes($this->attributeClass, \ReflectionAttribute::IS_INSTANCEOF); + + if (!empty($attributes)) { + $property->setAccessible(true); + $attributeInstances = array_map( + static fn (\ReflectionAttribute $attr): object => $attr->newInstance(), + $attributes + ); + + $cachedProperties[$property->getName()] = [ + 'attributes' => $attributeInstances, + 'property' => $property + ]; + } + } + + $this->cache[$className] = $cachedProperties; + } + + private function extractValues(object $object): array + { + $results = []; + $className = $object::class; + + foreach ($this->cache[$className] as $propertyName => $data) { + $results[$propertyName] = [ + 'value' => $data['property']->getValue($object), + 'attributes' => $data['attributes'] + ]; + } + + return $results; + } + + public function clearCache(): void + { + $this->cache = []; + } +} + +// Before +// final readonly class AttributeAnalyzer implements AttributeAnalyzerContract +// { +// public function __construct(private string $attributeClass) +// { +// } + +// public function analyzeObject(object $object): array +// { +// try { +// $results = []; +// $reflection = new \ReflectionClass($object); + +// foreach ($reflection->getProperties() as $property) { +// $propertyResult = $this->analyzeProperty($object, $property); +// if (null !== $propertyResult) { +// $results[$property->getName()] = $propertyResult; +// } +// } + +// return $results; +// } catch (\ReflectionException $e) { +// throw new PropertyInspectionException('Failed to analyze object: ' . $e->getMessage(), 0, $e); +// } catch (\Error $e) { +// throw new PropertyInspectionException('An error occurred during object analysis: ' . $e->getMessage(), 0, $e); +// } +// } + +// private function analyzeProperty(object $object, \ReflectionProperty $property): ?array +// { +// $attributes = $property->getAttributes($this->attributeClass, \ReflectionAttribute::IS_INSTANCEOF); +// if (empty($attributes)) { +// return null; +// } + +// $property->setAccessible(true); +// $propertyValue = $property->getValue($object); + +// $attributeInstances = array_map( +// static fn (\ReflectionAttribute $attr): object => $attr->newInstance(), +// $attributes +// ); + +// return [ +// 'value' => $propertyValue, +// 'attributes' => $attributeInstances, +// ]; +// } +// } + + + +// Executar o benchmark +$benchmark = new AttributeAnalyzerBenchmark(); +$benchmark->runBenchmark(); diff --git a/tests/benchmark_attribute_handler.php b/tests/benchmark_attribute_handler.php new file mode 100644 index 0000000..d819142 --- /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(); diff --git a/tests/benchmark_foreach.php b/tests/benchmark_foreach.php new file mode 100644 index 0000000..bc80b5f --- /dev/null +++ b/tests/benchmark_foreach.php @@ -0,0 +1,159 @@ + $this->benchmarkMatchVsTernary(), + 'Array Walk vs Foreach' => $this->benchmarkArrayWalkVsForeach(), + 'Array Map/Filter vs Foreach' => $this->benchmarkArrayMapVsForeach(), + ]; + + $this->printResults($results); + } + + private function benchmarkMatchVsTernary(): array + { + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $result = match (true) { + !(0 === $i % 2) => null, + default => $i, + }; + } + $matchTime = microtime(true) - $start; + + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $result = (0 === $i % 2) ? $i : null; + } + $ternaryTime = microtime(true) - $start; + + $percentDiff = (($matchTime - $ternaryTime) / $matchTime) * 100; + + return [ + 'Match Operation' => number_format($matchTime * 1000, 4), + 'Ternary Operation' => number_format($ternaryTime * 1000, 4), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $ternaryTime < $matchTime ? 'Ternary' : 'Match', + ]; + } + + private function benchmarkArrayWalkVsForeach(): array + { + $largeArray = array_fill(0, 10000, 'value'); + + $start = microtime(true); + array_walk($largeArray, function ($value, $key) { + $dummy = $value . $key; + }); + $arrayWalkTime = microtime(true) - $start; + + $start = microtime(true); + foreach ($largeArray as $key => $value) { + $dummy = $value . $key; + } + $foreachTime = microtime(true) - $start; + + $percentDiff = (($arrayWalkTime - $foreachTime) / $arrayWalkTime) * 100; + + return [ + 'Array Walk' => number_format($arrayWalkTime * 1000, 4), + 'Foreach Loop' => number_format($foreachTime * 1000, 4), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $foreachTime < $arrayWalkTime ? 'Foreach' : 'Array Walk', + ]; + } + + private function benchmarkArrayMapVsForeach(): array + { + $processors = array_fill(0, 100, ['config' => 'value']); + + $start = microtime(true); + $result = array_filter(array_map( + fn ($config) => $config['config'], + $processors + )); + $arrayMapTime = microtime(true) - $start; + + $start = microtime(true); + $result = []; + foreach ($processors as $config) { + if ($value = $config['config']) { + $result[] = $value; + } + } + $foreachTime = microtime(true) - $start; + + $percentDiff = (($arrayMapTime - $foreachTime) / $arrayMapTime) * 100; + + return [ + 'Array Map/Filter' => number_format($arrayMapTime * 1000, 4), + 'Foreach Loop' => number_format($foreachTime * 1000, 4), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $foreachTime < $arrayMapTime ? 'Foreach' : 'Array Map/Filter', + ]; + } + + private function printResults(array $results): void + { + echo self::ANSI_BOLD . "\nPERFORMANCE BENCHMARK RESULTS\n" . self::ANSI_RESET; + echo str_repeat('=', 50) . "\n\n"; + + foreach ($results as $testName => $data) { + echo self::ANSI_BOLD . "$testName Test\n" . self::ANSI_RESET; + echo str_repeat('-', 30) . "\n"; + + foreach ($data as $metric => $value) { + if ('Winner' === $metric) { + continue; + } + + $color = $this->getColor($data, $metric); + echo sprintf( + "%s%-20s: %s%s%s\n", + $color, + $metric, + $value, + 'Difference' !== $metric ? ' ms' : '%', + self::ANSI_RESET + ); + } + + echo sprintf( + "\n%s%s%s\n\n", + self::ANSI_GREEN, + "Winner: {$data['Winner']}", + self::ANSI_RESET + ); + } + + echo str_repeat('=', 50) . "\n"; + } + + private function getColor(array $data, string $metric): string + { + if ('Difference' === $metric) { + return self::ANSI_YELLOW; + } + + $winner = $data['Winner']; + $isWinner = false !== strpos($metric, $winner); + + return $isWinner ? self::ANSI_GREEN : self::ANSI_RED; + } +} + +// Run the benchmark +$benchmark = new FormattedBenchmark(); +$benchmark->runBenchmark(); diff --git a/tests/benchmark_storage_key.php b/tests/benchmark_storage_key.php new file mode 100644 index 0000000..7a038c5 --- /dev/null +++ b/tests/benchmark_storage_key.php @@ -0,0 +1,329 @@ +name; + } +} + +class CacheManager +{ + private WeakMap $cache; + private array $keyCache; + + public function __construct() + { + $this->cache = new WeakMap(); + } + + public function set(PropertyId $id, mixed $value): void + { + $key = $this->getOrCreateKey($id); + $this->cache[$key] = $value; + } + + public function get(PropertyId $id): mixed + { + $key = $this->getOrCreateKey($id); + + return $this->cache[$key] ?? null; + } + + private function getOrCreateKey(PropertyId $id): object + { + return $this->keyCache[$id->toString()] ??= new stdClass(); + } +} + +class CompleteBenchmark +{ + private const ITERATIONS = 100000; + private const ANSI_GREEN = "\033[32m"; + private const ANSI_YELLOW = "\033[33m"; + private const ANSI_RED = "\033[31m"; + private const ANSI_RESET = "\033[0m"; + private const ANSI_BOLD = "\033[1m"; + + public function runBenchmark(): void + { + $results = [ + 'Object Creation' => $this->benchmarkObjectCreation(), + 'Storage Operations' => $this->benchmarkStorageOperations(), + 'Memory Usage' => $this->benchmarkMemoryUsage(), + 'Batch Operations' => $this->benchmarkBatchOperations(), + ]; + + $this->printResults($results); + } + + private function benchmarkObjectCreation(): array + { + // stdClass + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $key = new stdClass(); + } + $stdClassTime = microtime(true) - $start; + + // StorageKey + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $key = StorageKey::forProperty("property_$i"); + } + $storageKeyTime = microtime(true) - $start; + + // Hybrid + $start = microtime(true); + $cacheManager = new CacheManager(); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $id = new PropertyId("property_$i"); + $cacheManager->set($id, "value_$i"); + } + $hybridTime = microtime(true) - $start; + + return [ + 'stdClass' => number_format($stdClassTime * 1000, 4), + 'StorageKey' => number_format($storageKeyTime * 1000, 4), + 'Hybrid' => number_format($hybridTime * 1000, 4), + 'Best' => $this->findBest([ + 'stdClass' => $stdClassTime, + 'StorageKey' => $storageKeyTime, + 'Hybrid' => $hybridTime, + ]), + ]; + } + + private function benchmarkStorageOperations(): array + { + $weakMapStd = new WeakMap(); + $weakMapStorage = new WeakMap(); + $cacheManager = new CacheManager(); + + // stdClass + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $key = new stdClass(); + $weakMapStd[$key] = "value_$i"; + $value = $weakMapStd[$key]; + } + $stdClassTime = microtime(true) - $start; + + // StorageKey + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $key = StorageKey::forProperty("property_$i"); + $weakMapStorage[$key] = "value_$i"; + $value = $weakMapStorage[$key]; + } + $storageKeyTime = microtime(true) - $start; + + // Hybrid + $start = microtime(true); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $id = new PropertyId("property_$i"); + $cacheManager->set($id, "value_$i"); + $value = $cacheManager->get($id); + } + $hybridTime = microtime(true) - $start; + + return [ + 'stdClass' => number_format($stdClassTime * 1000, 4), + 'StorageKey' => number_format($storageKeyTime * 1000, 4), + 'Hybrid' => number_format($hybridTime * 1000, 4), + 'Best' => $this->findBest([ + 'stdClass' => $stdClassTime, + 'StorageKey' => $storageKeyTime, + 'Hybrid' => $hybridTime, + ]), + ]; + } + + private function benchmarkMemoryUsage(): array + { + // stdClass + $start = memory_get_usage(); + $keys = []; + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $keys[] = new stdClass(); + } + $stdClassMemory = memory_get_usage() - $start; + unset($keys); + + // StorageKey + $start = memory_get_usage(); + $keys = []; + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $keys[] = StorageKey::forProperty("property_$i"); + } + $storageKeyMemory = memory_get_usage() - $start; + unset($keys); + + // Hybrid + $start = memory_get_usage(); + $cacheManager = new CacheManager(); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $id = new PropertyId("property_$i"); + $cacheManager->set($id, "value_$i"); + } + $hybridMemory = memory_get_usage() - $start; + + return [ + 'stdClass' => number_format($stdClassMemory / 1024, 2), + 'StorageKey' => number_format($storageKeyMemory / 1024, 2), + 'Hybrid' => number_format($hybridMemory / 1024, 2), + 'Best' => $this->findBest([ + 'stdClass' => $stdClassMemory, + 'StorageKey' => $storageKeyMemory, + 'Hybrid' => $hybridMemory, + ]), + ]; + } + + private function benchmarkBatchOperations(): array + { + $dataSize = 10000; + + // stdClass + $start = microtime(true); + $weakMap = new WeakMap(); + for ($i = 0; $i < $dataSize; ++$i) { + $key = new stdClass(); + $weakMap[$key] = "value_$i"; + } + for ($i = 0; $i < $dataSize; ++$i) { + $key = new stdClass(); + $weakMap[$key] = null; + } + $stdClassTime = microtime(true) - $start; + + // StorageKey + $start = microtime(true); + $weakMap = new WeakMap(); + for ($i = 0; $i < $dataSize; ++$i) { + $key = StorageKey::forProperty("property_$i"); + $weakMap[$key] = "value_$i"; + } + for ($i = 0; $i < $dataSize; ++$i) { + $key = StorageKey::forProperty("property_$i"); + $weakMap[$key] = null; + } + $storageKeyTime = microtime(true) - $start; + + // Hybrid + $start = microtime(true); + $cacheManager = new CacheManager(); + for ($i = 0; $i < $dataSize; ++$i) { + $id = new PropertyId("property_$i"); + $cacheManager->set($id, "value_$i"); + } + for ($i = 0; $i < $dataSize; ++$i) { + $id = new PropertyId("property_$i"); + $cacheManager->set($id, null); + } + $hybridTime = microtime(true) - $start; + + return [ + 'stdClass' => number_format($stdClassTime * 1000, 4), + 'StorageKey' => number_format($storageKeyTime * 1000, 4), + 'Hybrid' => number_format($hybridTime * 1000, 4), + 'Best' => $this->findBest([ + 'stdClass' => $stdClassTime, + 'StorageKey' => $storageKeyTime, + 'Hybrid' => $hybridTime, + ]), + ]; + } + + private function findBest(array $times): string + { + return array_keys($times, min($times))[0]; + } + + private function printResults(array $results): void + { + echo self::ANSI_BOLD . "\nCOMPLETE PERFORMANCE BENCHMARK\n" . self::ANSI_RESET; + echo str_repeat('=', 50) . "\n\n"; + + foreach ($results as $testName => $data) { + echo self::ANSI_BOLD . "$testName Test\n" . self::ANSI_RESET; + echo str_repeat('-', 30) . "\n"; + + foreach ($data as $metric => $value) { + if ('Best' === $metric) { + continue; + } + + $color = $this->getColor($data, $metric); + $unit = $this->getUnit($testName); + echo sprintf( + "%s%-20s: %s%s%s\n", + $color, + $metric, + $value, + $unit, + self::ANSI_RESET + ); + } + + echo sprintf( + "\n%s%s%s\n\n", + self::ANSI_GREEN, + "Best Performance: {$data['Best']}", + self::ANSI_RESET + ); + } + + echo str_repeat('=', 50) . "\n"; + } + + private function getUnit(string $testName): string + { + return match ($testName) { + 'Memory Usage' => ' KB', + default => ' ms' + }; + } + + private function getColor(array $data, string $metric): string + { + if ('Difference' === $metric) { + return self::ANSI_YELLOW; + } + + $best = $data['Best']; + + return ($metric === $best) ? self::ANSI_GREEN : self::ANSI_RED; + } +} + +// Run the benchmark +$benchmark = new CompleteBenchmark(); +$benchmark->runBenchmark(); diff --git a/tests/benchmark_weakmap.php b/tests/benchmark_weakmap.php new file mode 100644 index 0000000..87b7d55 --- /dev/null +++ b/tests/benchmark_weakmap.php @@ -0,0 +1,205 @@ + $this->benchmarkMemoryUsage(), + 'Cache Access Speed' => $this->benchmarkCacheAccess(), + 'Garbage Collection' => $this->benchmarkGarbageCollection(), + ]; + + $this->printResults($results); + } + + private function benchmarkMemoryUsage(): array + { + // Array Cache + $start = memory_get_usage(); + $arrayCache = []; + $objects = []; + + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $objects[] = $obj; + $arrayCache[spl_object_hash($obj)] = "data_$i"; + } + + $arrayMemory = memory_get_usage() - $start; + unset($arrayCache, $objects); + + // WeakMap Cache + $start = memory_get_usage(); + $weakMap = new WeakMap(); + $objects = []; + + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $objects[] = $obj; + $weakMap[$obj] = "data_$i"; + } + + $weakMapMemory = memory_get_usage() - $start; + unset($weakMap, $objects); + + $percentDiff = (($arrayMemory - $weakMapMemory) / $arrayMemory) * 100; + + return [ + 'Array Cache' => number_format($arrayMemory / 1024, 2), + 'WeakMap Cache' => number_format($weakMapMemory / 1024, 2), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $weakMapMemory < $arrayMemory ? 'WeakMap' : 'Array', + ]; + } + + private function benchmarkCacheAccess(): array + { + // Array Cache + $arrayCache = []; + $objects = []; + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $objects[] = $obj; + $arrayCache[spl_object_hash($obj)] = "data_$i"; + } + + $start = microtime(true); + foreach ($objects as $obj) { + $data = $arrayCache[spl_object_hash($obj)]; + } + $arrayTime = microtime(true) - $start; + + // WeakMap Cache + $weakMap = new WeakMap(); + $objects = []; + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $objects[] = $obj; + $weakMap[$obj] = "data_$i"; + } + + $start = microtime(true); + foreach ($objects as $obj) { + $data = $weakMap[$obj]; + } + $weakMapTime = microtime(true) - $start; + + $percentDiff = (($arrayTime - $weakMapTime) / $arrayTime) * 100; + + return [ + 'Array Access' => number_format($arrayTime * 1000, 4), + 'WeakMap Access' => number_format($weakMapTime * 1000, 4), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $weakMapTime < $arrayTime ? 'WeakMap' : 'Array', + ]; + } + + private function benchmarkGarbageCollection(): array + { + // Array Cache + $start = microtime(true); + $arrayCache = []; + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $arrayCache[spl_object_hash($obj)] = "data_$i"; + unset($obj); + } + gc_collect_cycles(); + $arrayTime = microtime(true) - $start; + + // WeakMap Cache + $start = microtime(true); + $weakMap = new WeakMap(); + for ($i = 0; $i < self::ITERATIONS; ++$i) { + $obj = new stdClass(); + $weakMap[$obj] = "data_$i"; + unset($obj); + } + gc_collect_cycles(); + $weakMapTime = microtime(true) - $start; + + $percentDiff = (($arrayTime - $weakMapTime) / $arrayTime) * 100; + + return [ + 'Array GC' => number_format($arrayTime * 1000, 4), + 'WeakMap GC' => number_format($weakMapTime * 1000, 4), + 'Difference' => number_format(abs($percentDiff), 2), + 'Winner' => $weakMapTime < $arrayTime ? 'WeakMap' : 'Array', + ]; + } + + private function printResults(array $results): void + { + echo self::ANSI_BOLD . "\nWEAKMAP VS ARRAY CACHE BENCHMARK\n" . self::ANSI_RESET; + echo str_repeat('=', 50) . "\n\n"; + + foreach ($results as $testName => $data) { + echo self::ANSI_BOLD . "$testName Test\n" . self::ANSI_RESET; + echo str_repeat('-', 30) . "\n"; + + foreach ($data as $metric => $value) { + if ('Winner' === $metric) { + continue; + } + + $color = $this->getColor($data, $metric); + $unit = $this->getUnit($testName, $metric); + echo sprintf( + "%s%-20s: %s%s%s\n", + $color, + $metric, + $value, + $unit, + self::ANSI_RESET + ); + } + + echo sprintf( + "\n%s%s%s\n\n", + self::ANSI_GREEN, + "Winner: {$data['Winner']}", + self::ANSI_RESET + ); + } + + echo str_repeat('=', 50) . "\n"; + } + + private function getUnit(string $testName, string $metric): string + { + if ('Difference' === $metric) { + return '%'; + } + + return match ($testName) { + 'Memory Usage' => ' KB', + default => ' ms' + }; + } + + private function getColor(array $data, string $metric): string + { + if ('Difference' === $metric) { + return self::ANSI_YELLOW; + } + + $winner = $data['Winner']; + $isWinner = false !== strpos($metric, $winner); + + return $isWinner ? self::ANSI_GREEN : self::ANSI_RED; + } +} + +// Run the benchmark +$benchmark = new WeakMapBenchmark(); +$benchmark->runBenchmark();