Skip to content

Commit 8ac88bf

Browse files
staabmolvlvl
authored andcommitted
Implement ParameterAttributeCollector
1 parent ca67a62 commit 8ac88bf

18 files changed

+536
-31
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: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99

1010
/**
1111
* @internal
12+
* @readonly
1213
*/
1314
class ClassAttributeCollector
1415
{
16+
private ParameterAttributeCollector $methodParameterCollector;
17+
1518
public function __construct(
1619
private Logger $log,
1720
) {
21+
$this->methodParameterCollector = new ParameterAttributeCollector($this->log);
1822
}
1923

2024
/**
@@ -24,6 +28,7 @@ public function __construct(
2428
* array<TransientTargetClass>,
2529
* array<TransientTargetMethod>,
2630
* array<TransientTargetProperty>,
31+
* array<array<TransientTargetMethodParameter>>,
2732
* }
2833
*
2934
* @throws ReflectionException
@@ -33,7 +38,7 @@ public function collectAttributes(string $class): array
3338
$classReflection = new ReflectionClass($class);
3439

3540
if (self::isAttribute($classReflection)) {
36-
return [ [], [], [] ];
41+
return [ [], [], [], [] ];
3742
}
3843

3944
$classAttributes = [];
@@ -53,23 +58,15 @@ public function collectAttributes(string $class): array
5358
}
5459

5560
$methodAttributes = [];
61+
$methodParameterAttributes = [];
5662

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

7572
$propertyAttributes = [];
@@ -93,7 +90,7 @@ public function collectAttributes(string $class): array
9390
}
9491
}
9592

96-
return [ $classAttributes, $methodAttributes, $propertyAttributes ];
93+
return [ $classAttributes, $methodAttributes, $propertyAttributes, $methodParameterAttributes ];
9794
}
9895

9996
/**
@@ -123,4 +120,39 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
123120

124121
return isset($ignored[$attribute->getName()]); // @phpstan-ignore offsetAccess.nonOffsetAccessible
125122
}
123+
124+
/**
125+
* @param array<TransientTargetMethod> $methodAttributes
126+
* @param array<array<TransientTargetMethodParameter>> $methodParameterAttributes
127+
*
128+
* @return void
129+
*/
130+
private function collectMethodAndParameterAttributes(
131+
string $class,
132+
\ReflectionMethod $methodReflection,
133+
array &$methodAttributes,
134+
array &$methodParameterAttributes,
135+
): void {
136+
foreach ($methodReflection->getAttributes() as $attribute) {
137+
if (self::isAttributeIgnored($attribute)) {
138+
continue;
139+
}
140+
141+
$method = $methodReflection->name;
142+
143+
$this->log->debug("Found attribute {$attribute->getName()} on $class::$method");
144+
145+
$methodAttributes[] = new TransientTargetMethod(
146+
$attribute->getName(),
147+
$attribute->getArguments(),
148+
$method,
149+
);
150+
}
151+
152+
$parameterAttributes = $this->methodParameterCollector->collectAttributes($methodReflection);
153+
154+
if (count($parameterAttributes)) {
155+
$methodParameterAttributes[] = $parameterAttributes;
156+
}
157+
}
126158
}

src/Collection.php

Lines changed: 74 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 where 0 are the
27+
* 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,50 @@ 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(
143+
string $attribute,
144+
array $arguments,
145+
string $class,
146+
string $method,
147+
string $parameter,
148+
): object {
149+
try {
150+
$a = new $attribute(...$arguments);
151+
return new TargetMethodParameter($a, $class, $method, $parameter);
152+
} catch (Throwable $e) {
153+
throw new RuntimeException(
154+
"An error occurred while instantiating attribute $attribute on parameter $class::$method($parameter)",
155+
previous: $e,
156+
);
157+
}
158+
}
159+
112160
/**
113161
* @template T of object
114162
*
@@ -196,6 +244,32 @@ public function filterTargetMethods(callable $predicate): array
196244
return $ar;
197245
}
198246

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

src/MemoizeAttributeCollector.php

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

@@ -58,7 +60,8 @@ public function collectAttributes(array $classMap): TransientCollection
5860
$classAttributes,
5961
$methodAttributes,
6062
$propertyAttributes,
61-
] = $this->state[$class] ?? [ 0, [], [], [] ];
63+
$methodParameterAttributes,
64+
] = $this->state[$class] ?? [ 0, [], [], [], [] ];
6265

6366
$mtime = filemtime($filepath);
6467

@@ -75,14 +78,21 @@ public function collectAttributes(array $classMap): TransientCollection
7578
$classAttributes,
7679
$methodAttributes,
7780
$propertyAttributes,
81+
$methodParameterAttributes,
7882
] = $classAttributeCollector->collectAttributes($class);
7983
} catch (Throwable $e) {
8084
$this->log->error(
8185
"Attribute collection failed for $class: {$e->getMessage()}",
8286
);
8387
}
8488

85-
$this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ];
89+
$this->state[$class] = [
90+
time(),
91+
$classAttributes,
92+
$methodAttributes,
93+
$propertyAttributes,
94+
$methodParameterAttributes,
95+
];
8696
}
8797

8898
if (count($classAttributes)) {
@@ -91,6 +101,9 @@ public function collectAttributes(array $classMap): TransientCollection
91101
if (count($methodAttributes)) {
92102
$collector->addMethodAttributes($class, $methodAttributes);
93103
}
104+
if (count($methodParameterAttributes)) {
105+
$collector->addMethodParameterAttributes($class, $methodParameterAttributes);
106+
}
94107
if (count($propertyAttributes)) {
95108
$collector->addTargetProperties($class, $propertyAttributes);
96109
}

src/ParameterAttributeCollector.php

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

src/Plugin.php

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

3333
/**
3434
* @uses onPostAutoloadDump

0 commit comments

Comments
 (0)