From 49e2c94782ace420e35031aba21683f5f91fa8b2 Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Fri, 16 Aug 2024 14:47:47 +0200 Subject: [PATCH 1/5] improve reflector test, use specific method for attribute specification --- docs/attributes.md | 4 +-- .../AnnotatedInjectAttributeConfig.php | 6 +++-- src/ClassScanner/AttributeSpecification.php | 25 +++++++++++++++++-- tests/Fake/FakeAllAttributes.php | 17 +++++++------ tests/Resolver/ReflectorTest.php | 22 ++++++++-------- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/docs/attributes.md b/docs/attributes.md index 730454a..97f73c7 100644 --- a/docs/attributes.md +++ b/docs/attributes.md @@ -226,7 +226,7 @@ class Route implements AttributeConfigInterface { $container->lazyLazy( $di->lazyCallable([ $di->lazyNew($specification->getClassName()), - $specification->getTargetConfig()['method'] + $specification->getTargetMethod() ]) ) ); @@ -273,7 +273,7 @@ class SymfonyRouteAttributeConfig implements AttributeConfigInterface $invokableRoute = $di->lazyCallable([ $container->lazyNew($annotatedClassName), - $specification->getTargetConfig()['method'] + $specification->getTargetMethod() ]); // these are not real parameters, but just examples diff --git a/src/ClassScanner/AnnotatedInjectAttributeConfig.php b/src/ClassScanner/AnnotatedInjectAttributeConfig.php index a0c33df..1391241 100644 --- a/src/ClassScanner/AnnotatedInjectAttributeConfig.php +++ b/src/ClassScanner/AnnotatedInjectAttributeConfig.php @@ -15,12 +15,14 @@ final class AnnotatedInjectAttributeConfig implements AttributeConfigInterface { + private const CONSTRUCTOR_NAME = '__construct'; + public static function define(Container $di, AttributeSpecification $specification): void { /** @var AnnotatedInjectInterface $attribute */ $attribute = $specification->getAttributeInstance(); - if ($specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) { - $di->params[$specification->getClassName()][$specification->getTargetConfig()['parameter']] = $attribute->inject(); + if ($specification->getTargetMethod() === self::CONSTRUCTOR_NAME && $specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) { + $di->params[$specification->getClassName()][$specification->getTargetParameter()] = $attribute->inject(); } } } \ No newline at end of file diff --git a/src/ClassScanner/AttributeSpecification.php b/src/ClassScanner/AttributeSpecification.php index 16b031f..fb66ea2 100644 --- a/src/ClassScanner/AttributeSpecification.php +++ b/src/ClassScanner/AttributeSpecification.php @@ -9,8 +9,14 @@ final class AttributeSpecification private object $attributeInstance; private string $className; private int $attributeTarget; + /** + * @param array{method?: string, parameter?: string, property?: string, constant?: string} $targetConfig + */ private array $targetConfig; + /** + * @param array{method?: string, parameter?: string, property?: string, constant?: string} $targetConfig + */ public function __construct( object $attributeInstance, string $className, @@ -39,8 +45,23 @@ public function getAttributeTarget(): int return $this->attributeTarget; } - public function getTargetConfig(): array + public function getTargetMethod(): ?string { - return $this->targetConfig; + return $this->targetConfig['method'] ?? null; + } + + public function getTargetParameter(): ?string + { + return $this->targetConfig['parameter'] ?? null; + } + + public function getTargetProperty(): ?string + { + return $this->targetConfig['property'] ?? null; + } + + public function getTargetConstant(): ?string + { + return $this->targetConfig['constant'] ?? null; } } \ No newline at end of file diff --git a/tests/Fake/FakeAllAttributes.php b/tests/Fake/FakeAllAttributes.php index 11651df..fbb28f3 100644 --- a/tests/Fake/FakeAllAttributes.php +++ b/tests/Fake/FakeAllAttributes.php @@ -1,25 +1,28 @@ $attributes */ $attributes = [...$reflector->yieldAttributes(FakeAllAttributes::class)]; - $this->assertCount(7, $attributes); - foreach ($attributes as $attribute) { - $instance = $attribute->getAttributeInstance(); - match ($attribute->getAttributeTarget()) { - \Attribute::TARGET_CLASS => $this->assertSame(1, $instance->getValue()), - \Attribute::TARGET_PROPERTY => $this->assertContains($instance->getValue(), [2, 4]), - \Attribute::TARGET_CLASS_CONSTANT => $this->assertSame(3, $instance->getValue()), - \Attribute::TARGET_PARAMETER => $this->assertSame(4, $instance->getValue()), - \Attribute::TARGET_METHOD => $this->assertSame(5, $instance->getValue()), - }; + $this->assertCount(8, $attributes); + foreach ($attributes as $specification) { + $instance = $specification->getAttributeInstance(); + $this->assertSame($specification->getAttributeTarget(), $instance->getValue() & $specification->getAttributeTarget()); + + if ($specification->getTargetParameter() === 'methodParameters') { + $this->assertSame('method', $specification->getTargetMethod()); + } + + if ($specification->getTargetParameter() === 'parameter') { + $this->assertSame('__construct', $specification->getTargetMethod()); + } } } } \ No newline at end of file From 69fe40cb319bc3602f0ccb070a8bd212268f5c1f Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Mon, 19 Aug 2024 12:35:21 +0200 Subject: [PATCH 2/5] add class specification class, allows attribute config to ask for related attributes --- docs/attributes.md | 8 +- .../AnnotatedInjectAttributeConfig.php | 16 ++-- src/ClassScanner/AttributeConfigInterface.php | 6 +- src/ClassScanner/AttributeSpecification.php | 31 +++++++ src/ClassScanner/BlueprintAttributeConfig.php | 8 +- src/ClassScanner/ClassMap.php | 52 ++++++------ src/ClassScanner/ClassScannerConfig.php | 5 +- src/ClassScanner/ClassSpecification.php | 84 +++++++++++++++++++ src/ClassScanner/ComposerMapGenerator.php | 8 +- .../ClassScanner/CachedFileGeneratorTest.php | 2 +- tests/Fake/FakeWorkerAttribute.php | 13 ++- 11 files changed, 185 insertions(+), 48 deletions(-) create mode 100644 src/ClassScanner/ClassSpecification.php diff --git a/docs/attributes.md b/docs/attributes.md index 97f73c7..a626738 100644 --- a/docs/attributes.md +++ b/docs/attributes.md @@ -205,6 +205,7 @@ will cause a `RealRoute` to be appended in the routes array. use Aura\Di\Attribute\AttributeConfigFor; use Aura\Di\ClassScanner\AttributeConfigInterface; use Aura\Di\ClassScanner\AttributeSpecification; +use Aura\Di\ClassScanner\ClassSpecification; use Aura\Di\Container; #[\Attribute] @@ -213,7 +214,7 @@ class Route implements AttributeConfigInterface { public function __construct(private string $method, private string $uri) { } - public static function define(Container $di, AttributeSpecification $specification): void + public static function define(Container $di, AttributeSpecification $specification, ClassSpecification $classSpecification): void { if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) { /** @var self $attribute */ @@ -259,15 +260,16 @@ you can create an implementation of `AttributeConfigInterface` yourself, and ann use Aura\Di\Attribute\AttributeConfigFor; use Aura\Di\ClassScanner\AttributeConfigInterface; use Aura\Di\ClassScanner\AttributeSpecification; +use Aura\Di\ClassScanner\ClassSpecification; use Aura\Di\Container; use Symfony\Component\Routing\Attribute\Route; #[AttributeConfigFor(Route::class)] class SymfonyRouteAttributeConfig implements AttributeConfigInterface { - public static function define(Container $di, AttributeSpecification $specification): void + public static function define(Container $di, AttributeSpecification $specification, ClassSpecification $classSpecification): void { - if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) { + if ($specification->isMethodAttribute()) { /** @var Route $attribute */ $attribute = $specification->getAttributeInstance(); diff --git a/src/ClassScanner/AnnotatedInjectAttributeConfig.php b/src/ClassScanner/AnnotatedInjectAttributeConfig.php index 1391241..3fab1bb 100644 --- a/src/ClassScanner/AnnotatedInjectAttributeConfig.php +++ b/src/ClassScanner/AnnotatedInjectAttributeConfig.php @@ -15,14 +15,16 @@ final class AnnotatedInjectAttributeConfig implements AttributeConfigInterface { - private const CONSTRUCTOR_NAME = '__construct'; - - public static function define(Container $di, AttributeSpecification $specification): void + public static function define( + Container $di, + AttributeSpecification $attributeSpecification, + ClassSpecification $classSpecification + ): void { - /** @var AnnotatedInjectInterface $attribute */ - $attribute = $specification->getAttributeInstance(); - if ($specification->getTargetMethod() === self::CONSTRUCTOR_NAME && $specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) { - $di->params[$specification->getClassName()][$specification->getTargetParameter()] = $attribute->inject(); + if ($attributeSpecification->isConstructorParameterAttribute()) { + /** @var AnnotatedInjectInterface $attribute */ + $attribute = $attributeSpecification->getAttributeInstance(); + $di->params[$attributeSpecification->getClassName()][$attributeSpecification->getTargetParameter()] = $attribute->inject(); } } } \ No newline at end of file diff --git a/src/ClassScanner/AttributeConfigInterface.php b/src/ClassScanner/AttributeConfigInterface.php index 51e606a..7be8260 100644 --- a/src/ClassScanner/AttributeConfigInterface.php +++ b/src/ClassScanner/AttributeConfigInterface.php @@ -14,5 +14,9 @@ interface AttributeConfigInterface { - public static function define(Container $di, AttributeSpecification $specification): void; + public static function define( + Container $di, + AttributeSpecification $attributeSpecification, + ClassSpecification $classSpecification + ): void; } \ No newline at end of file diff --git a/src/ClassScanner/AttributeSpecification.php b/src/ClassScanner/AttributeSpecification.php index fb66ea2..c54b519 100644 --- a/src/ClassScanner/AttributeSpecification.php +++ b/src/ClassScanner/AttributeSpecification.php @@ -6,6 +6,7 @@ final class AttributeSpecification { + private const CONSTRUCTOR_NAME = '__construct'; private object $attributeInstance; private string $className; private int $attributeTarget; @@ -64,4 +65,34 @@ public function getTargetConstant(): ?string { return $this->targetConfig['constant'] ?? null; } + + public function isConstructorParameterAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_PARAMETER && ($this->targetConfig['method'] ?? '') === self::CONSTRUCTOR_NAME; + } + + public function isMethodAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_METHOD; + } + + public function isClassAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_CLASS; + } + + public function isParameterAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_PARAMETER; + } + + public function isPropertyAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_PROPERTY; + } + + public function isClassConstantAttribute(): bool + { + return $this->attributeTarget === \Attribute::TARGET_CLASS_CONSTANT; + } } \ No newline at end of file diff --git a/src/ClassScanner/BlueprintAttributeConfig.php b/src/ClassScanner/BlueprintAttributeConfig.php index 0626a5d..7a80b02 100644 --- a/src/ClassScanner/BlueprintAttributeConfig.php +++ b/src/ClassScanner/BlueprintAttributeConfig.php @@ -14,8 +14,12 @@ final class BlueprintAttributeConfig implements AttributeConfigInterface { - public static function define(Container $di, AttributeSpecification $specification): void + public static function define( + Container $di, + AttributeSpecification $attributeSpecification, + ClassSpecification $classSpecification + ): void { - $di->params[$specification->getClassName()] = $di->params[$specification->getClassName()] ?? []; + $di->params[$attributeSpecification->getClassName()] = $di->params[$attributeSpecification->getClassName()] ?? []; } } \ No newline at end of file diff --git a/src/ClassScanner/ClassMap.php b/src/ClassScanner/ClassMap.php index 466aa6b..4ff367d 100644 --- a/src/ClassScanner/ClassMap.php +++ b/src/ClassScanner/ClassMap.php @@ -11,7 +11,7 @@ final class ClassMap private array $scanPaths; /** @var array */ private array $filesToClass = []; - /** @var array> */ + /** @var array */ private array $classesToAttributes = []; /** @@ -23,13 +23,10 @@ public function __construct(array $scanPaths, string $basePath) $this->basePath = $basePath; } - /** - * @param array $attributeSpecifications - */ - public function addClass(string $class, string $filename, array $attributeSpecifications): void + public function addClass(ClassSpecification $classSpecification): void { - $this->filesToClass[$filename] = $class; - $this->classesToAttributes[$class] = $attributeSpecifications; + $this->filesToClass[$classSpecification->getFilename()] = $classSpecification->getClassName(); + $this->classesToAttributes[$classSpecification->getClassName()] = $classSpecification; } public function remove(string $filename): void @@ -54,12 +51,13 @@ public function getFiles(): array return \array_keys($this->filesToClass); } - /** - * @return array - */ - public function getAttributeSpecificationsFor(string $className): array + public function getClassSpecificationFor(string $className): ?ClassSpecification { - return $this->classesToAttributes[$className] ?? []; + if (\array_key_exists($className, $this->classesToAttributes)) { + return $this->classesToAttributes[$className]; + } + + throw new \InvalidArgumentException('Cannot find class specification for: ' . $className); } /** @@ -67,7 +65,13 @@ public function getAttributeSpecificationsFor(string $className): array */ public function getAttributeSpecifications(): array { - return \array_merge([], ...array_values($this->classesToAttributes)); + return \array_merge( + [], + ...\array_values(array_map( + fn (ClassSpecification $classSpecification) => $classSpecification->getAttributes(), + $this->classesToAttributes + )) + ); } public function isAttributeClassFile(string $filename): bool @@ -81,14 +85,8 @@ public function isAttributeClassFile(string $filename): bool return false; } - $attributeSpecifications = $this->classesToAttributes[$class]; - foreach ($attributeSpecifications as $specification) { - if ($specification->getAttributeInstance() instanceof \Attribute) { - return true; - } - } - return false; + return $this->classesToAttributes[$class]->isAttributeClass(); } public function merge(ClassMap $other): ClassMap @@ -110,7 +108,7 @@ public function saveToFileHandle($fileHandle): void foreach ($this->filesToClass as $filename => $className) { $classMapJson['files'][$filename] = $className; - if ($attributeSpecifications = $this->getAttributeSpecificationsFor($className)) { + if ($attributeSpecifications = $this->getClassSpecificationFor($className)->getAttributes()) { $classMapJson['attributes'][$className] = \array_map( fn (AttributeSpecification $attribute) => \serialize($attribute), $attributeSpecifications @@ -143,11 +141,13 @@ public static function fromFileHandle($fileHandle): ClassMap foreach ($cacheContentsJson['files'] as $filename => $className) { $classMap->addClass( - $className, - $filename, - \array_map( - fn (string $serializedAttributeSpecification) => \unserialize($serializedAttributeSpecification), - $cacheContentsJson['attributes'][$className] ?? [] + new ClassSpecification( + $className, + $filename, + \array_map( + fn (string $serializedAttributeSpecification) => \unserialize($serializedAttributeSpecification), + $cacheContentsJson['attributes'][$className] ?? [] + ) ) ); } diff --git a/src/ClassScanner/ClassScannerConfig.php b/src/ClassScanner/ClassScannerConfig.php index aea275e..a1d3dfe 100644 --- a/src/ClassScanner/ClassScannerConfig.php +++ b/src/ClassScanner/ClassScannerConfig.php @@ -64,13 +64,15 @@ public function define(Container $di): void } } - foreach ($classMap->getAttributeSpecificationsFor($className) as $specification) { + $classSpecification = $classMap->getClassSpecificationFor($className); + foreach ($classSpecification->getAttributes() as $specification) { $attribute = $specification->getAttributeInstance(); if (\array_key_exists($attribute::class, $attributeConfigs)) { $configuredBy = $attributeConfigs[$attribute::class]; $configuredBy::define( $di, $specification, + $classSpecification ); } @@ -81,6 +83,7 @@ public function define(Container $di): void $configuredBy::define( $di, $specification, + $classSpecification ); } } diff --git a/src/ClassScanner/ClassSpecification.php b/src/ClassScanner/ClassSpecification.php new file mode 100644 index 0000000..ce5c25e --- /dev/null +++ b/src/ClassScanner/ClassSpecification.php @@ -0,0 +1,84 @@ + $attributes + */ + private array $attributes; + + /** + * @param array $attributes + */ + public function __construct( + string $className, + string $filename, + array $attributes = [], + ) + { + $this->className = $className; + $this->filename = $filename; + $this->attributes = $attributes; + } + + public function getClassName(): string + { + return $this->className; + } + + public function getFilename(): string + { + return $this->filename; + } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + public function isAttributeClass(): bool + { + foreach ($this->attributes as $specification) { + if ($specification->getAttributeInstance() instanceof \Attribute) { + return true; + } + } + + return false; + } + + /** + * @return array + */ + public function getClassAttributes(): array + { + return \array_filter( + $this->attributes, + function (AttributeSpecification $specification) { + return $specification->getAttributeTarget() === \Attribute::TARGET_CLASS; + } + ); + } + + /** + * @return array + */ + public function getParameterAttributesForMethod(string $methodName): array + { + return \array_filter( + $this->attributes, + function (AttributeSpecification $specification) use ($methodName) { + return $specification->getTargetMethod() === $methodName; + } + ); + } +} \ No newline at end of file diff --git a/src/ClassScanner/ComposerMapGenerator.php b/src/ClassScanner/ComposerMapGenerator.php index a74c904..4798900 100644 --- a/src/ClassScanner/ComposerMapGenerator.php +++ b/src/ClassScanner/ComposerMapGenerator.php @@ -51,9 +51,11 @@ private function convertToClassMap(ClassMap $classMap, array $composerMap): Clas } $classMap->addClass( - $class, - $path, - [...$this->reflector->yieldAttributes($class)] + new ClassSpecification( + $class, + $path, + [...$this->reflector->yieldAttributes($class)] + ) ); } diff --git a/tests/ClassScanner/CachedFileGeneratorTest.php b/tests/ClassScanner/CachedFileGeneratorTest.php index 84f3761..e4d39cd 100644 --- a/tests/ClassScanner/CachedFileGeneratorTest.php +++ b/tests/ClassScanner/CachedFileGeneratorTest.php @@ -51,7 +51,7 @@ public function testAddingClass() $classMap2 = $cachedFileModificationGenerator->update($classMap, [$newFile]); $this->assertContains($newClassName, $classMap2->getClasses()); - $this->assertCount(1, $classMap2->getAttributeSpecificationsFor($newClassName)); + $this->assertCount(1, $classMap2->getClassSpecificationFor($newClassName)->getAttributes()); } public function testRemovingClass() diff --git a/tests/Fake/FakeWorkerAttribute.php b/tests/Fake/FakeWorkerAttribute.php index 1379573..386fbb3 100644 --- a/tests/Fake/FakeWorkerAttribute.php +++ b/tests/Fake/FakeWorkerAttribute.php @@ -5,6 +5,7 @@ use Aura\Di\Attribute\BlueprintNamespace; use Aura\Di\ClassScanner\AttributeConfigInterface; use Aura\Di\ClassScanner\AttributeSpecification; +use Aura\Di\ClassScanner\ClassSpecification; use Aura\Di\Container; #[\Attribute] @@ -19,15 +20,19 @@ public function __construct(int $someSetting = 1) $this->someSetting = $someSetting; } - public static function define(Container $di, AttributeSpecification $specification): void + public static function define( + Container $di, + AttributeSpecification $attributeSpecification, + ClassSpecification $classSpecification + ): void { /** @var self $attribute */ - $attribute = $specification->getAttributeInstance(); - if ($specification->getAttributeTarget() === \Attribute::TARGET_CLASS) { + $attribute = $attributeSpecification->getAttributeInstance(); + if ($attributeSpecification->getAttributeTarget() === \Attribute::TARGET_CLASS) { $di->values['worker'] = $di->values['worker'] ?? []; $di->values['worker'][] = [ 'someSetting' => $attribute->someSetting, - 'className' => $specification->getClassName(), + 'className' => $attributeSpecification->getClassName(), ]; } } From a9ca13829ccd130b5ae3930f2689383bf40d9941 Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Wed, 21 Aug 2024 21:19:40 +0200 Subject: [PATCH 3/5] no need to postfix the variable with specification --- .../AnnotatedInjectAttributeConfig.php | 14 +++++--------- src/ClassScanner/AttributeConfigInterface.php | 6 +----- src/ClassScanner/BlueprintAttributeConfig.php | 8 ++------ tests/Fake/FakeWorkerAttribute.php | 16 ++++++---------- 4 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/ClassScanner/AnnotatedInjectAttributeConfig.php b/src/ClassScanner/AnnotatedInjectAttributeConfig.php index 3fab1bb..3ff6406 100644 --- a/src/ClassScanner/AnnotatedInjectAttributeConfig.php +++ b/src/ClassScanner/AnnotatedInjectAttributeConfig.php @@ -15,16 +15,12 @@ final class AnnotatedInjectAttributeConfig implements AttributeConfigInterface { - public static function define( - Container $di, - AttributeSpecification $attributeSpecification, - ClassSpecification $classSpecification - ): void + public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void { - if ($attributeSpecification->isConstructorParameterAttribute()) { - /** @var AnnotatedInjectInterface $attribute */ - $attribute = $attributeSpecification->getAttributeInstance(); - $di->params[$attributeSpecification->getClassName()][$attributeSpecification->getTargetParameter()] = $attribute->inject(); + if ($attribute->isConstructorParameterAttribute()) { + /** @var AnnotatedInjectInterface $annotatedInject */ + $annotatedInject = $attribute->getAttributeInstance(); + $di->params[$attribute->getClassName()][$attribute->getTargetParameter()] = $annotatedInject->inject(); } } } \ No newline at end of file diff --git a/src/ClassScanner/AttributeConfigInterface.php b/src/ClassScanner/AttributeConfigInterface.php index 7be8260..514b407 100644 --- a/src/ClassScanner/AttributeConfigInterface.php +++ b/src/ClassScanner/AttributeConfigInterface.php @@ -14,9 +14,5 @@ interface AttributeConfigInterface { - public static function define( - Container $di, - AttributeSpecification $attributeSpecification, - ClassSpecification $classSpecification - ): void; + public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void; } \ No newline at end of file diff --git a/src/ClassScanner/BlueprintAttributeConfig.php b/src/ClassScanner/BlueprintAttributeConfig.php index 7a80b02..6738ff8 100644 --- a/src/ClassScanner/BlueprintAttributeConfig.php +++ b/src/ClassScanner/BlueprintAttributeConfig.php @@ -14,12 +14,8 @@ final class BlueprintAttributeConfig implements AttributeConfigInterface { - public static function define( - Container $di, - AttributeSpecification $attributeSpecification, - ClassSpecification $classSpecification - ): void + public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void { - $di->params[$attributeSpecification->getClassName()] = $di->params[$attributeSpecification->getClassName()] ?? []; + $di->params[$attribute->getClassName()] = $di->params[$attribute->getClassName()] ?? []; } } \ No newline at end of file diff --git a/tests/Fake/FakeWorkerAttribute.php b/tests/Fake/FakeWorkerAttribute.php index 386fbb3..91008b1 100644 --- a/tests/Fake/FakeWorkerAttribute.php +++ b/tests/Fake/FakeWorkerAttribute.php @@ -20,19 +20,15 @@ public function __construct(int $someSetting = 1) $this->someSetting = $someSetting; } - public static function define( - Container $di, - AttributeSpecification $attributeSpecification, - ClassSpecification $classSpecification - ): void + public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void { - /** @var self $attribute */ - $attribute = $attributeSpecification->getAttributeInstance(); - if ($attributeSpecification->getAttributeTarget() === \Attribute::TARGET_CLASS) { + /** @var self $instance */ + $instance = $attribute->getAttributeInstance(); + if ($attribute->getAttributeTarget() === \Attribute::TARGET_CLASS) { $di->values['worker'] = $di->values['worker'] ?? []; $di->values['worker'][] = [ - 'someSetting' => $attribute->someSetting, - 'className' => $attributeSpecification->getClassName(), + 'someSetting' => $instance->someSetting, + 'className' => $attribute->getClassName(), ]; } } From 4fd40d75004668b6920b5ac28b0f53543e5a35ea Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Wed, 21 Aug 2024 21:37:14 +0200 Subject: [PATCH 4/5] add test for classmap --- src/ClassScanner/ClassMap.php | 8 +++ tests/ClassScanner/ClassMapTest.php | 82 +++++++++++++++++++++++++++++ tests/Fake/FakeAllAttributes.php | 2 + tests/Fake/FakeWorkerAttribute.php | 2 + 4 files changed, 94 insertions(+) create mode 100644 tests/ClassScanner/ClassMapTest.php diff --git a/src/ClassScanner/ClassMap.php b/src/ClassScanner/ClassMap.php index 4ff367d..83fde8d 100644 --- a/src/ClassScanner/ClassMap.php +++ b/src/ClassScanner/ClassMap.php @@ -134,8 +134,16 @@ public static function fromFile(string $filename): self */ public static function fromFileHandle($fileHandle): ClassMap { + \fseek($fileHandle, 0); $cacheContents = \stream_get_contents($fileHandle); + if ($cacheContents === '') { + throw new \InvalidArgumentException('Cannot read empty file handle'); + } + $cacheContentsJson = \json_decode($cacheContents, true, 512, \JSON_THROW_ON_ERROR); + if (!$cacheContentsJson) { + throw new \InvalidArgumentException('Cannot parse json from file handle'); + } $classMap = new ClassMap($cacheContentsJson['scanPaths'], $cacheContentsJson['basePath']); diff --git a/tests/ClassScanner/ClassMapTest.php b/tests/ClassScanner/ClassMapTest.php new file mode 100644 index 0000000..2dc9d81 --- /dev/null +++ b/tests/ClassScanner/ClassMapTest.php @@ -0,0 +1,82 @@ +yieldAttributes(FakeAllAttributes::class)]; + $spec = new ClassSpecification(FakeAllAttributes::class, FakeAllAttributes::FILE, $attributes); + + $map = new ClassMap([], '/'); + $map->addClass($spec); + + $this->assertContains(FakeAllAttributes::class, $map->getClasses()); + $this->assertContains(FakeAllAttributes::FILE, $map->getFiles()); + $this->assertNotNull($map->getClassSpecificationFor(FakeAllAttributes::class)); + $this->assertSame($spec, $map->getClassSpecificationFor(FakeAllAttributes::class)); + $this->assertCount(\count($attributes), $map->getAttributeSpecifications()); + } + + public function testAttributeClass(): void + { + $reflector = new Reflector(); + $spec1 = new ClassSpecification( + FakeAllAttributes::class, + FakeAllAttributes::FILE, + [...$reflector->yieldAttributes(FakeAllAttributes::class)] + ); + $spec2 = new ClassSpecification( + FakeWorkerAttribute::class, + FakeWorkerAttribute::FILE, + [...$reflector->yieldAttributes(FakeWorkerAttribute::class)] + ); + + $map = new ClassMap([], '/'); + $map->addClass($spec1); + $map->addClass($spec2); + + $this->assertFalse($map->isAttributeClassFile(FakeAllAttributes::FILE)); + $this->assertTrue($map->isAttributeClassFile(FakeWorkerAttribute::FILE)); + } + + public function testReconstituteFromFileHandle(): void + { + $reflector = new Reflector(); + $spec1 = new ClassSpecification( + FakeAllAttributes::class, + FakeAllAttributes::FILE, + [...$reflector->yieldAttributes(FakeAllAttributes::class)] + ); + $spec2 = new ClassSpecification( + FakeWorkerAttribute::class, + FakeWorkerAttribute::FILE, + [...$reflector->yieldAttributes(FakeWorkerAttribute::class)] + ); + + $stream = fopen('php://temp', 'w'); + + $map1 = new ClassMap([], '/'); + $map1->addClass($spec1); + $map1->addClass($spec2); + $map1->saveToFileHandle($stream); + + $map2 = ClassMap::fromFileHandle($stream); + + $this->assertContains(FakeAllAttributes::class, $map2->getClasses()); + $this->assertContains(FakeAllAttributes::FILE, $map2->getFiles()); + $this->assertContains(FakeWorkerAttribute::class, $map2->getClasses()); + $this->assertContains(FakeWorkerAttribute::FILE, $map2->getFiles()); + $this->assertCount(\count($spec1->getAttributes()), $map2->getClassSpecificationFor(FakeAllAttributes::class)->getAttributes()); + $this->assertCount(\count($spec2->getAttributes()), $map2->getClassSpecificationFor(FakeWorkerAttribute::class)->getAttributes()); + } +} \ No newline at end of file diff --git a/tests/Fake/FakeAllAttributes.php b/tests/Fake/FakeAllAttributes.php index fbb28f3..954da3c 100644 --- a/tests/Fake/FakeAllAttributes.php +++ b/tests/Fake/FakeAllAttributes.php @@ -10,6 +10,8 @@ class FakeAllAttributes #[FakeAttribute(\Attribute::TARGET_CLASS_CONSTANT)] public const CONSTANT = 1; + public const FILE = __FILE__; + public function __construct( #[FakeAttribute(\Attribute::TARGET_PARAMETER)] $parameter, diff --git a/tests/Fake/FakeWorkerAttribute.php b/tests/Fake/FakeWorkerAttribute.php index 91008b1..2b09742 100644 --- a/tests/Fake/FakeWorkerAttribute.php +++ b/tests/Fake/FakeWorkerAttribute.php @@ -13,6 +13,8 @@ #[BlueprintNamespace(__NAMESPACE__)] class FakeWorkerAttribute implements AttributeConfigInterface { + public const FILE = __FILE__; + private int $someSetting; public function __construct(int $someSetting = 1) From 6efb9e6950d5d03194e793dd63f51dfa00cad6f5 Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Wed, 21 Aug 2024 21:44:33 +0200 Subject: [PATCH 5/5] add test for class map generator --- src/ClassScanner/ComposerMapGenerator.php | 3 +- .../ClassScanner/ComposerMapGeneratorTest.php | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/ClassScanner/ComposerMapGeneratorTest.php diff --git a/src/ClassScanner/ComposerMapGenerator.php b/src/ClassScanner/ComposerMapGenerator.php index 4798900..2e2f832 100644 --- a/src/ClassScanner/ComposerMapGenerator.php +++ b/src/ClassScanner/ComposerMapGenerator.php @@ -79,7 +79,6 @@ public function update(ClassMap $classMap, array $updatedFiles): ClassMap $deleted = []; $skip = []; - $generator = new ClassMapGenerator(); foreach ($classMap->getFiles() as $file) { if (!\in_array($file, $updatedFiles, true)) { $skip[$file] = true; @@ -94,6 +93,8 @@ public function update(ClassMap $classMap, array $updatedFiles): ClassMap $fileList = new FileList(); $fileList->files = $skip; + + $generator = new ClassMapGenerator(); $generator->avoidDuplicateScans($fileList); foreach ($this->paths as $path) { diff --git a/tests/ClassScanner/ComposerMapGeneratorTest.php b/tests/ClassScanner/ComposerMapGeneratorTest.php new file mode 100644 index 0000000..a26d1b1 --- /dev/null +++ b/tests/ClassScanner/ComposerMapGeneratorTest.php @@ -0,0 +1,79 @@ +dir = \sys_get_temp_dir() . '/aura_di_' . \bin2hex(random_bytes(4)); + \mkdir($this->dir); + } + + protected function tearDown(): void + { + parent::tearDown(); + $files = new \FilesystemIterator($this->dir); + + foreach ($files as $fileinfo) { + \unlink($fileinfo->getPathname()); + } + + \rmdir($this->dir); + } + + public function testAddingClass() + { + $generator = new ComposerMapGenerator([__DIR__ . '/../Fake', $this->dir]); + + $classSuffix = \bin2hex(\random_bytes(4)); + $newClassName = 'CacheTest\\NewFile' . $classSuffix; + + $classMap = $generator->generate(); + $this->assertNotContains($newClassName, $classMap->getClasses()); + + $newFile = $this->createRandomClassFile($newClassName); + + $classMap2 = $generator->update($classMap, [$newFile]); + $this->assertContains($newClassName, $classMap2->getClasses()); + $this->assertCount(1, $classMap2->getClassSpecificationFor($newClassName)->getAttributes()); + } + + public function testRemovingClass() + { + $generator = new ComposerMapGenerator([ + __DIR__ . '/../Fake', + $this->dir + ]); + + $classSuffix = \bin2hex(\random_bytes(4)); + $newClassName = 'CacheTest\\NewFile' . $classSuffix; + $this->createRandomClassFile($newClassName); + + $classMap = $generator->generate(); + $this->assertContains($newClassName, $classMap->getClasses()); + + \unlink($this->dir . '/NewFile' . $classSuffix . '.php'); + + $classMap2 = $generator->update($classMap, [$this->dir . '/NewFile' . $classSuffix . '.php']); + $this->assertNotContains($newClassName, $classMap2->getClasses()); + } + + private function createRandomClassFile(string $className, int $value = 0): string + { + $bareName = \array_reverse(\explode('\\', $className))[0]; + $phpFile = $this->dir . '/' . $bareName . '.php'; + $phpClass = "