From 69c1a6d6544e703a0e8a916b5e7c0478faf38f18 Mon Sep 17 00:00:00 2001 From: Markus Staab <47448731+clxmstaab@users.noreply.github.com> Date: Tue, 27 May 2025 18:29:04 +0200 Subject: [PATCH 01/11] Implement ParameterAttributeCollector --- README.md | 5 ++ src/Attributes.php | 22 +++++ src/ClassAttributeCollector.php | 12 ++- src/Collection.php | 76 +++++++++++++++- src/MemoizeAttributeCollector.php | 11 ++- src/ParameterAttributeCollector.php | 70 +++++++++++++++ src/Plugin.php | 2 +- src/TargetMethodParameter.php | 44 ++++++++++ src/TransientCollection.php | 15 ++++ src/TransientCollectionRenderer.php | 38 ++++++-- src/TransientTargetMethodParameter.php | 40 +++++++++ .../PSR4/Presentation/ArticleController.php | 14 +++ tests/Acme81/Attribute/ParameterA.php | 16 ++++ tests/Acme81/Attribute/ParameterB.php | 19 ++++ tests/Acme81/PSR4/AFunction.php | 12 +++ tests/ClassAttributeCollectorTest.php | 33 +++++++ tests/CollectionTest.php | 41 +++++++++ tests/PluginTest.php | 88 +++++++++++++++++++ 18 files changed, 544 insertions(+), 14 deletions(-) create mode 100644 src/ParameterAttributeCollector.php create mode 100644 src/TargetMethodParameter.php create mode 100644 src/TransientTargetMethodParameter.php create mode 100644 tests/Acme81/Attribute/ParameterA.php create mode 100644 tests/Acme81/Attribute/ParameterB.php create mode 100644 tests/Acme81/PSR4/AFunction.php diff --git a/README.md b/README.md index 7ebf647..1e2606c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,11 @@ foreach (Attributes::findTargetProperties(Column::class) as $target) { var_dump($target->attribute, $target->class, $target->name); } +// Find the target method-parameters of the UserInput attribute. +foreach (Attributes::findTargetMethodParameters(UserInput::class) as $target) { + var_dump($target->attribute, $target->class, $target->method, $target->name); +} + // Filter target methods using a predicate. // You can also filter target classes and properties. $predicate = fn($attribute) => is_a($attribute, Route::class, true); diff --git a/src/Attributes.php b/src/Attributes.php index cf8a91a..036ce8d 100644 --- a/src/Attributes.php +++ b/src/Attributes.php @@ -61,6 +61,18 @@ public static function findTargetProperties(string $attribute): array return self::getCollection()->findTargetProperties($attribute); } + /** + * @template T of object + * + * @param class-string $attribute + * + * @return TargetMethodParameter[] + */ + public static function findTargetMethodParameters(string $attribute): array + { + return self::getCollection()->findTargetMethodParameters($attribute); + } + /** * @param callable(class-string $attribute, class-string $class):bool $predicate * @@ -91,6 +103,16 @@ public static function filterTargetProperties(callable $predicate): array return self::getCollection()->filterTargetProperties($predicate); } + /** + * @param callable(class-string $attribute, class-string $class, string $property, string $method):bool $predicate + * + * @return array> + */ + public static function filterTargetMethodParameters(callable $predicate): array + { + return self::getCollection()->filterTargetMethodParameters($predicate); + } + /** * @param class-string $class * diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 11bd4f9..38182ce 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -45,6 +45,7 @@ public function __construct(IOInterface $io, Parser $parser) * array, * array, * array, + * array>, * } * * @throws ReflectionException @@ -54,7 +55,7 @@ public function collectAttributes(string $class): array $classReflection = new ReflectionClass($class); if ($this->isAttribute($classReflection)) { - return [ [], [], [] ]; + return [ [], [], [], [] ]; } $classAttributes = []; @@ -74,7 +75,9 @@ public function collectAttributes(string $class): array } $methodAttributes = []; + $methodParameterAttributes = []; + $parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->parser); foreach ($classReflection->getMethods() as $methodReflection) { foreach ($this->getMethodAttributes($methodReflection) as $attribute) { if (self::isAttributeIgnored($attribute->getName())) { @@ -91,6 +94,11 @@ public function collectAttributes(string $class): array $method, ); } + + $parameterAttributes = $parameterAttributeCollector->collectAttributes($methodReflection); + if ($parameterAttributes !== []) { + $methodParameterAttributes[$methodReflection->getName()] = $parameterAttributes; + } } $propertyAttributes = []; @@ -114,7 +122,7 @@ public function collectAttributes(string $class): array } } - return [ $classAttributes, $methodAttributes, $propertyAttributes ]; + return [ $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ]; } /** diff --git a/src/Collection.php b/src/Collection.php index f2fa0a2..3cc51e9 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -24,6 +24,10 @@ final class Collection * @var array> */ private array $targetProperties; + /** + * @var array> + */ + private array $targetMethodParameters; /** * @param array> $targetClasses * Where _key_ is an attribute class and _value_ an array of arrays @@ -34,12 +38,16 @@ final class Collection * @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. + * @param array> $targetMethodParameters + * Where _key_ is an attribute class and _value_ an array of arrays + * where 0 are the attribute arguments, 1 is a target class, 2 is the target method, and 3 is the target parameter. */ - public function __construct(array $targetClasses, array $targetMethods, array $targetProperties) + public function __construct(array $targetClasses, array $targetMethods, array $targetProperties, array $targetMethodParameters) { $this->targetClasses = $targetClasses; $this->targetMethods = $targetMethods; $this->targetProperties = $targetProperties; + $this->targetMethodParameters = $targetMethodParameters; } /** * @template T of object @@ -118,6 +126,46 @@ private static function createMethodAttribute(string $attribute, array $argument } } + /** + * @template T of object + * + * @param class-string $attribute + * + * @return array> + */ + public function findTargetMethodParameters(string $attribute): array + { + return array_map( + fn(array $t) => self::createMethodParameterAttribute($attribute, ...$t), + $this->targetMethodParameters[$attribute] ?? [], + ); + } + + /** + * @template T of object + * + * @param class-string $attribute + * @param array $arguments + * @param class-string $class + * @param non-empty-string $method + * @param non-empty-string $parameter + * + * @return TargetMethodParameter + */ + private static function createMethodParameterAttribute(string $attribute, array $arguments, string $class, string $method, string $parameter): object + { + try { + $a = new $attribute(...$arguments); + return new TargetMethodParameter($a, $class, $method, $parameter); + } catch (Throwable $e) { + throw new RuntimeException( + "An error occurred while instantiating attribute $attribute on method $class::$method($parameter)", + 0, + $e, + ); + } + } + /** * @template T of object * @@ -202,6 +250,32 @@ public function filterTargetMethods(callable $predicate): array return $ar; } + /** + * @param callable(class-string $attribute, class-string $class, non-empty-string $method, non-empty-string $parameter):bool $predicate + * + * @return array> + */ + public function filterTargetMethodParameters(callable $predicate): array + { + $ar = []; + + foreach ($this->targetMethodParameters as $attribute => $references) { + foreach ($references as [$arguments, $class, $method, $parameter]) { + if ($predicate($attribute, $class, $method, $parameter)) { + $ar[] = self::createMethodParameterAttribute( + $attribute, + $arguments, + $class, + $method, + $parameter + ); + } + } + } + + return $ar; + } + /** * @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate * diff --git a/src/MemoizeAttributeCollector.php b/src/MemoizeAttributeCollector.php index f2a80d4..0982202 100644 --- a/src/MemoizeAttributeCollector.php +++ b/src/MemoizeAttributeCollector.php @@ -26,12 +26,14 @@ class MemoizeAttributeCollector * array, * array, * array, + * array>, * }> * Where _key_ is a class and _value is an array where: * - `0` is a timestamp * - `1` is an array of class attributes * - `2` is an array of method attributes * - `3` is an array of property attributes + * - `4` is an array of arrays. _key_ is a method name and _value_ parameter attributes */ private array $state; @@ -62,7 +64,8 @@ public function collectAttributes(array $classMap): TransientCollection $classAttributes, $methodAttributes, $propertyAttributes, - ] = $this->state[$class] ?? [ 0, [], [], [] ]; + $methodParameterAttributes, + ] = $this->state[$class] ?? [ 0, [], [], [], [] ]; $mtime = filemtime($filepath); @@ -79,6 +82,7 @@ public function collectAttributes(array $classMap): TransientCollection $classAttributes, $methodAttributes, $propertyAttributes, + $methodParameterAttributes, ] = $classAttributeCollector->collectAttributes($class); } catch (Throwable $e) { $this->io->error( @@ -86,7 +90,7 @@ public function collectAttributes(array $classMap): TransientCollection ); } - $this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ]; + $this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ]; } if (count($classAttributes)) { @@ -95,6 +99,9 @@ public function collectAttributes(array $classMap): TransientCollection if (count($methodAttributes)) { $collector->addMethodAttributes($class, $methodAttributes); } + if (count($methodParameterAttributes)) { + $collector->addMethodParameterAttributes($class, $methodParameterAttributes); + } if (count($propertyAttributes)) { $collector->addTargetProperties($class, $propertyAttributes); } diff --git a/src/ParameterAttributeCollector.php b/src/ParameterAttributeCollector.php new file mode 100644 index 0000000..b1942da --- /dev/null +++ b/src/ParameterAttributeCollector.php @@ -0,0 +1,70 @@ +io = $io; + $this->parser = $parser; + } + + /** + * @return array + */ + public function collectAttributes(\ReflectionFunctionAbstract $reflectionFunctionAbstract): array + { + $funcParameterAttributes = []; + foreach($reflectionFunctionAbstract->getParameters() as $parameter) { + $attributes = $this->getParameterAttributes($parameter); + $functionName = $reflectionFunctionAbstract->name; + $parameterName = $parameter->name; + assert($functionName !== ''); + assert($parameterName !== ''); + + $paramLabel = ''; + if ($reflectionFunctionAbstract instanceof \ReflectionMethod) { + $paramLabel = $reflectionFunctionAbstract->class .'::'.$functionName .'(' . $parameterName .')'; + } elseif ($reflectionFunctionAbstract instanceof \ReflectionFunction) { + $paramLabel = $functionName . '(' . $parameterName .')'; + } + + foreach($attributes as $attribute) { + $this->io->debug("Found attribute {$attribute->getName()} on $paramLabel"); + + $funcParameterAttributes[] = new TransientTargetMethodParameter( + $attribute->getName(), + $attribute->getArguments(), + $functionName, + $parameterName + ); + } + } + + return $funcParameterAttributes; + } + + /** + * @return \ReflectionAttribute[] + */ + private function getParameterAttributes(\ReflectionParameter $parameterReflection): array + { + if (PHP_VERSION_ID >= 80000) { + return $parameterReflection->getAttributes(); + } + + // todo: implement PHPParser based inspection + throw new \LogicException(); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index e92b502..f10c58f 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 diff --git a/src/TargetMethodParameter.php b/src/TargetMethodParameter.php new file mode 100644 index 0000000..1b025bd --- /dev/null +++ b/src/TargetMethodParameter.php @@ -0,0 +1,44 @@ +attribute = $attribute; + $this->class = $class; + $this->name = $name; + $this->method = $method; + } +} diff --git a/src/TransientCollection.php b/src/TransientCollection.php index 6f5c291..d34999c 100644 --- a/src/TransientCollection.php +++ b/src/TransientCollection.php @@ -19,6 +19,11 @@ final class TransientCollection */ public array $methods = []; + /** + * @var array>> + */ + public array $methodParameters = []; + /** * @var array> * Where _key_ is a target class. @@ -45,6 +50,16 @@ public function addMethodAttributes(string $class, iterable $targets): void $this->methods[$class] = $targets; } + /** + * @param class-string $class + * @param iterable> $targets + * The target class. + */ + public function addMethodParameterAttributes(string $class, iterable $targets): void + { + $this->methodParameters[$class] = $targets; + } + /** * @param class-string $class * @param iterable $targets diff --git a/src/TransientCollectionRenderer.php b/src/TransientCollectionRenderer.php index 5968ed6..0e728c0 100644 --- a/src/TransientCollectionRenderer.php +++ b/src/TransientCollectionRenderer.php @@ -2,6 +2,7 @@ namespace olvlvl\ComposerAttributeCollector; +use function is_iterable; use function var_export; /** @@ -16,6 +17,7 @@ public static function render(TransientCollection $collector): string $targetClassesCode = self::targetsToCode($collector->classes); $targetMethodsCode = self::targetsToCode($collector->methods); $targetPropertiesCode = self::targetsToCode($collector->properties); + $targetMethodParametersCode = self::targetsToCode($collector->methodParameters); return << new \olvlvl\ComposerAttributeCollector\Collection( + // classes $targetClassesCode, + // methods $targetMethodsCode, + // properties $targetPropertiesCode, + // method parameters + $targetMethodParametersCode, )); PHP; } /** * //phpcs:disable Generic.Files.LineLength.TooLong - * @param iterable> $targetByClass + * @param iterable>> $targetByClass * * @return string */ @@ -45,7 +52,7 @@ private static function targetsToCode(iterable $targetByClass): string /** * //phpcs:disable Generic.Files.LineLength.TooLong - * @param iterable> $targetByClass + * @param iterable>> $targetByClass * * @return array, class-string, 2?:non-empty-string }>> */ @@ -54,14 +61,29 @@ 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; + foreach ($targets as $target) { + if (!is_iterable($target)) { + $target = [$target]; } - $by[$t->attribute][] = $a; + foreach($target as $t) { + // args in order how the Target* classes expects them in __construct() + $args = [ $t->arguments, $class ]; + + if ( + $t instanceof TransientTargetMethod + || $t instanceof TransientTargetProperty + || $t instanceof TransientTargetMethodParameter + ) { + $args[] = $t->name; + } + + if ($t instanceof TransientTargetMethodParameter) { + $args[] = $t->method; + } + + $by[$t->attribute][] = $args; + } } } diff --git a/src/TransientTargetMethodParameter.php b/src/TransientTargetMethodParameter.php new file mode 100644 index 0000000..3bea4a1 --- /dev/null +++ b/src/TransientTargetMethodParameter.php @@ -0,0 +1,40 @@ + + */ + public array $arguments; + /** + * @var non-empty-string + */ + public string $name; + /** + * @var non-empty-string + */ + public string $method; + /** + * @param class-string $attribute The attribute class. + * @param array $arguments The attribute arguments. + * @param non-empty-string $method The target method. + * @param non-empty-string $name The target parameter. + */ + public function __construct(string $attribute, array $arguments, string $method, string $name) + { + $this->attribute = $attribute; + $this->arguments = $arguments; + $this->method = $method; + $this->name = $name; + } +} diff --git a/tests/Acme/PSR4/Presentation/ArticleController.php b/tests/Acme/PSR4/Presentation/ArticleController.php index 2e408c4..4b042b2 100644 --- a/tests/Acme/PSR4/Presentation/ArticleController.php +++ b/tests/Acme/PSR4/Presentation/ArticleController.php @@ -11,6 +11,8 @@ use Acme\Attribute\Resource; use Acme\Attribute\Route; +use Acme81\Attribute\ParameterA; +use Acme81\Attribute\ParameterB; #[Resource("articles")] final class ArticleController @@ -24,4 +26,16 @@ public function list(): void public function show(int $id): void { } + + #[Route("/articles/method/", 'GET', 'articles:method')] + public function aMethod( + #[ParameterA("my parameter label")] + $myParameter, + #[ParameterB("my 2nd parameter label", "some more data")] + $anotherParameter, + #[ParameterA("my yet another parameter label")] + $yetAnotherParameter, + ) { + + } } diff --git a/tests/Acme81/Attribute/ParameterA.php b/tests/Acme81/Attribute/ParameterA.php new file mode 100644 index 0000000..870fa03 --- /dev/null +++ b/tests/Acme81/Attribute/ParameterA.php @@ -0,0 +1,16 @@ +label = $label; + } +} diff --git a/tests/Acme81/Attribute/ParameterB.php b/tests/Acme81/Attribute/ParameterB.php new file mode 100644 index 0000000..5ac8087 --- /dev/null +++ b/tests/Acme81/Attribute/ParameterB.php @@ -0,0 +1,19 @@ +label = $label; + $this->moreData = $moreData; + } +} diff --git a/tests/Acme81/PSR4/AFunction.php b/tests/Acme81/PSR4/AFunction.php new file mode 100644 index 0000000..283a683 --- /dev/null +++ b/tests/Acme81/PSR4/AFunction.php @@ -0,0 +1,12 @@ + [ + new TransientTargetMethodParameter( + 'Acme81\Attribute\ParameterA', + ["my parameter label"], + 'aMethod', + 'myParameter' + ), + new TransientTargetMethodParameter( + 'Acme81\Attribute\ParameterB', + ["my 2nd parameter label", "some more data"], + 'aMethod', + 'anotherParameter' + ), + new TransientTargetMethodParameter( + 'Acme81\Attribute\ParameterA', + ["my yet another parameter label"], + 'aMethod', + 'yetAnotherParameter' + ), + ] + ], ] ], @@ -113,6 +144,7 @@ public static function provideCollectAttributes(): array new TransientTargetMethod('Acme\Attribute\Subscribe', [], 'onEventA'), ], [], + [], ] ], @@ -132,6 +164,7 @@ public static function provideCollectAttributes(): array new TransientTargetProperty('Acme\Attribute\ActiveRecord\Text', [], 'body'), new TransientTargetProperty('Acme\Attribute\ActiveRecord\Boolean', [], 'active'), ], + [], ] ], diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 00a2860..77d6aff 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -15,10 +15,13 @@ use Acme\Presentation\ImageController; use Acme\PSR4\ActiveRecord\Article; use Acme\PSR4\Presentation\ArticleController; +use Acme81\Attribute\ParameterA; +use Acme81\Attribute\ParameterB; use olvlvl\ComposerAttributeCollector\Attributes; use olvlvl\ComposerAttributeCollector\Collection; use olvlvl\ComposerAttributeCollector\TargetClass; use olvlvl\ComposerAttributeCollector\TargetMethod; +use olvlvl\ComposerAttributeCollector\TargetMethodParameter; use olvlvl\ComposerAttributeCollector\TargetProperty; use PHPUnit\Framework\TestCase; @@ -39,6 +42,8 @@ public function testFilterTargetClasses(): void [ ], [ + ], + [ ] ); @@ -69,6 +74,8 @@ public function testFilterTargetMethods(): void ], ], [ + ], + [ ] ); @@ -81,6 +88,36 @@ public function testFilterTargetMethods(): void ], $actual); } + public function testFilterTargetMethodParameters(): void + { + $collection = new Collection( + [ + ], + [ + ], + [ + ], + [ + ParameterA::class => [ + [ [ 'a' ], ArticleController::class, 'myMethod', 'myParamA', ], + [ [ 'a2' ], ArticleController::class, 'myMethod', 'myParamA2' ], + [ [ 'a3' ], ArticleController::class, 'myFoo', 'fooParam' ], + ], + ParameterB::class => [ + [ [ 'b', 'more data'], ArticleController::class, 'myMethod', 'myParamB' ], + ], + ] + ); + + $actual = $collection->filterTargetMethodParameters(fn($a) => is_a($a, ParameterA::class, true)); + + $this->assertEquals([ + new TargetMethodParameter(new ParameterA('a'), ArticleController::class, 'myMethod', 'myParamA'), + new TargetMethodParameter(new ParameterA('a2'), ArticleController::class, 'myMethod', 'myParamA2'), + new TargetMethodParameter(new ParameterA('a3'), ArticleController::class, 'myFoo', 'fooParam'), + ], $actual); + } + public function testFilterTargetProperties(): void { $collection = new Collection( @@ -110,6 +147,8 @@ public function testFilterTargetProperties(): void Text::class => [ [ [ ], Article::class, 'body' ], ] + ], + [ ] ); @@ -155,6 +194,8 @@ public function testForClass(): void Text::class => [ [ [ ], Article::class, 'body' ], ] + ], + [ ] ); diff --git a/tests/PluginTest.php b/tests/PluginTest.php index 448353e..6177868 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -23,13 +23,17 @@ use Acme\Attribute\Route; use Acme\Attribute\Subscribe; use Acme\PSR4\Presentation\ArticleController; +use Acme81\Attribute\ParameterA; +use Acme81\Attribute\ParameterB; use Composer\IO\NullIO; use olvlvl\ComposerAttributeCollector\Attributes; use olvlvl\ComposerAttributeCollector\Config; use olvlvl\ComposerAttributeCollector\Plugin; use olvlvl\ComposerAttributeCollector\TargetClass; use olvlvl\ComposerAttributeCollector\TargetMethod; +use olvlvl\ComposerAttributeCollector\TargetMethodParameter; use olvlvl\ComposerAttributeCollector\TargetProperty; +use PhpParser\Node\Param; use PHPUnit\Framework\TestCase; use ReflectionException; @@ -158,6 +162,10 @@ public static function provideTargetMethods(): array [ Route::class, [ + [ + new Route("/articles/method/", 'GET', 'articles:method'), + 'Acme\PSR4\Presentation\ArticleController::aMethod' + ], [ new Route("/articles", 'GET', 'articles:list'), 'Acme\PSR4\Presentation\ArticleController::list' @@ -188,6 +196,52 @@ public static function provideTargetMethods(): array ]; } + /** + * @dataProvider provideTargetMethodParameters + * + * @param class-string $attribute + * @param array $expected + */ + public function testTargetMethodParameters(string $attribute, array $expected): void + { + $actual = Attributes::findTargetMethodParameters($attribute); + + $this->assertEquals($expected, $this->collectMethodParameters($actual)); + } + + /** + * @return array }> + */ + public static function provideTargetMethodParameters(): array + { + return [ + + [ + ParameterA::class, + [ + [ + new ParameterA('my parameter label'), + 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' + ], + [ + new ParameterA('my yet another parameter label'), + 'Acme\PSR4\Presentation\ArticleController::aMethod(yetAnotherParameter)' + ], + ] + ], + [ + ParameterB::class, + [ + [ + new ParameterB('my 2nd parameter label', 'some more data'), + 'Acme\PSR4\Presentation\ArticleController::aMethod(anotherParameter)' + ], + ] + ], + + ]; + } + /** * @dataProvider provideTargetProperties * @@ -256,6 +310,7 @@ public function testFilterTargetMethods(): void ); $this->assertEquals([ + [ new Route("/articles/method/", 'GET', 'articles:method'), 'Acme\PSR4\Presentation\ArticleController::aMethod' ], [ new Route("/articles", 'GET', 'articles:list'), 'Acme\PSR4\Presentation\ArticleController::list' ], [ new Route("/articles/{id}", 'GET', 'articles:show'), 'Acme\PSR4\Presentation\ArticleController::show' ], [ new Get(), 'Acme\Presentation\FileController::list' ], @@ -296,6 +351,18 @@ public function testFilterTargetMethods81(): void $this->assertEquals($expected, $actual); } + public function testFilterTargetMethodParameters(): void + { + $actual = Attributes::filterTargetMethodParameters( + Attributes::predicateForAttributeInstanceOf(ParameterA::class) + ); + + $this->assertEquals([ + [ new ParameterA("my parameter label"), 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' ], + [ new ParameterA('my yet another parameter label'), 'Acme\PSR4\Presentation\ArticleController::aMethod(yetAnotherParameter)' ], + ], $this->collectMethodParameters($actual)); + } + public function testFilterTargetProperties(): void { $actual = Attributes::filterTargetProperties( @@ -323,6 +390,7 @@ public function testForClass(): void $this->assertEquals([ 'list' => [ new Route("/articles", 'GET', 'articles:list') ], 'show' => [ new Route("/articles/{id}", 'GET', 'articles:show') ], + 'aMethod' => [ new Route("/articles/method/", 'GET', 'articles:method') ], ], $forClass->methodsAttributes); } @@ -366,6 +434,26 @@ private function collectMethods(array $targets): array return $methods; } + /** + * @template T of object + * + * @param TargetMethodParameter[] $targets + * + * @return array + */ + private function collectMethodParameters(array $targets): array + { + $parameters = []; + + foreach ($targets as $target) { + $parameters[] = [ $target->attribute, "$target->class::$target->method($target->name)" ]; + } + + usort($parameters, fn($a, $b) => $a[1] <=> $b[1]); + + return $parameters; + } + /** * @template T of object * From 4599456a6fd7fd3be3ebd7df3b9215e51708e963 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 30 May 2025 11:57:06 +0200 Subject: [PATCH 02/11] Support attributes on parameters of hooked properties --- src/ClassAttributeCollector.php | 85 ++++++++++++++----- src/FakeMethod.php | 7 ++ src/MemoizeAttributeCollector.php | 2 +- src/TransientCollection.php | 4 +- .../PSR4/Presentation/HookedProperties.php | 27 ++++++ tests/ClassAttributeCollectorTest.php | 2 +- tests/PluginTest.php | 6 ++ 7 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 src/FakeMethod.php create mode 100644 tests/Acme81/PSR4/Presentation/HookedProperties.php diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 38182ce..b84ddb7 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -4,6 +4,7 @@ use Attribute; use Composer\IO\IOInterface; +use LogicException; use PhpParser\ConstExprEvaluationException; use PhpParser\ConstExprEvaluator; use PhpParser\Node; @@ -16,6 +17,7 @@ use ReflectionAttribute; use ReflectionClass; use ReflectionException; +use ReflectionMethod; use ReflectionProperty; use function file_get_contents; @@ -45,7 +47,7 @@ public function __construct(IOInterface $io, Parser $parser) * array, * array, * array, - * array>, + * array>, * } * * @throws ReflectionException @@ -77,28 +79,13 @@ public function collectAttributes(string $class): array $methodAttributes = []; $methodParameterAttributes = []; - $parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->parser); foreach ($classReflection->getMethods() as $methodReflection) { - foreach ($this->getMethodAttributes($methodReflection) as $attribute) { - if (self::isAttributeIgnored($attribute->getName())) { - continue; - } - - $method = $methodReflection->name; - - $this->io->debug("Found attribute {$attribute->getName()} on $class::$method"); - - $methodAttributes[] = new TransientTargetMethod( - $attribute->getName(), - $attribute->getArguments(), - $method, - ); - } - - $parameterAttributes = $parameterAttributeCollector->collectAttributes($methodReflection); - if ($parameterAttributes !== []) { - $methodParameterAttributes[$methodReflection->getName()] = $parameterAttributes; - } + $this->collectMethodAndParameterAttributes( + $class, + $methodReflection, + $methodAttributes, + $methodParameterAttributes, + ); } $propertyAttributes = []; @@ -120,6 +107,15 @@ public function collectAttributes(string $class): array $property, ); } + + foreach($this->getPropertyHooks($propertyReflection) as $methodReflection) { + $this->collectMethodAndParameterAttributes( + $class, + $methodReflection, + $methodAttributes, + $methodParameterAttributes, + ); + } } return [ $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ]; @@ -371,4 +367,49 @@ public function enterNode(Node $node): ?int return $this->attrGroupsToAttributes($methodVisitor->methodNodeToReturn->attrGroups); } + + /** + * @return array + */ + private function getPropertyHooks(\ReflectionProperty $propertyReflection): array + { + if (PHP_VERSION_ID >= 80400) { + return $propertyReflection->getHooks(); + } + + // todo implement php-parser based inspection + throw new LogicException("Implement me"); + } + + /** + * @param string $class + * @param ReflectionMethod $methodReflection + * @param array $methodAttributes + * @param array> $methodParameterAttributes + * @return void + */ + private function collectMethodAndParameterAttributes(string $class, \ReflectionMethod $methodReflection, array &$methodAttributes, array &$methodParameterAttributes): void + { + $parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->parser); + foreach ($this->getMethodAttributes($methodReflection) as $attribute) { + if (self::isAttributeIgnored($attribute->getName())) { + continue; + } + + $method = $methodReflection->name; + + $this->io->debug("Found attribute {$attribute->getName()} on $class::$method"); + + $methodAttributes[] = new TransientTargetMethod( + $attribute->getName(), + $attribute->getArguments(), + $method, + ); + } + + $parameterAttributes = $parameterAttributeCollector->collectAttributes($methodReflection); + if ($parameterAttributes !== []) { + $methodParameterAttributes[] = $parameterAttributes; + } + } } diff --git a/src/FakeMethod.php b/src/FakeMethod.php new file mode 100644 index 0000000..2b4d2c3 --- /dev/null +++ b/src/FakeMethod.php @@ -0,0 +1,7 @@ +, * array, * array, - * array>, + * array>, * }> * Where _key_ is a class and _value is an array where: * - `0` is a timestamp diff --git a/src/TransientCollection.php b/src/TransientCollection.php index d34999c..dde33e0 100644 --- a/src/TransientCollection.php +++ b/src/TransientCollection.php @@ -20,7 +20,7 @@ final class TransientCollection public array $methods = []; /** - * @var array>> + * @var array>> */ public array $methodParameters = []; @@ -52,7 +52,7 @@ public function addMethodAttributes(string $class, iterable $targets): void /** * @param class-string $class - * @param iterable> $targets + * @param iterable> $targets * The target class. */ public function addMethodParameterAttributes(string $class, iterable $targets): void diff --git a/tests/Acme81/PSR4/Presentation/HookedProperties.php b/tests/Acme81/PSR4/Presentation/HookedProperties.php new file mode 100644 index 0000000..7d91b06 --- /dev/null +++ b/tests/Acme81/PSR4/Presentation/HookedProperties.php @@ -0,0 +1,27 @@ += 8.4 + +namespace Acme81\PSR4\Presentation; + +use Acme\Attribute\Subscribe; +use Acme81\Attribute\ParameterA; + +final class HookedProperties { + private bool $modified = false; + + public string $foo = 'default value' { + get { + if ($this->modified) { + return $this->foo . ' (modified)'; + } + return $this->foo; + } + #[Subscribe] + set( + #[ParameterA("a hook parameter")] + string $value + ) { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} diff --git a/tests/ClassAttributeCollectorTest.php b/tests/ClassAttributeCollectorTest.php index 940701d..358399e 100644 --- a/tests/ClassAttributeCollectorTest.php +++ b/tests/ClassAttributeCollectorTest.php @@ -112,7 +112,7 @@ public static function provideCollectAttributes(): array ], [], [ - 'aMethod' => [ + [ new TransientTargetMethodParameter( 'Acme81\Attribute\ParameterA', ["my parameter label"], diff --git a/tests/PluginTest.php b/tests/PluginTest.php index 6177868..a58450b 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -188,6 +188,7 @@ public static function provideTargetMethods(): array [ Subscribe::class, [ + [ new Subscribe(), 'Acme81\PSR4\Presentation\HookedProperties::$foo::set' ], [ new Subscribe(), 'Acme\PSR4\SubscriberA::onEventA' ], [ new Subscribe(), 'Acme\PSR4\SubscriberB::onEventA' ], ] @@ -219,6 +220,10 @@ public static function provideTargetMethodParameters(): array [ ParameterA::class, [ + [ + new ParameterA('a hook parameter'), + 'Acme81\PSR4\Presentation\HookedProperties::$foo::set(value)' + ], [ new ParameterA('my parameter label'), 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' @@ -358,6 +363,7 @@ public function testFilterTargetMethodParameters(): void ); $this->assertEquals([ + [ new ParameterA('a hook parameter'), 'Acme81\PSR4\Presentation\HookedProperties::$foo::set(value)' ], [ new ParameterA("my parameter label"), 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' ], [ new ParameterA('my yet another parameter label'), 'Acme\PSR4\Presentation\ArticleController::aMethod(yetAnotherParameter)' ], ], $this->collectMethodParameters($actual)); From dba08026cf20738de174d667a80a3134d80b7539 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Jun 2025 21:25:56 +0200 Subject: [PATCH 03/11] remove hooked property support cannot be emulated via runtime reflection on PHP < 8.4 --- src/ClassAttributeCollector.php | 22 --------------- .../PSR4/Presentation/HookedProperties.php | 27 ------------------- tests/PluginTest.php | 6 ----- 3 files changed, 55 deletions(-) delete mode 100644 tests/Acme81/PSR4/Presentation/HookedProperties.php diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index b84ddb7..9e8fbd8 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -107,15 +107,6 @@ public function collectAttributes(string $class): array $property, ); } - - foreach($this->getPropertyHooks($propertyReflection) as $methodReflection) { - $this->collectMethodAndParameterAttributes( - $class, - $methodReflection, - $methodAttributes, - $methodParameterAttributes, - ); - } } return [ $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ]; @@ -368,19 +359,6 @@ public function enterNode(Node $node): ?int return $this->attrGroupsToAttributes($methodVisitor->methodNodeToReturn->attrGroups); } - /** - * @return array - */ - private function getPropertyHooks(\ReflectionProperty $propertyReflection): array - { - if (PHP_VERSION_ID >= 80400) { - return $propertyReflection->getHooks(); - } - - // todo implement php-parser based inspection - throw new LogicException("Implement me"); - } - /** * @param string $class * @param ReflectionMethod $methodReflection diff --git a/tests/Acme81/PSR4/Presentation/HookedProperties.php b/tests/Acme81/PSR4/Presentation/HookedProperties.php deleted file mode 100644 index 7d91b06..0000000 --- a/tests/Acme81/PSR4/Presentation/HookedProperties.php +++ /dev/null @@ -1,27 +0,0 @@ -= 8.4 - -namespace Acme81\PSR4\Presentation; - -use Acme\Attribute\Subscribe; -use Acme81\Attribute\ParameterA; - -final class HookedProperties { - private bool $modified = false; - - public string $foo = 'default value' { - get { - if ($this->modified) { - return $this->foo . ' (modified)'; - } - return $this->foo; - } - #[Subscribe] - set( - #[ParameterA("a hook parameter")] - string $value - ) { - $this->foo = strtolower($value); - $this->modified = true; - } - } -} diff --git a/tests/PluginTest.php b/tests/PluginTest.php index a58450b..6177868 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -188,7 +188,6 @@ public static function provideTargetMethods(): array [ Subscribe::class, [ - [ new Subscribe(), 'Acme81\PSR4\Presentation\HookedProperties::$foo::set' ], [ new Subscribe(), 'Acme\PSR4\SubscriberA::onEventA' ], [ new Subscribe(), 'Acme\PSR4\SubscriberB::onEventA' ], ] @@ -220,10 +219,6 @@ public static function provideTargetMethodParameters(): array [ ParameterA::class, [ - [ - new ParameterA('a hook parameter'), - 'Acme81\PSR4\Presentation\HookedProperties::$foo::set(value)' - ], [ new ParameterA('my parameter label'), 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' @@ -363,7 +358,6 @@ public function testFilterTargetMethodParameters(): void ); $this->assertEquals([ - [ new ParameterA('a hook parameter'), 'Acme81\PSR4\Presentation\HookedProperties::$foo::set(value)' ], [ new ParameterA("my parameter label"), 'Acme\PSR4\Presentation\ArticleController::aMethod(myParameter)' ], [ new ParameterA('my yet another parameter label'), 'Acme\PSR4\Presentation\ArticleController::aMethod(yetAnotherParameter)' ], ], $this->collectMethodParameters($actual)); From 7de595db0c519ec45e34526a407dd639d220ce01 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Jun 2025 21:37:28 +0200 Subject: [PATCH 04/11] extract AttributeGroupsReflector --- src/AttributeGroupsReflector.php | 50 ++++++++++++++++++++++++++++++++ src/ClassAttributeCollector.php | 46 ++--------------------------- 2 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 src/AttributeGroupsReflector.php diff --git a/src/AttributeGroupsReflector.php b/src/AttributeGroupsReflector.php new file mode 100644 index 0000000..fb92d6f --- /dev/null +++ b/src/AttributeGroupsReflector.php @@ -0,0 +1,50 @@ +[] + */ + public function attrGroupsToAttributes(array $attrGroups): array + { + $evaluator = new ConstExprEvaluator(function (Expr $expr) { + if ($expr instanceof Expr\ClassConstFetch && $expr->class instanceof Node\Name && $expr->name instanceof Node\Identifier) { + return constant(sprintf('%s::%s', $expr->class->toString(), $expr->name->toString())); + } + + throw new ConstExprEvaluationException("Expression of type {$expr->getType()} cannot be evaluated"); + }); + + $attributes = []; + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $argValues = []; + foreach ($attr->args as $i => $arg) { + if ($arg->name === null) { + $argValues[$i] = $evaluator->evaluateDirectly($arg->value); + continue; + } + + $argValues[$arg->name->toString()] = $evaluator->evaluateDirectly($arg->value); + } + $attributes[] = new FakeAttribute( + $attr->name, + $argValues, + ); + } + } + + return $attributes; // @phpstan-ignore return.type + } +} diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 9e8fbd8..675a9e5 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -4,11 +4,7 @@ use Attribute; use Composer\IO\IOInterface; -use LogicException; -use PhpParser\ConstExprEvaluationException; -use PhpParser\ConstExprEvaluator; use PhpParser\Node; -use PhpParser\Node\Expr; use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; @@ -184,7 +180,7 @@ public function enterNode(Node $node) return []; } - return $this->attrGroupsToAttributes($classVisitor->classNodeToReturn->attrGroups); + return (new AttributeGroupsReflector())->attrGroupsToAttributes($classVisitor->classNodeToReturn->attrGroups); } /** @@ -207,42 +203,6 @@ private function parse(string $file): array return $this->parserCache[$file] = $nameTraverser->traverse($ast); } - /** - * @param Node\AttributeGroup[] $attrGroups - * @return ReflectionAttribute[] - */ - private function attrGroupsToAttributes(array $attrGroups): array - { - $evaluator = new ConstExprEvaluator(function (Expr $expr) { - if ($expr instanceof Expr\ClassConstFetch && $expr->class instanceof Node\Name && $expr->name instanceof Node\Identifier) { - return constant(sprintf('%s::%s', $expr->class->toString(), $expr->name->toString())); - } - - throw new ConstExprEvaluationException("Expression of type {$expr->getType()} cannot be evaluated"); - }); - - $attributes = []; - foreach ($attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - $argValues = []; - foreach ($attr->args as $i => $arg) { - if ($arg->name === null) { - $argValues[$i] = $evaluator->evaluateDirectly($arg->value); - continue; - } - - $argValues[$arg->name->toString()] = $evaluator->evaluateDirectly($arg->value); - } - $attributes[] = new FakeAttribute( - $attr->name, - $argValues, - ); - } - } - - return $attributes; // @phpstan-ignore return.type - } - /** * @return ReflectionAttribute[] */ @@ -299,7 +259,7 @@ public function enterNode(Node $node): ?int return []; } - return $this->attrGroupsToAttributes($propertyVisitor->propertyNodeToReturn->attrGroups); + return (new AttributeGroupsReflector())->attrGroupsToAttributes($propertyVisitor->propertyNodeToReturn->attrGroups); } /** @@ -356,7 +316,7 @@ public function enterNode(Node $node): ?int return []; } - return $this->attrGroupsToAttributes($methodVisitor->methodNodeToReturn->attrGroups); + return (new AttributeGroupsReflector())->attrGroupsToAttributes($methodVisitor->methodNodeToReturn->attrGroups); } /** From 1ff72d3abb23ea5a249717e9650a331f2700a824 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Jun 2025 21:43:38 +0200 Subject: [PATCH 05/11] extract CachedParser --- src/CachedParser.php | 46 +++++++++++++++++++++++++++++++++ src/ClassAttributeCollector.php | 18 +++---------- 2 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 src/CachedParser.php diff --git a/src/CachedParser.php b/src/CachedParser.php new file mode 100644 index 0000000..3bd9b03 --- /dev/null +++ b/src/CachedParser.php @@ -0,0 +1,46 @@ + */ + private array $parserCache = []; + + public function __construct(IOInterface $io, Parser $parser) { + $this->io = $io; + $this->parser = $parser; + } + + /** + * @return Node[] + */ + public function parse(string $file): array + { + if (isset($this->parserCache[$file])) { + return $this->parserCache[$file]; + } + $contents = file_get_contents($file); + if ($contents === false) { + return []; + } + + $ast = $this->parser->parse($contents); + assert($ast !== null); + $nameTraverser = new NodeTraverser(new NameResolver()); + + return $this->parserCache[$file] = $nameTraverser->traverse($ast); + } +} diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 675a9e5..8b7b346 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -28,13 +28,13 @@ class ClassAttributeCollector { private IOInterface $io; private Parser $parser; + private CachedParser $cachedParser; - /** @var array */ - private array $parserCache = []; public function __construct(IOInterface $io, Parser $parser) { $this->io = $io; $this->parser = $parser; + $this->cachedParser = new CachedParser($io, $parser); } /** * @param class-string $class @@ -188,19 +188,7 @@ public function enterNode(Node $node) */ private function parse(string $file): array { - if (isset($this->parserCache[$file])) { - return $this->parserCache[$file]; - } - $contents = file_get_contents($file); - if ($contents === false) { - return []; - } - - $ast = $this->parser->parse($contents); - assert($ast !== null); - $nameTraverser = new NodeTraverser(new NameResolver()); - - return $this->parserCache[$file] = $nameTraverser->traverse($ast); + return $this->cachedParser->parse($file); } /** From 8287f03d29d0b508e507c2deac69ac811a8485fb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Jun 2025 21:59:54 +0200 Subject: [PATCH 06/11] Implement ParameterAttributeCollector->getParameterAttributes() via AST parsing --- src/ParameterAttributeCollector.php | 77 ++++++++++++++++++- .../PSR4/Presentation/ArticleController.php | 2 +- tests/Acme81/Attribute/ParameterA.php | 2 +- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/ParameterAttributeCollector.php b/src/ParameterAttributeCollector.php index b1942da..0683b98 100644 --- a/src/ParameterAttributeCollector.php +++ b/src/ParameterAttributeCollector.php @@ -4,6 +4,10 @@ use Composer\IO\IOInterface; +use PhpParser\Node; +use PhpParser\Node\Stmt\ClassLike; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitorAbstract; use PhpParser\Parser; /** @@ -14,10 +18,13 @@ class ParameterAttributeCollector private IOInterface $io; private Parser $parser; + private CachedParser $cachedParser; + public function __construct(IOInterface $io, Parser $parser) { $this->io = $io; $this->parser = $parser; + $this->cachedParser = new CachedParser($io, $parser); } /** @@ -64,7 +71,73 @@ private function getParameterAttributes(\ReflectionParameter $parameterReflectio return $parameterReflection->getAttributes(); } - // todo: implement PHPParser based inspection - throw new \LogicException(); + $reflectionClass = $parameterReflection->getDeclaringClass(); + if ($reflectionClass === null || $reflectionClass->getFileName() === false) { + return []; + } + + $ast = $this->parse($reflectionClass->getFileName()); + $parameterVisitor = new class ($reflectionClass->getName(), $parameterReflection->getDeclaringFunction()->getName(), $parameterReflection->getName()) extends NodeVisitorAbstract { + private string $className; + + private string $methodName; + private string $parameterName; + + public ?Node\Param $paramToReturn = null; + + public function __construct(string $className, string $methodName, string $parameterName) + { + $this->className = $className; + $this->methodName = $methodName; + $this->parameterName = $parameterName; + } + + public function enterNode(Node $node): ?int + { + if ($node instanceof ClassLike) { + if ($node->namespacedName === null) { + return self::DONT_TRAVERSE_CHILDREN; + } + if ($node->namespacedName->toString() !== $this->className) { + return self::DONT_TRAVERSE_CHILDREN; + } + } + + if ($node instanceof Node\Stmt\ClassMethod) { + if ($node->name->toString() !== $this->methodName) { + return self::DONT_TRAVERSE_CHILDREN; + } + } + + if ($node instanceof Node\Param) { + if ( + $node->var instanceof Node\Expr\Variable + && $node->var->name === $this->parameterName + ) { + $this->paramToReturn = $node; + return self::DONT_TRAVERSE_CHILDREN; + } + } + + return null; + } + }; + $traverser = new NodeTraverser($parameterVisitor); + $traverser->traverse($ast); + + if ($parameterVisitor->paramToReturn === null) { + return []; + } + + return (new AttributeGroupsReflector())->attrGroupsToAttributes($parameterVisitor->paramToReturn->attrGroups); + } + + /** + * @return Node[] + */ + private function parse(string $file): array + { + return $this->cachedParser->parse($file); } + } diff --git a/tests/Acme/PSR4/Presentation/ArticleController.php b/tests/Acme/PSR4/Presentation/ArticleController.php index 4b042b2..0ef4489 100644 --- a/tests/Acme/PSR4/Presentation/ArticleController.php +++ b/tests/Acme/PSR4/Presentation/ArticleController.php @@ -34,7 +34,7 @@ public function aMethod( #[ParameterB("my 2nd parameter label", "some more data")] $anotherParameter, #[ParameterA("my yet another parameter label")] - $yetAnotherParameter, + $yetAnotherParameter ) { } diff --git a/tests/Acme81/Attribute/ParameterA.php b/tests/Acme81/Attribute/ParameterA.php index 870fa03..66cc034 100644 --- a/tests/Acme81/Attribute/ParameterA.php +++ b/tests/Acme81/Attribute/ParameterA.php @@ -9,7 +9,7 @@ class ParameterA { public string $label; public function __construct( - string $label = '', + string $label = '' ) { $this->label = $label; } From b2b53b0eceb1f572d96f6760d46dec6401cb9e1f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Jun 2025 22:01:34 +0200 Subject: [PATCH 07/11] Delete FakeMethod.php --- src/FakeMethod.php | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/FakeMethod.php diff --git a/src/FakeMethod.php b/src/FakeMethod.php deleted file mode 100644 index 2b4d2c3..0000000 --- a/src/FakeMethod.php +++ /dev/null @@ -1,7 +0,0 @@ - Date: Mon, 2 Jun 2025 22:04:24 +0200 Subject: [PATCH 08/11] fix build --- src/CachedParser.php | 4 +--- src/ClassAttributeCollector.php | 2 +- src/ParameterAttributeCollector.php | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/CachedParser.php b/src/CachedParser.php index 3bd9b03..ab0f30f 100644 --- a/src/CachedParser.php +++ b/src/CachedParser.php @@ -13,14 +13,12 @@ */ class CachedParser { - private IOInterface $io; private Parser $parser; /** @var array */ private array $parserCache = []; - public function __construct(IOInterface $io, Parser $parser) { - $this->io = $io; + public function __construct(Parser $parser) { $this->parser = $parser; } diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 8b7b346..7e7f3c2 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -34,7 +34,7 @@ public function __construct(IOInterface $io, Parser $parser) { $this->io = $io; $this->parser = $parser; - $this->cachedParser = new CachedParser($io, $parser); + $this->cachedParser = new CachedParser($parser); } /** * @param class-string $class diff --git a/src/ParameterAttributeCollector.php b/src/ParameterAttributeCollector.php index 0683b98..4ce755c 100644 --- a/src/ParameterAttributeCollector.php +++ b/src/ParameterAttributeCollector.php @@ -16,15 +16,12 @@ class ParameterAttributeCollector { private IOInterface $io; - private Parser $parser; - private CachedParser $cachedParser; public function __construct(IOInterface $io, Parser $parser) { $this->io = $io; - $this->parser = $parser; - $this->cachedParser = new CachedParser($io, $parser); + $this->cachedParser = new CachedParser($parser); } /** From 51b3b71a2e1f867026fe974128e540a7a75a889e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 3 Jun 2025 09:13:05 +0200 Subject: [PATCH 09/11] cs --- src/AttributeGroupsReflector.php | 3 ++- src/CachedParser.php | 3 ++- src/ParameterAttributeCollector.php | 10 ++++------ src/TransientCollectionRenderer.php | 2 +- tests/Acme/PSR4/Presentation/ArticleController.php | 1 - tests/Acme81/PSR4/AFunction.php | 1 - 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/AttributeGroupsReflector.php b/src/AttributeGroupsReflector.php index fb92d6f..ad837ee 100644 --- a/src/AttributeGroupsReflector.php +++ b/src/AttributeGroupsReflector.php @@ -11,7 +11,8 @@ /** * @internal */ -class AttributeGroupsReflector { +class AttributeGroupsReflector +{ /** * @param Node\AttributeGroup[] $attrGroups * @return ReflectionAttribute[] diff --git a/src/CachedParser.php b/src/CachedParser.php index ab0f30f..4715bde 100644 --- a/src/CachedParser.php +++ b/src/CachedParser.php @@ -18,7 +18,8 @@ class CachedParser /** @var array */ private array $parserCache = []; - public function __construct(Parser $parser) { + public function __construct(Parser $parser) + { $this->parser = $parser; } diff --git a/src/ParameterAttributeCollector.php b/src/ParameterAttributeCollector.php index 4ce755c..ece5ae4 100644 --- a/src/ParameterAttributeCollector.php +++ b/src/ParameterAttributeCollector.php @@ -2,7 +2,6 @@ namespace olvlvl\ComposerAttributeCollector; - use Composer\IO\IOInterface; use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; @@ -30,7 +29,7 @@ public function __construct(IOInterface $io, Parser $parser) public function collectAttributes(\ReflectionFunctionAbstract $reflectionFunctionAbstract): array { $funcParameterAttributes = []; - foreach($reflectionFunctionAbstract->getParameters() as $parameter) { + foreach ($reflectionFunctionAbstract->getParameters() as $parameter) { $attributes = $this->getParameterAttributes($parameter); $functionName = $reflectionFunctionAbstract->name; $parameterName = $parameter->name; @@ -39,12 +38,12 @@ public function collectAttributes(\ReflectionFunctionAbstract $reflectionFunctio $paramLabel = ''; if ($reflectionFunctionAbstract instanceof \ReflectionMethod) { - $paramLabel = $reflectionFunctionAbstract->class .'::'.$functionName .'(' . $parameterName .')'; + $paramLabel = $reflectionFunctionAbstract->class . '::' . $functionName . '(' . $parameterName . ')'; } elseif ($reflectionFunctionAbstract instanceof \ReflectionFunction) { - $paramLabel = $functionName . '(' . $parameterName .')'; + $paramLabel = $functionName . '(' . $parameterName . ')'; } - foreach($attributes as $attribute) { + foreach ($attributes as $attribute) { $this->io->debug("Found attribute {$attribute->getName()} on $paramLabel"); $funcParameterAttributes[] = new TransientTargetMethodParameter( @@ -136,5 +135,4 @@ private function parse(string $file): array { return $this->cachedParser->parse($file); } - } diff --git a/src/TransientCollectionRenderer.php b/src/TransientCollectionRenderer.php index 0e728c0..bb7b17a 100644 --- a/src/TransientCollectionRenderer.php +++ b/src/TransientCollectionRenderer.php @@ -66,7 +66,7 @@ private static function targetsToArray(iterable $targetByClass): array $target = [$target]; } - foreach($target as $t) { + foreach ($target as $t) { // args in order how the Target* classes expects them in __construct() $args = [ $t->arguments, $class ]; diff --git a/tests/Acme/PSR4/Presentation/ArticleController.php b/tests/Acme/PSR4/Presentation/ArticleController.php index 0ef4489..dbd74d5 100644 --- a/tests/Acme/PSR4/Presentation/ArticleController.php +++ b/tests/Acme/PSR4/Presentation/ArticleController.php @@ -36,6 +36,5 @@ public function aMethod( #[ParameterA("my yet another parameter label")] $yetAnotherParameter ) { - } } diff --git a/tests/Acme81/PSR4/AFunction.php b/tests/Acme81/PSR4/AFunction.php index 283a683..c475fc1 100644 --- a/tests/Acme81/PSR4/AFunction.php +++ b/tests/Acme81/PSR4/AFunction.php @@ -8,5 +8,4 @@ function aFunc( #[ParameterA("my function parameter label")] $aParameter ) { - } From 3740689987ce04cfe87e0bc86f34603b9f3f43e5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 3 Jun 2025 09:15:47 +0200 Subject: [PATCH 10/11] re-use cached parser --- src/ClassAttributeCollector.php | 2 +- src/ParameterAttributeCollector.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index 7e7f3c2..c7ff583 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -316,7 +316,7 @@ public function enterNode(Node $node): ?int */ private function collectMethodAndParameterAttributes(string $class, \ReflectionMethod $methodReflection, array &$methodAttributes, array &$methodParameterAttributes): void { - $parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->parser); + $parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->cachedParser); foreach ($this->getMethodAttributes($methodReflection) as $attribute) { if (self::isAttributeIgnored($attribute->getName())) { continue; diff --git a/src/ParameterAttributeCollector.php b/src/ParameterAttributeCollector.php index ece5ae4..230cdc6 100644 --- a/src/ParameterAttributeCollector.php +++ b/src/ParameterAttributeCollector.php @@ -17,10 +17,10 @@ class ParameterAttributeCollector private IOInterface $io; private CachedParser $cachedParser; - public function __construct(IOInterface $io, Parser $parser) + public function __construct(IOInterface $io, CachedParser $parser) { $this->io = $io; - $this->cachedParser = new CachedParser($parser); + $this->cachedParser = $parser; } /** From 2c6fcd4a6aeabf3d71ada084dd203afa7df7a2a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 3 Jun 2025 09:17:18 +0200 Subject: [PATCH 11/11] Update ClassAttributeCollector.php --- src/ClassAttributeCollector.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ClassAttributeCollector.php b/src/ClassAttributeCollector.php index c7ff583..aa00a35 100644 --- a/src/ClassAttributeCollector.php +++ b/src/ClassAttributeCollector.php @@ -27,13 +27,11 @@ class ClassAttributeCollector { private IOInterface $io; - private Parser $parser; private CachedParser $cachedParser; public function __construct(IOInterface $io, Parser $parser) { $this->io = $io; - $this->parser = $parser; $this->cachedParser = new CachedParser($parser); } /**