From 51bb9faf6b35f1998d7cf3536ae93695bdcad2a6 Mon Sep 17 00:00:00 2001 From: Olivier Laviale Date: Thu, 21 Nov 2024 12:10:55 +0100 Subject: [PATCH] Use reflection (alt) --- CHANGELOG.md | 1 + README.md | 31 ++- docker-compose.yaml | 2 +- phpcs.xml | 2 + phpstan.neon | 2 +- src/ClassAttributeCollector.php | 28 ++- src/Collection.php | 178 +------------- src/Config.php | 8 +- src/Datastore/FileDatastore.php | 6 +- src/MemoizeAttributeCollector.php | 20 +- src/Plugin.php | 24 +- src/Reflexive/Collection.php | 198 ++++++++++++++++ src/Reflexive/TransientCollectionRenderer.php | 82 +++++++ src/Static/Collection.php | 222 ++++++++++++++++++ src/Static/TransientCollectionRenderer.php | 76 ++++++ src/TransientCollectionRenderer.php | 65 +---- tests/ClassAttributeCollectorTest.php | 2 +- tests/ConfigTest.php | 1 + tests/FileDatastoreTest.php | 12 +- tests/MemoizeClassMapGeneratorTest.php | 2 +- tests/PluginTest.php | 1 + tests/{ => Static}/CollectionTest.php | 4 +- 22 files changed, 701 insertions(+), 266 deletions(-) create mode 100644 src/Reflexive/Collection.php create mode 100644 src/Reflexive/TransientCollectionRenderer.php create mode 100644 src/Static/Collection.php create mode 100644 src/Static/TransientCollectionRenderer.php rename tests/{ => Static}/CollectionTest.php (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e59b4..26fdb27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ None ### New features - The `InheritsAttributes` attribute can be used on classes that inherit their attributes from traits, properties, or methods, and were previously ignored by the collection process. +- The plugin can generate a file that uses reflection to create attributes instead of embedding their arguments. ### Backward Incompatible Changes diff --git a/README.md b/README.md index 36e91d2..4ec52f0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ _discover_ attribute targets in a codebase—for known targets you can use refle #### Features - Little configuration -- No reflection in the generated file +- No reflection in the generated file (as an option) - No impact on performance - No dependency (except Composer of course) - A single interface to get attribute targets: classes, methods, and properties @@ -77,6 +77,10 @@ var_dump($attributes->methodsAttributes); var_dump($attributes->propertyAttributes); ``` +> [!NOTE] +> The plugin supports class, method, and property targets. [Contribute](CONTRIBUTING.md) if you're +> interested in expending its support. + ## Getting started @@ -192,6 +196,30 @@ replaced with the path to the vendor folder. } ``` +### Use reflection + +> [!NOTE] +> New in v2.1 + +By default, the "attributes" file embeds arguments to instantiate attributes without using +reflection. This can cause issues when the arguments are sophisticated types that don't support var +export. Alternatively, attributes can be instantiated using reflection. + +The API remains the same whether reflections are used or not. You can safely switch between modes +and see what works best for you. + +Use the `use-reflection` property for the "attributes" file to use reflection. + +```json +{ + "extra": { + "composer-attribute-collector": { + "use-reflection": true + } + } +} +``` + ### Cache discoveries between runs The plugin is able to maintain a cache to reuse discoveries between runs. To enable the cache, @@ -205,6 +233,7 @@ COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=1 composer dump-autoload + ## Test drive with the Symfony Demo You can try the plugin with a fresh installation of the [Symfony Demo Application](https://github.com/symfony/demo). diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ab81ef..25c7cfe 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -37,7 +37,7 @@ services: build: context: . args: - PHP_TAG: '8.4.0RC4-cli-bookworm' + PHP_TAG: '8.4-cli-bookworm' environment: PHP_IDE_CONFIG: 'serverName=olvlvl/attribute-collector' volumes: diff --git a/phpcs.xml b/phpcs.xml index aff2a22..436c444 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -18,6 +18,8 @@ tests/bootstrap.php + src/Static/TransientCollectionRenderer.php + src/Reflexive/TransientCollectionRenderer.php tests/* diff --git a/phpstan.neon b/phpstan.neon index fac0ff7..0e74baa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ parameters: bootstrapFiles: - tests/bootstrap.php - level: max + level: 9 paths: - src diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 323c3f1..ab69c7c 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -13,8 +13,14 @@ */ class ClassAttributeCollector { + /** + * @param bool $ignoreArguments + * Attribute arguments aren't used when generating a file that uses reflection. + * Setting `$ignoreArguments` to `true` ignores arguments during the attribute collection. + */ public function __construct( private IOInterface $io, + private bool $ignoreArguments, ) { } @@ -49,7 +55,7 @@ public function collectAttributes(string $class): array $classAttributes[] = new TransientTargetClass( $attribute->getName(), - $attribute->getArguments(), + $this->getArguments($attribute), ); } @@ -67,7 +73,7 @@ public function collectAttributes(string $class): array $methodAttributes[] = new TransientTargetMethod( $attribute->getName(), - $attribute->getArguments(), + $this->getArguments($attribute), $method, ); } @@ -88,7 +94,7 @@ public function collectAttributes(string $class): array $propertyAttributes[] = new TransientTargetProperty( $attribute->getName(), - $attribute->getArguments(), + $this->getArguments($attribute), $property, ); } @@ -123,6 +129,20 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool InheritsAttributes::class => true, ]; - return isset($ignored[$attribute->getName()]); // @phpstan-ignore offsetAccess.nonOffsetAccessible + return isset($ignored[$attribute->getName()]); + } + + /** + * @param ReflectionAttribute $attribute + * + * @return array + */ + private function getArguments(ReflectionAttribute $attribute): array + { + if ($this->ignoreArguments) { + return []; + } + + return $attribute->getArguments(); } } diff --git a/src/Collection.php b/src/Collection.php index 87f1d85..79ebc4c 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -2,34 +2,8 @@ namespace olvlvl\ComposerAttributeCollector; -use RuntimeException; -use Throwable; - -use function array_map; - -/** - * @internal - */ -final class Collection +abstract class Collection { - /** - * @param array> $targetClasses - * Where _key_ is an attribute class and _value_ an array of arrays - * where 0 are the attribute arguments and 1 is a target class. - * @param array> $targetMethods - * Where _key_ is an attribute class and _value_ an array of arrays - * where 0 are the attribute arguments, 1 is a target class, and 2 is the target method. - * @param array> $targetProperties - * Where _key_ is an attribute class and _value_ an array of arrays - * where 0 are the attribute arguments, 1 is a target class, and 2 is the target property. - */ - public function __construct( - private array $targetClasses, - private array $targetMethods, - private array $targetProperties, - ) { - } - /** * @template T of object * @@ -37,34 +11,7 @@ public function __construct( * * @return array> */ - public function findTargetClasses(string $attribute): array - { - return array_map( - fn(array $a) => new TargetClass(self::createClassAttribute($attribute, ...$a), $a[1]), - $this->targetClasses[$attribute] ?? [] - ); - } - - /** - * @template T of object - * - * @param class-string $attribute - * @param array $arguments - * @param class-string $class - * - * @return T - */ - private static function createClassAttribute(string $attribute, array $arguments, string $class): object - { - try { - return new $attribute(...$arguments); - } catch (Throwable $e) { - throw new RuntimeException( - "An error occurred while instantiating attribute $attribute on class $class", - previous: $e - ); - } - } + abstract public function findTargetClasses(string $attribute): array; /** * @template T of object @@ -73,38 +20,7 @@ private static function createClassAttribute(string $attribute, array $arguments * * @return array> */ - public function findTargetMethods(string $attribute): array - { - return array_map( - fn(array $a) => new TargetMethod(self::createMethodAttribute($attribute, ...$a), $a[1], $a[2]), - $this->targetMethods[$attribute] ?? [] - ); - } - - /** - * @template T of object - * - * @param class-string $attribute - * @param array $arguments - * @param class-string $class - * - * @return T - */ - private static function createMethodAttribute( - string $attribute, - array $arguments, - string $class, - string $method - ): object { - try { - return new $attribute(...$arguments); - } catch (Throwable $e) { - throw new RuntimeException( - "An error occurred while instantiating attribute $attribute on method $class::$method", - previous: $e - ); - } - } + abstract public function findTargetMethods(string $attribute): array; /** * @template T of object @@ -113,108 +29,28 @@ private static function createMethodAttribute( * * @return array> */ - public function findTargetProperties(string $attribute): array - { - return array_map( - fn(array $a) => new TargetProperty(self::createPropertyAttribute($attribute, ...$a), $a[1], $a[2]), - $this->targetProperties[$attribute] ?? [] - ); - } - - /** - * @template T of object - * - * @param class-string $attribute - * @param array $arguments - * @param class-string $class - * - * @return T - */ - private static function createPropertyAttribute( - string $attribute, - array $arguments, - string $class, - string $property - ): object { - try { - return new $attribute(...$arguments); - } catch (Throwable $e) { - throw new RuntimeException( - "An error occurred while instantiating attribute $attribute on property $class::$property", - previous: $e - ); - } - } + abstract public function findTargetProperties(string $attribute): array; /** * @param callable(class-string $attribute, class-string $class):bool $predicate * * @return array> */ - public function filterTargetClasses(callable $predicate): array - { - $ar = []; - - foreach ($this->targetClasses as $attribute => $references) { - foreach ($references as [ $arguments, $class ]) { - if ($predicate($attribute, $class)) { - $ar[] = new TargetClass(self::createClassAttribute($attribute, $arguments, $class), $class); - } - } - } - - return $ar; - } + abstract public function filterTargetClasses(callable $predicate): array; /** * @param callable(class-string $attribute, class-string $class, non-empty-string $method):bool $predicate * * @return array> */ - public function filterTargetMethods(callable $predicate): array - { - $ar = []; - - foreach ($this->targetMethods as $attribute => $references) { - foreach ($references as [ $arguments, $class, $method ]) { - if ($predicate($attribute, $class, $method)) { - $ar[] = new TargetMethod(self::createMethodAttribute( - $attribute, - $arguments, - $class, - $method - ), $class, $method); - } - } - } - - return $ar; - } + abstract public function filterTargetMethods(callable $predicate): array; /** * @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate * * @return array> */ - public function filterTargetProperties(callable $predicate): array - { - $ar = []; - - foreach ($this->targetProperties as $attribute => $references) { - foreach ($references as [ $arguments, $class, $property ]) { - if ($predicate($attribute, $class, $property)) { - $ar[] = new TargetProperty(self::createPropertyAttribute( - $attribute, - $arguments, - $class, - $property - ), $class, $property); - } - } - } - - return $ar; - } + abstract public function filterTargetProperties(callable $predicate): array; /** * @param class-string $class diff --git a/src/Config.php b/src/Config.php index 1f763de..5b6c86a 100644 --- a/src/Config.php +++ b/src/Config.php @@ -30,6 +30,7 @@ final class Config public const EXTRA = 'composer-attribute-collector'; public const EXTRA_INCLUDE = 'include'; public const EXTRA_EXCLUDE = 'exclude'; + public const EXTRA_USE_REFLECTION = 'use-reflection'; public const ENV_USE_CACHE = 'COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE'; /** @@ -49,11 +50,12 @@ public static function from(PartialComposer $composer): self $rootDir .= DIRECTORY_SEPARATOR; - /** @var array{ include?: non-empty-string[], exclude?: non-empty-string[] } $extra */ + /** @var array{ include?: non-empty-string[], exclude?: non-empty-string[], use-reflection?: bool } $extra */ $extra = $composer->getPackage()->getExtra()[self::EXTRA] ?? []; $include = self::expandPaths($extra[self::EXTRA_INCLUDE] ?? [], $vendorDir, $rootDir); $exclude = self::expandPaths($extra[self::EXTRA_EXCLUDE] ?? [], $vendorDir, $rootDir); + $useReflection = $extra[self::EXTRA_USE_REFLECTION] ?? false; $useCache = filter_var(Platform::getEnv(self::ENV_USE_CACHE), FILTER_VALIDATE_BOOL); @@ -63,6 +65,7 @@ public static function from(PartialComposer $composer): self include: $include, exclude: $exclude, useCache: $useCache, + useReflection: $useReflection, ); } @@ -95,6 +98,8 @@ public static function resolveVendorDir(PartialComposer $composer): string * Paths that should be excluded from the attribute collection. * @param bool $useCache * Whether a cache should be used during the process. + * @param bool $useReflection + * Whether the generated file should embed the arguments to instantiate attributes or use reflection instead. */ public function __construct( public string $vendorDir, @@ -102,6 +107,7 @@ public function __construct( public array $include, public array $exclude, public bool $useCache, + public bool $useReflection, ) { $this->excludeRegExp = count($exclude) ? self::compileExclude($this->exclude) : null; } diff --git a/src/Datastore/FileDatastore.php b/src/Datastore/FileDatastore.php index c6d36e4..3f7318b 100644 --- a/src/Datastore/FileDatastore.php +++ b/src/Datastore/FileDatastore.php @@ -30,6 +30,7 @@ final class FileDatastore implements Datastore public function __construct( private string $dir, private IOInterface $io, + private bool $useReflection, ) { if (!is_dir($dir)) { mkdir($dir); @@ -86,11 +87,12 @@ private function safeGet(string $filename): array return $ar; } - private function formatFilename(string $key): string + public function formatFilename(string $key): string { $major = Plugin::VERSION_MAJOR; $minor = Plugin::VERSION_MINOR; + $mode = $this->useReflection ? 'r' : 's'; - return $this->dir . DIRECTORY_SEPARATOR . "v$major-$minor-$key"; + return $this->dir . DIRECTORY_SEPARATOR . "v$major-$minor-$mode-$key"; } } diff --git a/src/MemoizeAttributeCollector.php b/src/MemoizeAttributeCollector.php index 24c9c66..116c656 100644 --- a/src/MemoizeAttributeCollector.php +++ b/src/MemoizeAttributeCollector.php @@ -59,7 +59,7 @@ public function collectAttributes(array $classMap): TransientCollection $timestamp, $classAttributes, $methodAttributes, - $propertyAttributes + $propertyAttributes, ] = $this->state[$class] ?? [ 0, [], [], [] ]; $mtime = filemtime($filepath); @@ -76,20 +76,26 @@ public function collectAttributes(array $classMap): TransientCollection [ $classAttributes, $methodAttributes, - $propertyAttributes + $propertyAttributes, ] = $classAttributeCollector->collectAttributes($class); } catch (Throwable $e) { $this->io->error( - "Attribute collection failed for $class: {$e->getMessage()}" + "Attribute collection failed for $class: {$e->getMessage()}", ); } $this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ]; } - $collector->addClassAttributes($class, $classAttributes); - $collector->addMethodAttributes($class, $methodAttributes); - $collector->addTargetProperties($class, $propertyAttributes); + if (count($classAttributes)) { + $collector->addClassAttributes($class, $classAttributes); + } + if (count($methodAttributes)) { + $collector->addMethodAttributes($class, $methodAttributes); + } + if (count($propertyAttributes)) { + $collector->addTargetProperties($class, $propertyAttributes); + } } /** @@ -98,7 +104,7 @@ public function collectAttributes(array $classMap): TransientCollection $this->state = array_filter( $this->state, static fn(string $k): bool => $filterClasses[$k] ?? false, - ARRAY_FILTER_USE_KEY + ARRAY_FILTER_USE_KEY, ); $this->datastore->set(self::KEY, $this->state); diff --git a/src/Plugin.php b/src/Plugin.php index 113cf09..b07259f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -27,7 +27,7 @@ final class Plugin implements PluginInterface, EventSubscriberInterface { public const CACHE_DIR = '.composer-attribute-collector'; public const VERSION_MAJOR = 2; - public const VERSION_MINOR = 0; + public const VERSION_MINOR = 1; /** * @uses onPostAutoloadDump @@ -125,7 +125,11 @@ public static function dump(Config $config, IOInterface $io): void // Collect attributes // $start = microtime(true); - $attributeCollector = new MemoizeAttributeCollector(new ClassAttributeCollector($io), $datastore, $io); + $attributeCollector = new MemoizeAttributeCollector( + new ClassAttributeCollector($io, $config->useReflection), + $datastore, + $io, + ); $collection = $attributeCollector->collectAttributes($classMap); $elapsed = self::renderElapsedTime($start); $io->debug("Generating attributes file: collected attributes in $elapsed"); @@ -134,7 +138,7 @@ public static function dump(Config $config, IOInterface $io): void // Render attributes // $start = microtime(true); - $code = self::render($collection); + $code = self::render($collection, $config->useReflection); file_put_contents($config->attributesFile, $code); $elapsed = self::renderElapsedTime($start); $io->debug("Generating attributes file: rendered code in $elapsed"); @@ -150,7 +154,11 @@ private static function buildDefaultDatastore(Config $config, IOInterface $io): assert($basePath !== ''); - return new FileDatastore($basePath . DIRECTORY_SEPARATOR . self::CACHE_DIR, $io); + return new FileDatastore( + $basePath . DIRECTORY_SEPARATOR . self::CACHE_DIR, + $io, + $config->useReflection, + ); } private static function renderElapsedTime(float $start): string @@ -166,8 +174,12 @@ private static function buildFileFilter(): Filter ]); } - private static function render(TransientCollection $collector): string + private static function render(TransientCollection $collector, bool $useReflection): string { - return TransientCollectionRenderer::render($collector); + if ($useReflection) { + return Reflexive\TransientCollectionRenderer::render($collector); + } + + return Static\TransientCollectionRenderer::render($collector); } } diff --git a/src/Reflexive/Collection.php b/src/Reflexive/Collection.php new file mode 100644 index 0000000..e7e8ca4 --- /dev/null +++ b/src/Reflexive/Collection.php @@ -0,0 +1,198 @@ +, + * methods?: array, + * properties?: array + * }> $attributes + */ + public function __construct( + private array $attributes, + ) { + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function findTargetClasses(string $attribute): array + { + $t = []; + + foreach ($this->attributes[$attribute]['classes'] ?? [] as $class) { + $t[] = $this->createTargetClasses($attribute, $class); + } + + return array_merge(...$t); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param class-string $class + * + * @return array> + * @throws ReflectionException + */ + private function createTargetClasses(string $attribute, string $class): array + { + $t = []; + $reflection = new ReflectionClass($class); + + foreach ($reflection->getAttributes($attribute) as $reflectionAttribute) { + $t[] = new TargetClass($reflectionAttribute->newInstance(), $class); + } + + return $t; + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function findTargetMethods(string $attribute): array + { + $t = []; + + foreach ($this->attributes[$attribute]['methods'] ?? [] as [ $class, $method ]) { + $t[] = $this->createTargetMethods($attribute, $class, $method); + } + + return array_merge(...$t); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param class-string $class + * @param non-empty-string $method + * + * @return array> + * @throws ReflectionException + */ + private function createTargetMethods(string $attribute, string $class, string $method): array + { + $t = []; + + $reflection = new \ReflectionMethod($class, $method); + + foreach ($reflection->getAttributes($attribute) as $reflectionAttribute) { + $t[] = new TargetMethod($reflectionAttribute->newInstance(), $class, $method); + } + + return $t; + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function findTargetProperties(string $attribute): array + { + $t = []; + + foreach ($this->attributes[$attribute]['properties'] ?? [] as [ $class, $property ]) { + $t[] = $this->createTargetProperties($attribute, $class, $property); + } + + return array_merge(...$t); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param class-string $class + * @param non-empty-string $property + * + * @return array> + * @throws ReflectionException + */ + private function createTargetProperties(string $attribute, string $class, string $property): array + { + $t = []; + + $reflection = new \ReflectionProperty($class, $property); + + foreach ($reflection->getAttributes($attribute) as $reflectionAttribute) { + $t[] = new TargetProperty($reflectionAttribute->newInstance(), $class, $property); + } + + return $t; + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function filterTargetClasses(callable $predicate): array + { + $t = []; + + foreach (array_keys($this->attributes) as $attribute) { + foreach ($this->attributes[$attribute]['classes'] ?? [] as $class) { + if ($predicate($attribute, $class)) { + $t[] = $this->createTargetClasses($attribute, $class); + } + } + } + + return array_merge(...$t); + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function filterTargetMethods(callable $predicate): array + { + $t = []; + + foreach (array_keys($this->attributes) as $attribute) { + foreach ($this->attributes[$attribute]['methods'] ?? [] as [ $class, $method ]) { + if ($predicate($attribute, $class, $method)) { + $t[] = $this->createTargetMethods($attribute, $class, $method); + } + } + } + + return array_merge(...$t); + } + + /** + * @inheritdoc + * @throws ReflectionException + */ + public function filterTargetProperties(callable $predicate): array + { + $t = []; + + foreach (array_keys($this->attributes) as $attribute) { + foreach ($this->attributes[$attribute]['properties'] ?? [] as [ $class, $properties ]) { + if ($predicate($attribute, $class, $properties)) { + $t[] = $this->createTargetProperties($attribute, $class, $properties); + } + } + } + + return array_merge(...$t); + } +} diff --git a/src/Reflexive/TransientCollectionRenderer.php b/src/Reflexive/TransientCollectionRenderer.php new file mode 100644 index 0000000..cba3dc1 --- /dev/null +++ b/src/Reflexive/TransientCollectionRenderer.php @@ -0,0 +1,82 @@ + new \olvlvl\ComposerAttributeCollector\Reflexive\Collection( + $rendered_attributes + )); + PHP; + } + + /** + * @return array, + * methods?: array, + * properties?: array + * }> + */ + private static function index_by_attributes(TransientCollection $collector): array + { + $by_attributes = []; + + foreach ($collector->classes as $class => $targets) { + foreach ($targets as $target) { + $by_attributes[$target->attribute]['classes'][] = $class; + } + } + + foreach ($collector->methods as $class => $targets) { + foreach ($targets as $target) { + $by_attributes[$target->attribute]['methods'][] = [ $class, $target->name ]; + } + } + + foreach ($collector->properties as $class => $targets) { + foreach ($targets as $target) { + $by_attributes[$target->attribute]['properties'][] = [ $class, $target->name ]; + } + } + + array_walk($by_attributes, function (&$value) { + foreach ($value as &$v) { + $v = array_unique($v, SORT_REGULAR); + } + }); + + ksort($by_attributes); + + return $by_attributes; // @phpstan-ignore return.type + } + + /** + * @param array, + * methods?: array, + * properties?: array + * }> $index_by_attributes + */ + private static function render_attributes(array $index_by_attributes): string + { + $r = var_export($index_by_attributes, true); + $r = str_replace('\\\\', '\\', $r); + $r = str_replace('array (', '[', $r); + $r = str_replace(')', ']', $r); + + return $r; + } +} diff --git a/src/Static/Collection.php b/src/Static/Collection.php new file mode 100644 index 0000000..3c1cbcd --- /dev/null +++ b/src/Static/Collection.php @@ -0,0 +1,222 @@ +> $targetClasses + * Where _key_ is an attribute class and _value_ an array of arrays + * where 0 are the attribute arguments and 1 is a target class. + * @param array> $targetMethods + * Where _key_ is an attribute class and _value_ an array of arrays + * where 0 are the attribute arguments, 1 is a target class, and 2 is the target method. + * @param array> $targetProperties + * Where _key_ is an attribute class and _value_ an array of arrays + * where 0 are the attribute arguments, 1 is a target class, and 2 is the target property. + */ + public function __construct( + private array $targetClasses, + private array $targetMethods, + private array $targetProperties, + ) { + } + + /** + * @template T of object + * + * @param class-string $attribute + * + * @return array> + */ + public function findTargetClasses(string $attribute): array + { + return array_map( + fn(array $a) => new TargetClass(self::createClassAttribute($attribute, ...$a), $a[1]), + $this->targetClasses[$attribute] ?? [] + ); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param array $arguments + * @param class-string $class + * + * @return T + */ + private static function createClassAttribute(string $attribute, array $arguments, string $class): object + { + try { + return new $attribute(...$arguments); + } catch (Throwable $e) { + throw new RuntimeException( + "An error occurred while instantiating attribute $attribute on class $class", + previous: $e + ); + } + } + + /** + * @template T of object + * + * @param class-string $attribute + * + * @return array> + */ + public function findTargetMethods(string $attribute): array + { + return array_map( + fn(array $a) => new TargetMethod(self::createMethodAttribute($attribute, ...$a), $a[1], $a[2]), + $this->targetMethods[$attribute] ?? [] + ); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param array $arguments + * @param class-string $class + * + * @return T + */ + private static function createMethodAttribute( + string $attribute, + array $arguments, + string $class, + string $method + ): object { + try { + return new $attribute(...$arguments); + } catch (Throwable $e) { + throw new RuntimeException( + "An error occurred while instantiating attribute $attribute on method $class::$method", + previous: $e + ); + } + } + + /** + * @template T of object + * + * @param class-string $attribute + * + * @return array> + */ + public function findTargetProperties(string $attribute): array + { + return array_map( + fn(array $a) => new TargetProperty(self::createPropertyAttribute($attribute, ...$a), $a[1], $a[2]), + $this->targetProperties[$attribute] ?? [] + ); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param array $arguments + * @param class-string $class + * + * @return T + */ + private static function createPropertyAttribute( + string $attribute, + array $arguments, + string $class, + string $property + ): object { + try { + return new $attribute(...$arguments); + } catch (Throwable $e) { + throw new RuntimeException( + "An error occurred while instantiating attribute $attribute on property $class::$property", + previous: $e + ); + } + } + + /** + * @param callable(class-string $attribute, class-string $class):bool $predicate + * + * @return array> + */ + public function filterTargetClasses(callable $predicate): array + { + $ar = []; + + foreach ($this->targetClasses as $attribute => $references) { + foreach ($references as [ $arguments, $class ]) { + if ($predicate($attribute, $class)) { + $ar[] = new TargetClass(self::createClassAttribute($attribute, $arguments, $class), $class); + } + } + } + + return $ar; + } + + /** + * @param callable(class-string $attribute, class-string $class, non-empty-string $method):bool $predicate + * + * @return array> + */ + public function filterTargetMethods(callable $predicate): array + { + $ar = []; + + foreach ($this->targetMethods as $attribute => $references) { + foreach ($references as [ $arguments, $class, $method ]) { + if ($predicate($attribute, $class, $method)) { + $ar[] = new TargetMethod(self::createMethodAttribute( + $attribute, + $arguments, + $class, + $method + ), $class, $method); + } + } + } + + return $ar; + } + + /** + * @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate + * + * @return array> + */ + public function filterTargetProperties(callable $predicate): array + { + $ar = []; + + foreach ($this->targetProperties as $attribute => $references) { + foreach ($references as [ $arguments, $class, $property ]) { + if ($predicate($attribute, $class, $property)) { + $ar[] = new TargetProperty(self::createPropertyAttribute( + $attribute, + $arguments, + $class, + $property + ), $class, $property); + } + } + } + + return $ar; + } +} diff --git a/src/Static/TransientCollectionRenderer.php b/src/Static/TransientCollectionRenderer.php new file mode 100644 index 0000000..b38b7ec --- /dev/null +++ b/src/Static/TransientCollectionRenderer.php @@ -0,0 +1,76 @@ +classes); + $targetMethodsCode = self::targetsToCode($collector->methods); + $targetPropertiesCode = self::targetsToCode($collector->properties); + + return << new \olvlvl\ComposerAttributeCollector\Static\Collection( + targetClasses: $targetClassesCode, + targetMethods: $targetMethodsCode, + targetProperties: $targetPropertiesCode, + )); + PHP; + } + + /** + * //phpcs:disable Generic.Files.LineLength.TooLong + * @param iterable> $targetByClass + * + * @return string + */ + private static function targetsToCode(iterable $targetByClass): string + { + $array = self::targetsToArray($targetByClass); + + return var_export($array, true); + } + + /** + * //phpcs:disable Generic.Files.LineLength.TooLong + * @param iterable> $targetByClass + * + * @return array, class-string, 2?:non-empty-string }>> + */ + private static function targetsToArray(iterable $targetByClass): array + { + $by = []; + + foreach ($targetByClass as $class => $targets) { + foreach ($targets as $t) { + $a = [ $t->arguments, $class ]; + + if ($t instanceof TransientTargetMethod || $t instanceof TransientTargetProperty) { + $a[] = $t->name; + } + + $by[$t->attribute][] = $a; + } + } + + return $by; + } +} diff --git a/src/TransientCollectionRenderer.php b/src/TransientCollectionRenderer.php index 60f8018..241e877 100644 --- a/src/TransientCollectionRenderer.php +++ b/src/TransientCollectionRenderer.php @@ -2,69 +2,10 @@ namespace olvlvl\ComposerAttributeCollector; -use function var_export; - -/** - * Renders collected attribute targets as PHP code. - * - * @internal - */ -final class TransientCollectionRenderer +interface TransientCollectionRenderer { - public static function render(TransientCollection $collector): string - { - $targetClassesCode = self::targetsToCode($collector->classes); - $targetMethodsCode = self::targetsToCode($collector->methods); - $targetPropertiesCode = self::targetsToCode($collector->properties); - - return << new \olvlvl\ComposerAttributeCollector\Collection( - targetClasses: $targetClassesCode, - targetMethods: $targetMethodsCode, - targetProperties: $targetPropertiesCode, - )); - PHP; - } - /** - * //phpcs:disable Generic.Files.LineLength.TooLong - * @param iterable> $targetByClass - * - * @return string + * @return string The rendered PHP code. */ - private static function targetsToCode(iterable $targetByClass): string - { - $array = self::targetsToArray($targetByClass); - - return var_export($array, true); - } - - /** - * //phpcs:disable Generic.Files.LineLength.TooLong - * @param iterable> $targetByClass - * - * @return array, class-string, 2?:non-empty-string }>> - */ - private static function targetsToArray(iterable $targetByClass): array - { - $by = []; - - foreach ($targetByClass as $class => $targets) { - foreach ($targets as $t) { - $a = [ $t->arguments, $class ]; - - if ($t instanceof TransientTargetMethod || $t instanceof TransientTargetProperty) { - $a[] = $t->name; - } - - $by[$t->attribute][] = $a; - } - } - - return $by; - } + public static function render(TransientCollection $collector): string; } diff --git a/tests/ClassAttributeCollectorTest.php b/tests/ClassAttributeCollectorTest.php index 6be3237..5089756 100644 --- a/tests/ClassAttributeCollectorTest.php +++ b/tests/ClassAttributeCollectorTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); - $this->sut = new ClassAttributeCollector(new NullIO()); + $this->sut = new ClassAttributeCollector(new NullIO(), ignoreArguments: false); } /** diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index b67136f..0c92326 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -58,6 +58,7 @@ public function testFrom(): void "$cwd/vendor/vendor1/package1/file.php", ], useCache: false, + useReflection: false, ); $actual = Config::from($composer); diff --git a/tests/FileDatastoreTest.php b/tests/FileDatastoreTest.php index 7f72063..8e9e797 100644 --- a/tests/FileDatastoreTest.php +++ b/tests/FileDatastoreTest.php @@ -24,7 +24,7 @@ final class FileDatastoreTest extends TestCase protected function setUp(): void { $this->io = $this->createMock(IOInterface::class); - $this->sut = new FileDatastore(self::DIR, $this->io); + $this->sut = new FileDatastore(self::DIR, $this->io, useReflection: false); parent::setUp(); } @@ -38,7 +38,7 @@ public function testValidUnserialize(): void ->method('warning') ->withAnyParameters(); - self::write($str); + $this->write($str); $actual = $this->sut->get(self::KEY); @@ -54,7 +54,7 @@ public function testUnserializeWithMissingEnum(): void ->method('warning') ->with($this->stringStartsWith("Unable to unserialize cache item")); - self::write($str); + $this->write($str); $actual = $this->sut->get(self::KEY); @@ -72,16 +72,16 @@ public function testUnserializeWithMissingClass(): void ->method('warning') ->with($this->stringStartsWith("Unable to unserialize cache item")); - self::write($str); + $this->write($str); $actual = $this->sut->get(self::KEY); $this->assertEquals($expected, $actual); } - private static function write(string $str): void + private function write(string $str): void { - $filename = self::DIR . 'v' . Plugin::VERSION_MAJOR . '-' . Plugin::VERSION_MINOR . '-' . self::KEY; + $filename = $this->sut->formatFilename(self::KEY); file_put_contents($filename, $str); } diff --git a/tests/MemoizeClassMapGeneratorTest.php b/tests/MemoizeClassMapGeneratorTest.php index 29169f7..eae3d17 100644 --- a/tests/MemoizeClassMapGeneratorTest.php +++ b/tests/MemoizeClassMapGeneratorTest.php @@ -102,7 +102,7 @@ private static function map(string $path): array { $io = new NullIO(); $generator = new MemoizeClassMapGenerator( - new FileDatastore(get_cache_dir(), $io), + new FileDatastore(get_cache_dir(), $io, useReflection: false), $io, ); diff --git a/tests/PluginTest.php b/tests/PluginTest.php index 5490bf0..f5df674 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -66,6 +66,7 @@ protected function setUp(): void ], exclude: $exclude, useCache: false, + useReflection: false, ); Plugin::dump( diff --git a/tests/CollectionTest.php b/tests/Static/CollectionTest.php similarity index 98% rename from tests/CollectionTest.php rename to tests/Static/CollectionTest.php index 4dfabbb..9b36f50 100644 --- a/tests/CollectionTest.php +++ b/tests/Static/CollectionTest.php @@ -1,6 +1,6 @@