Skip to content

Commit 29749d4

Browse files
olvlvlstaabm
andauthored
Collect parameter attributes (#37)
--------- Co-authored-by: Markus Staab <[email protected]>
1 parent d8f26b0 commit 29749d4

19 files changed

+511
-31
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ None
1010

1111
Attributes are now collected from interfaces and traits as well as classes.
1212

13+
Parameter attributes are now collected. Use the method `findTargetParameters()`
14+
to find target parameters, and the method `filterTargetParameters()` to filter
15+
target parameters according to a predicate.
16+
1317
### Deprecated Features
1418

1519
None

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ _discover_ attribute targets in a codebase—for known targets you can use refle
2222
- Can cache discoveries to speed up consecutive runs.
2323

2424
> [!NOTE]
25-
> Currently, the plugin supports class, method, and property targets.
25+
> Currently, the plugin supports class, method, property, and parameter targets.
2626
> You're welcome to [contribute](CONTRIBUTING.md) if you're interested in expending its support.
2727
28+
> [!WARNING]
29+
> Attributes used on functions are ignored at this time.
30+
2831

2932

3033
#### Usage
@@ -59,6 +62,11 @@ foreach (Attributes::findTargetProperties(Column::class) as $target) {
5962
var_dump($target->attribute, $target->class, $target->name);
6063
}
6164

65+
// Find the target method parameters of the UserInput attribute.
66+
foreach (Attributes::findTargetParameters(UserInput::class) as $target) {
67+
var_dump($target->attribute, $target->class, $target->method, $target->name);
68+
}
69+
6270
// Filter target methods using a predicate.
6371
// You can also filter target classes and properties.
6472
$predicate = fn($attribute) => is_a($attribute, Route::class, true);

phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<exclude-pattern>tests/bootstrap.php</exclude-pattern>
1919
</rule>
2020
<rule ref="Generic.Files.LineLength.TooLong">
21+
<exclude-pattern>src/Collection.php</exclude-pattern>
2122
<exclude-pattern>tests/*</exclude-pattern>
2223
</rule>
2324
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">

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 TargetParameter<T>[]
70+
*/
71+
public static function findTargetParameters(string $attribute): array
72+
{
73+
return self::getCollection()->findTargetParameters($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<TargetParameter<object>>
110+
*/
111+
public static function filterTargetParameters(callable $predicate): array
112+
{
113+
return self::getCollection()->filterTargetParameters($predicate);
114+
}
115+
94116
/**
95117
* @param class-string $class
96118
*

src/ClassAttributeCollector.php

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
/**
1111
* @internal
12+
* @readonly
1213
*/
1314
class ClassAttributeCollector
1415
{
@@ -24,6 +25,7 @@ public function __construct(
2425
* array<TransientTargetClass>,
2526
* array<TransientTargetMethod>,
2627
* array<TransientTargetProperty>,
28+
* array<TransientTargetParameter>,
2729
* }
2830
*
2931
* @throws ReflectionException
@@ -33,7 +35,7 @@ public function collectAttributes(string $class): array
3335
$classReflection = new ReflectionClass($class);
3436

3537
if (self::isAttribute($classReflection)) {
36-
return [ [], [], [] ];
38+
return [ [], [], [], [] ];
3739
}
3840

3941
$classAttributes = [];
@@ -52,24 +54,18 @@ public function collectAttributes(string $class): array
5254
);
5355
}
5456

57+
/** @var array<TransientTargetMethod> $methodAttributes */
5558
$methodAttributes = [];
59+
/** @var array<TransientTargetParameter> $parameterAttributes */
60+
$parameterAttributes = [];
5661

5762
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-
}
63+
$this->collectMethodAndParameterAttributes(
64+
$class,
65+
$methodReflection,
66+
$methodAttributes,
67+
$parameterAttributes,
68+
);
7369
}
7470

7571
$propertyAttributes = [];
@@ -93,7 +89,7 @@ public function collectAttributes(string $class): array
9389
}
9490
}
9591

96-
return [ $classAttributes, $methodAttributes, $propertyAttributes ];
92+
return [ $classAttributes, $methodAttributes, $propertyAttributes, $parameterAttributes ];
9793
}
9894

9995
/**
@@ -119,8 +115,75 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
119115
{
120116
static $ignored = [
121117
\ReturnTypeWillChange::class => true,
118+
\SensitiveParameter::class => true,
122119
];
123120

124121
return isset($ignored[$attribute->getName()]); // @phpstan-ignore offsetAccess.nonOffsetAccessible
125122
}
123+
124+
/**
125+
* @param array<TransientTargetMethod> $methodAttributes
126+
* @param array<TransientTargetParameter> $parameterAttributes
127+
*/
128+
private function collectMethodAndParameterAttributes(
129+
string $class,
130+
\ReflectionMethod $methodReflection,
131+
array &$methodAttributes,
132+
array &$parameterAttributes,
133+
): void {
134+
foreach ($methodReflection->getAttributes() as $attribute) {
135+
if (self::isAttributeIgnored($attribute)) {
136+
continue;
137+
}
138+
139+
$method = $methodReflection->name;
140+
141+
$this->log->debug("Found attribute {$attribute->getName()} on $class::$method");
142+
143+
$methodAttributes[] = new TransientTargetMethod(
144+
$attribute->getName(),
145+
$attribute->getArguments(),
146+
$method,
147+
);
148+
}
149+
150+
$parameterAttributes = array_merge(
151+
$parameterAttributes,
152+
$this->collectParameterAttributes($methodReflection),
153+
);
154+
}
155+
156+
/**
157+
* @return array<TransientTargetParameter>
158+
*/
159+
private function collectParameterAttributes(\ReflectionMethod $reflectionFunctionAbstract): array
160+
{
161+
$targets = [];
162+
$class = $reflectionFunctionAbstract->class;
163+
$method = $reflectionFunctionAbstract->name;
164+
165+
foreach ($reflectionFunctionAbstract->getParameters() as $parameter) {
166+
/** @var non-empty-string $name */
167+
$name = $parameter->name;
168+
169+
$paramLabel = $class . '::' . $method . '(' . $name . ')';
170+
171+
foreach ($parameter->getAttributes() as $attribute) {
172+
if (self::isAttributeIgnored($attribute)) {
173+
continue;
174+
}
175+
176+
$this->log->debug("Found attribute {$attribute->getName()} on $paramLabel");
177+
178+
$targets[] = new TransientTargetParameter(
179+
$attribute->getName(),
180+
$attribute->getArguments(),
181+
$method,
182+
$name
183+
);
184+
}
185+
}
186+
187+
return $targets;
188+
}
126189
}

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 }>> $targetParameters
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 $targetParameters,
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<TargetParameter<T>>
122+
*/
123+
public function findTargetParameters(string $attribute): array
124+
{
125+
return array_map(
126+
fn(array $t) => self::createParameterAttribute($attribute, ...$t),
127+
$this->targetParameters[$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 TargetParameter<T>
141+
*/
142+
private static function createParameterAttribute(
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 TargetParameter($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<TargetParameter<object>>
251+
*/
252+
public function filterTargetParameters(callable $predicate): array
253+
{
254+
$ar = [];
255+
256+
foreach ($this->targetParameters as $attribute => $references) {
257+
foreach ($references as [$arguments, $class, $method, $parameter]) {
258+
if ($predicate($attribute, $class, $method, $parameter)) {
259+
$ar[] = self::createParameterAttribute(
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: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ class MemoizeAttributeCollector
2222
* array<TransientTargetClass>,
2323
* array<TransientTargetMethod>,
2424
* array<TransientTargetProperty>,
25+
* array<TransientTargetParameter>,
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
28-
* - `1` is an array of class attributes
29-
* - `2` is an array of method attributes
30-
* - `3` is an array of property attributes
3129
*/
3230
private array $state;
3331

@@ -58,7 +56,8 @@ public function collectAttributes(array $classMap): TransientCollection
5856
$classAttributes,
5957
$methodAttributes,
6058
$propertyAttributes,
61-
] = $this->state[$class] ?? [ 0, [], [], [] ];
59+
$parameterAttributes,
60+
] = $this->state[$class] ?? [ 0, [], [], [], [] ];
6261

6362
$mtime = filemtime($filepath);
6463

@@ -75,14 +74,21 @@ public function collectAttributes(array $classMap): TransientCollection
7574
$classAttributes,
7675
$methodAttributes,
7776
$propertyAttributes,
77+
$parameterAttributes,
7878
] = $classAttributeCollector->collectAttributes($class);
7979
} catch (Throwable $e) {
8080
$this->log->error(
8181
"Attribute collection failed for $class: {$e->getMessage()}",
8282
);
8383
}
8484

85-
$this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ];
85+
$this->state[$class] = [
86+
time(),
87+
$classAttributes,
88+
$methodAttributes,
89+
$propertyAttributes,
90+
$parameterAttributes,
91+
];
8692
}
8793

8894
if (count($classAttributes)) {
@@ -91,6 +97,9 @@ public function collectAttributes(array $classMap): TransientCollection
9197
if (count($methodAttributes)) {
9298
$collector->addMethodAttributes($class, $methodAttributes);
9399
}
100+
if (count($parameterAttributes)) {
101+
$collector->addParameterAttributes($class, $parameterAttributes);
102+
}
94103
if (count($propertyAttributes)) {
95104
$collector->addTargetProperties($class, $propertyAttributes);
96105
}

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)