Skip to content

Commit 0995d20

Browse files
committed
Implement ParameterAttributeCollector
1 parent 990adbc commit 0995d20

18 files changed

+556
-30
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ foreach (Attributes::findTargetProperties(Column::class) as $target) {
5959
var_dump($target->attribute, $target->class, $target->name);
6060
}
6161

62+
// Find the target method-parameters of the UserInput attribute.
63+
foreach (Attributes::findTargetMethodParameters(UserInput::class) as $target) {
64+
var_dump($target->attribute, $target->class, $target->method, $target->name);
65+
}
66+
6267
// Filter target methods using a predicate.
6368
// You can also filter target classes and properties.
6469
$predicate = fn($attribute) => is_a($attribute, Route::class, true);

src/Attributes.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ public static function findTargetProperties(string $attribute): array
6161
return self::getCollection()->findTargetProperties($attribute);
6262
}
6363

64+
/**
65+
* @template T of object
66+
*
67+
* @param class-string<T> $attribute
68+
*
69+
* @return TargetMethodParameter<T>[]
70+
*/
71+
public static function findTargetMethodParameters(string $attribute): array
72+
{
73+
return self::getCollection()->findTargetMethodParameters($attribute);
74+
}
75+
6476
/**
6577
* @param callable(class-string $attribute, class-string $class):bool $predicate
6678
*
@@ -91,6 +103,16 @@ public static function filterTargetProperties(callable $predicate): array
91103
return self::getCollection()->filterTargetProperties($predicate);
92104
}
93105

106+
/**
107+
* @param callable(class-string $attribute, class-string $class, string $property, string $method):bool $predicate
108+
*
109+
* @return array<TargetMethodParameter<object>>
110+
*/
111+
public static function filterTargetMethodParameters(callable $predicate): array
112+
{
113+
return self::getCollection()->filterTargetMethodParameters($predicate);
114+
}
115+
94116
/**
95117
* @param class-string $class
96118
*

src/ClassAttributeCollector.php

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function __construct(
2525
* array<TransientTargetClass>,
2626
* array<TransientTargetMethod>,
2727
* array<TransientTargetProperty>,
28+
* array<array<TransientTargetMethodParameter>>,
2829
* }
2930
*
3031
* @throws ReflectionException
@@ -34,7 +35,7 @@ public function collectAttributes(string $class): array
3435
$classReflection = new ReflectionClass($class);
3536

3637
if (self::isAttribute($classReflection)) {
37-
return [ [], [], [] ];
38+
return [ [], [], [], [] ];
3839
}
3940

4041
$classAttributes = [];
@@ -54,23 +55,15 @@ public function collectAttributes(string $class): array
5455
}
5556

5657
$methodAttributes = [];
58+
$methodParameterAttributes = [];
5759

5860
foreach ($classReflection->getMethods() as $methodReflection) {
59-
foreach ($methodReflection->getAttributes() as $attribute) {
60-
if (self::isAttributeIgnored($attribute)) {
61-
continue;
62-
}
63-
64-
$method = $methodReflection->name;
65-
66-
$this->io->debug("Found attribute {$attribute->getName()} on $class::$method");
67-
68-
$methodAttributes[] = new TransientTargetMethod(
69-
$attribute->getName(),
70-
$attribute->getArguments(),
71-
$method,
72-
);
73-
}
61+
$this->collectMethodAndParameterAttributes(
62+
$class,
63+
$methodReflection,
64+
$methodAttributes,
65+
$methodParameterAttributes,
66+
);
7467
}
7568

7669
$propertyAttributes = [];
@@ -94,7 +87,7 @@ public function collectAttributes(string $class): array
9487
}
9588
}
9689

97-
return [ $classAttributes, $methodAttributes, $propertyAttributes ];
90+
return [ $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ];
9891
}
9992

10093
/**
@@ -124,4 +117,34 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
124117

125118
return isset($ignored[$attribute->getName()]); // @phpstan-ignore offsetAccess.nonOffsetAccessible
126119
}
120+
121+
/**
122+
* @param array<TransientTargetMethod> $methodAttributes
123+
* @param array<array<TransientTargetMethodParameter>> $methodParameterAttributes
124+
* @return void
125+
*/
126+
private function collectMethodAndParameterAttributes(string $class, \ReflectionMethod $methodReflection, array &$methodAttributes, array &$methodParameterAttributes): void
127+
{
128+
$parameterAttributeCollector = new ParameterAttributeCollector($this->io);
129+
foreach ($methodReflection->getAttributes() as $attribute) {
130+
if (self::isAttributeIgnored($attribute)) {
131+
continue;
132+
}
133+
134+
$method = $methodReflection->name;
135+
136+
$this->io->debug("Found attribute {$attribute->getName()} on $class::$method");
137+
138+
$methodAttributes[] = new TransientTargetMethod(
139+
$attribute->getName(),
140+
$attribute->getArguments(),
141+
$method,
142+
);
143+
}
144+
145+
$parameterAttributes = $parameterAttributeCollector->collectAttributes($methodReflection);
146+
if ($parameterAttributes !== []) {
147+
$methodParameterAttributes[] = $parameterAttributes;
148+
}
149+
}
127150
}

src/Collection.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ final class Collection
2222
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string }>> $targetProperties
2323
* Where _key_ is an attribute class and _value_ an array of arrays
2424
* where 0 are the attribute arguments, 1 is a target class, and 2 is the target property.
25+
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string, non-empty-string }>> $targetMethodParameters
26+
* Where _key_ is an attribute class and _value_ an array of arrays
27+
* where 0 are the attribute arguments, 1 is a target class, 2 is the target method, and 3 is the target parameter.
2528
*/
2629
public function __construct(
2730
private array $targetClasses,
2831
private array $targetMethods,
2932
private array $targetProperties,
33+
private array $targetMethodParameters,
3034
) {
3135
}
3236

@@ -109,6 +113,46 @@ private static function createMethodAttribute(
109113
}
110114
}
111115

116+
/**
117+
* @template T of object
118+
*
119+
* @param class-string<T> $attribute
120+
*
121+
* @return array<TargetMethodParameter<T>>
122+
*/
123+
public function findTargetMethodParameters(string $attribute): array
124+
{
125+
return array_map(
126+
fn(array $t) => self::createMethodParameterAttribute($attribute, ...$t),
127+
$this->targetMethodParameters[$attribute] ?? [],
128+
);
129+
}
130+
131+
/**
132+
* @template T of object
133+
*
134+
* @param class-string<T> $attribute
135+
* @param array<mixed> $arguments
136+
* @param class-string $class
137+
* @param non-empty-string $method
138+
* @param non-empty-string $parameter
139+
*
140+
* @return TargetMethodParameter<T>
141+
*/
142+
private static function createMethodParameterAttribute(string $attribute, array $arguments, string $class, string $method, string $parameter): object
143+
{
144+
try {
145+
$a = new $attribute(...$arguments);
146+
return new TargetMethodParameter($a, $class, $method, $parameter);
147+
} catch (Throwable $e) {
148+
throw new RuntimeException(
149+
"An error occurred while instantiating attribute $attribute on method $class::$method($parameter)",
150+
0,
151+
$e,
152+
);
153+
}
154+
}
155+
112156
/**
113157
* @template T of object
114158
*
@@ -196,6 +240,32 @@ public function filterTargetMethods(callable $predicate): array
196240
return $ar;
197241
}
198242

243+
/**
244+
* @param callable(class-string $attribute, class-string $class, non-empty-string $method, non-empty-string $parameter):bool $predicate
245+
*
246+
* @return array<TargetMethodParameter<object>>
247+
*/
248+
public function filterTargetMethodParameters(callable $predicate): array
249+
{
250+
$ar = [];
251+
252+
foreach ($this->targetMethodParameters as $attribute => $references) {
253+
foreach ($references as [$arguments, $class, $method, $parameter]) {
254+
if ($predicate($attribute, $class, $method, $parameter)) {
255+
$ar[] = self::createMethodParameterAttribute(
256+
$attribute,
257+
$arguments,
258+
$class,
259+
$method,
260+
$parameter
261+
);
262+
}
263+
}
264+
}
265+
266+
return $ar;
267+
}
268+
199269
/**
200270
* @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate
201271
*

src/MemoizeAttributeCollector.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ class MemoizeAttributeCollector
2323
* array<TransientTargetClass>,
2424
* array<TransientTargetMethod>,
2525
* array<TransientTargetProperty>,
26+
* array<array<TransientTargetMethodParameter>>,
2627
* }>
2728
* Where _key_ is a class and _value is an array where:
2829
* - `0` is a timestamp
2930
* - `1` is an array of class attributes
3031
* - `2` is an array of method attributes
3132
* - `3` is an array of property attributes
33+
* - `4` is an array of arrays. _key_ is a method name and _value_ parameter attributes
3234
*/
3335
private array $state;
3436

@@ -59,7 +61,8 @@ public function collectAttributes(array $classMap): TransientCollection
5961
$classAttributes,
6062
$methodAttributes,
6163
$propertyAttributes,
62-
] = $this->state[$class] ?? [ 0, [], [], [] ];
64+
$methodParameterAttributes,
65+
] = $this->state[$class] ?? [ 0, [], [], [], [] ];
6366

6467
$mtime = filemtime($filepath);
6568

@@ -76,14 +79,15 @@ public function collectAttributes(array $classMap): TransientCollection
7679
$classAttributes,
7780
$methodAttributes,
7881
$propertyAttributes,
82+
$methodParameterAttributes,
7983
] = $classAttributeCollector->collectAttributes($class);
8084
} catch (Throwable $e) {
8185
$this->io->error(
8286
"Attribute collection failed for $class: {$e->getMessage()}",
8387
);
8488
}
8589

86-
$this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ];
90+
$this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ];
8791
}
8892

8993
if (count($classAttributes)) {
@@ -92,6 +96,9 @@ public function collectAttributes(array $classMap): TransientCollection
9296
if (count($methodAttributes)) {
9397
$collector->addMethodAttributes($class, $methodAttributes);
9498
}
99+
if (count($methodParameterAttributes)) {
100+
$collector->addMethodParameterAttributes($class, $methodParameterAttributes);
101+
}
95102
if (count($propertyAttributes)) {
96103
$collector->addTargetProperties($class, $propertyAttributes);
97104
}

src/ParameterAttributeCollector.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace olvlvl\ComposerAttributeCollector;
4+
5+
use Composer\IO\IOInterface;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\ClassLike;
8+
use PhpParser\NodeTraverser;
9+
use PhpParser\NodeVisitorAbstract;
10+
use PhpParser\Parser;
11+
12+
/**
13+
* @internal
14+
*/
15+
class ParameterAttributeCollector
16+
{
17+
private IOInterface $io;
18+
19+
public function __construct(IOInterface $io)
20+
{
21+
$this->io = $io;
22+
}
23+
24+
/**
25+
* @return array<TransientTargetMethodParameter>
26+
*/
27+
public function collectAttributes(\ReflectionFunctionAbstract $reflectionFunctionAbstract): array
28+
{
29+
$funcParameterAttributes = [];
30+
foreach ($reflectionFunctionAbstract->getParameters() as $parameter) {
31+
$attributes = $parameter->getAttributes();
32+
$functionName = $reflectionFunctionAbstract->name;
33+
$parameterName = $parameter->name;
34+
assert($functionName !== '');
35+
assert($parameterName !== '');
36+
37+
$paramLabel = '';
38+
if ($reflectionFunctionAbstract instanceof \ReflectionMethod) {
39+
$paramLabel = $reflectionFunctionAbstract->class . '::' . $functionName . '(' . $parameterName . ')';
40+
} elseif ($reflectionFunctionAbstract instanceof \ReflectionFunction) {
41+
$paramLabel = $functionName . '(' . $parameterName . ')';
42+
}
43+
44+
foreach ($attributes as $attribute) {
45+
$this->io->debug("Found attribute {$attribute->getName()} on $paramLabel");
46+
47+
$funcParameterAttributes[] = new TransientTargetMethodParameter(
48+
$attribute->getName(),
49+
$attribute->getArguments(),
50+
$functionName,
51+
$parameterName
52+
);
53+
}
54+
}
55+
56+
return $funcParameterAttributes;
57+
}
58+
59+
}

src/Plugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class Plugin implements PluginInterface, EventSubscriberInterface
2727
{
2828
public const CACHE_DIR = '.composer-attribute-collector';
2929
public const VERSION_MAJOR = 2;
30-
public const VERSION_MINOR = 0;
30+
public const VERSION_MINOR = 1;
3131

3232
/**
3333
* @uses onPostAutoloadDump

0 commit comments

Comments
 (0)