Skip to content

Commit 5fa6952

Browse files
authored
Add support for property attributes (#17)
1 parent b61523c commit 5fa6952

29 files changed

+809
-218
lines changed

MIGRATION.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Migration
22

3+
## v1.2 to v1.3
4+
5+
### New Requirements
6+
7+
None
8+
9+
### New features
10+
11+
- The plugin now collects attributes on properties. `Attributes::findTargetProperties()` returns target properties, and `filterTargetProperties()` filters properties with a predicate.
12+
13+
### Backward Incompatible Changes
14+
15+
None
16+
17+
### Deprecated Features
18+
19+
None
20+
21+
### Other Changes
22+
23+
None
24+
25+
26+
327
## v1.1 to v1.2
428

529
### New Requirements

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ Later, these targets can be retrieved through a convenient interface.
1212

1313
#### Features
1414

15-
- Zero configuration.
16-
- No reflection in the generated file.
17-
- No impact on performance.
18-
- No dependency (except Composer of course).
19-
- A single interface to get attribute targets.
20-
- A single interface to get class attributes.
21-
- 3 types of cache speed up generation by limiting updates to changed files.
15+
- Zero configuration
16+
- No reflection in the generated file
17+
- No impact on performance
18+
- No dependency (except Composer of course)
19+
- A single interface to get attribute targets: classes, methods, and properties
20+
- 3 types of cache speed up generation by limiting updates to changed files
2221

2322

2423

@@ -32,6 +31,7 @@ The following example demonstrates how targets and their attributes can be retri
3231
use olvlvl\ComposerAttributeCollector\Attributes;
3332
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
3433
use Symfony\Component\Routing\Annotation\Route;
34+
use Doctrine\ORM\Mapping\Column;
3535

3636
require_once 'vendor/autoload.php';
3737
require_once 'vendor/attributes.php'; // <-- the file created by the plugin
@@ -48,18 +48,25 @@ foreach (Attributes::findTargetMethods(Route::class) as $target) {
4848
var_dump($target->attribute, $target->class, $target->name);
4949
}
5050

51+
// Find the target properties of the Column attribute.
52+
foreach (Attributes::findTargetProperties(Column::class) as $target) {
53+
var_dump($target->attribute, $target->class, $target->name);
54+
}
55+
5156
// Filter target methods using a predicate.
57+
// This is also available for classes and properties.
5258
foreach (Attributes::filterTargetMethods(
5359
fn($attribute) => is_a($attribute, Route::class, true)
5460
) as $target) {
5561
var_dump($target->attribute, $target->class, $target->name);
5662
}
5763

58-
// Find class and method attributes for the ArticleController class.
64+
// Find class, method, and property attributes for the ArticleController class.
5965
$attributes = Attributes::forClass(ArticleController::class);
6066

6167
var_dump($attributes->classAttributes);
6268
var_dump($attributes->methodsAttributes);
69+
var_dump($attributes->propertyAttributes);
6370
```
6471

6572

@@ -204,6 +211,7 @@ $attributes = Attributes::forClass(ArticleController::class);
204211

205212
var_dump($attributes->classAttributes);
206213
var_dump($attributes->methodsAttributes);
214+
var_dump($attributes->propertyAttributes);
207215
```
208216

209217

@@ -372,7 +380,7 @@ $config->from_attributes();
372380

373381
### Filtering target methods
374382

375-
`filterTargetMethods()` can filter target methods using a predicate. This can be helpful when a number of attributes extend another one, and you are interested in collecting any instance of that attribute.
383+
`filterTargetMethods()` can filter target methods using a predicate. This can be helpful when a number of attributes extend another one, and you are interested in collecting any instance of that attribute. The `filerTargetClasses()` and `filterTargetProperties()` methods provide similar feature for classes and properties.
376384

377385
Let's say we have a `Route` attribute extended by `Get`, `Post`, `Put`
378386

src/Attributes.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ public static function findTargetMethods(string $attribute): array
5656
return self::getCollection()->findTargetMethods($attribute);
5757
}
5858

59+
/**
60+
* @template T of object
61+
*
62+
* @param class-string<T> $attribute
63+
*
64+
* @return TargetProperty<T>[]
65+
*/
66+
public static function findTargetProperties(string $attribute): array
67+
{
68+
return self::getCollection()->findTargetProperties($attribute);
69+
}
70+
5971
/**
6072
* @param callable(class-string $attribute, class-string $class):bool $predicate
6173
*
@@ -76,6 +88,16 @@ public static function filterTargetMethods(callable $predicate): array
7688
return self::getCollection()->filterTargetMethods($predicate);
7789
}
7890

91+
/**
92+
* @param callable(class-string $attribute, class-string $class, string $property):bool $predicate
93+
*
94+
* @return array<TargetProperty<object>>
95+
*/
96+
public static function filterTargetProperties(callable $predicate): array
97+
{
98+
return self::getCollection()->filterTargetProperties($predicate);
99+
}
100+
79101
/**
80102
* @param class-string $class
81103
*

src/ClassAttributeCollector.php

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ public function __construct(
2222
* @param class-string $class
2323
*
2424
* @return array{
25-
* array<array{ class-string, array<int|string, mixed> }>,
26-
* array<array{ class-string, array<int|string, mixed>, non-empty-string }>
25+
* array<TransientTargetClass>,
26+
* array<TransientTargetMethod>,
27+
* array<TransientTargetProperty>,
2728
* }
28-
* Where `0` is an array of class attributes, and `1` is an array of method attributes.
29+
* Where `0` is an array of class attributes, `1` is an array of method attributes,
30+
* and `2` is an array of property attributes.
2931
* @throws ReflectionException
3032
*/
3133
public function collectAttributes(string $class): array
3234
{
3335
$classReflection = new ReflectionClass($class);
3436

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

3941
$classAttributes = [];
@@ -46,7 +48,10 @@ public function collectAttributes(string $class): array
4648

4749
$this->io->debug("Found attribute {$attribute->getName()} on $class");
4850

49-
$classAttributes[] = [ $attribute->getName(), $attribute->getArguments() ];
51+
$classAttributes[] = new TransientTargetClass(
52+
$attribute->getName(),
53+
$attribute->getArguments(),
54+
);
5055
}
5156

5257
$methodAttributes = [];
@@ -62,11 +67,36 @@ public function collectAttributes(string $class): array
6267

6368
$this->io->debug("Found attribute {$attribute->getName()} on $class::$method");
6469

65-
$methodAttributes[] = [ $attribute->getName(), $attribute->getArguments(), $method ];
70+
$methodAttributes[] = new TransientTargetMethod(
71+
$attribute->getName(),
72+
$attribute->getArguments(),
73+
$method,
74+
);
6675
}
6776
}
6877

69-
return [ $classAttributes, $methodAttributes ];
78+
$propertyAttributes = [];
79+
80+
foreach ($classReflection->getProperties() as $propertyReflection) {
81+
foreach ($propertyReflection->getAttributes() as $attribute) {
82+
if (self::isAttributeIgnored($attribute)) {
83+
continue;
84+
}
85+
86+
$property = $propertyReflection->name;
87+
assert($property !== '');
88+
89+
$this->io->debug("Found attribute {$attribute->getName()} on $class::$property");
90+
91+
$propertyAttributes[] = new TransientTargetProperty(
92+
$attribute->getName(),
93+
$attribute->getArguments(),
94+
$property,
95+
);
96+
}
97+
}
98+
99+
return [ $classAttributes, $methodAttributes, $propertyAttributes ];
70100
}
71101

72102
/**

src/Collection.php

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ final class Collection
2323
* @param array<class-string, array<array{ mixed[], class-string }>> $targetClasses
2424
* Where _key_ is an attribute class and _value_ an array of arrays
2525
* where 0 are the attribute arguments and 1 is a target class.
26-
* @param array<class-string, array<array{ mixed[], class-string, string }>> $targetMethods
26+
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string }>> $targetMethods
2727
* Where _key_ is an attribute class and _value_ an array of arrays
2828
* where 0 are the attribute arguments, 1 is a target class, and 2 is the target method.
29+
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string }>> $targetProperties
30+
* Where _key_ is an attribute class and _value_ an array of arrays
31+
* where 0 are the attribute arguments, 1 is a target class, and 2 is the target property.
2932
*/
3033
public function __construct(
3134
private array $targetClasses,
3235
private array $targetMethods,
36+
private array $targetProperties,
3337
) {
3438
}
3539

@@ -84,6 +88,71 @@ public function findTargetMethods(string $attribute): array
8488
);
8589
}
8690

91+
/**
92+
* @template T of object
93+
*
94+
* @param class-string<T> $attribute
95+
* @param array<int|string, mixed> $arguments
96+
* @param class-string $class
97+
*
98+
* @return T
99+
*/
100+
private static function createMethodAttribute(
101+
string $attribute,
102+
array $arguments,
103+
string $class,
104+
string $method
105+
): object {
106+
try {
107+
return new $attribute(...$arguments);
108+
} catch (Throwable $e) {
109+
throw new RuntimeException(
110+
"An error occurred while instantiating attribute $attribute on method $class::$method",
111+
previous: $e
112+
);
113+
}
114+
}
115+
116+
/**
117+
* @template T of object
118+
*
119+
* @param class-string<T> $attribute
120+
*
121+
* @return array<TargetProperty<T>>
122+
*/
123+
public function findTargetProperties(string $attribute): array
124+
{
125+
return array_map(
126+
fn(array $a) => new TargetProperty(self::createPropertyAttribute($attribute, ...$a), $a[1], $a[2]),
127+
$this->targetProperties[$attribute] ?? []
128+
);
129+
}
130+
131+
/**
132+
* @template T of object
133+
*
134+
* @param class-string<T> $attribute
135+
* @param array<int|string, mixed> $arguments
136+
* @param class-string $class
137+
*
138+
* @return T
139+
*/
140+
private static function createPropertyAttribute(
141+
string $attribute,
142+
array $arguments,
143+
string $class,
144+
string $property
145+
): object {
146+
try {
147+
return new $attribute(...$arguments);
148+
} catch (Throwable $e) {
149+
throw new RuntimeException(
150+
"An error occurred while instantiating attribute $attribute on property $class::$property",
151+
previous: $e
152+
);
153+
}
154+
}
155+
87156
/**
88157
* @param callable(class-string $attribute, class-string $class):bool $predicate
89158
*
@@ -105,7 +174,7 @@ public function filterTargetClasses(callable $predicate): array
105174
}
106175

107176
/**
108-
* @param callable(class-string $attribute, class-string $class, string $method):bool $predicate
177+
* @param callable(class-string $attribute, class-string $class, non-empty-string $method):bool $predicate
109178
*
110179
* @return array<TargetMethod<object>>
111180
*/
@@ -130,28 +199,28 @@ public function filterTargetMethods(callable $predicate): array
130199
}
131200

132201
/**
133-
* @template T of object
134-
*
135-
* @param class-string<T> $attribute
136-
* @param array<int|string, mixed> $arguments
137-
* @param class-string $class
202+
* @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate
138203
*
139-
* @return T
204+
* @return array<TargetProperty<object>>
140205
*/
141-
private static function createMethodAttribute(
142-
string $attribute,
143-
array $arguments,
144-
string $class,
145-
string $method
146-
): object {
147-
try {
148-
return new $attribute(...$arguments);
149-
} catch (Throwable $e) {
150-
throw new RuntimeException(
151-
"An error occurred while instantiating attribute $attribute on method $class::$method",
152-
previous: $e
153-
);
206+
public function filterTargetProperties(callable $predicate): array
207+
{
208+
$ar = [];
209+
210+
foreach ($this->targetProperties as $attribute => $references) {
211+
foreach ($references as [ $arguments, $class, $property ]) {
212+
if ($predicate($attribute, $class, $property)) {
213+
$ar[] = new TargetProperty(self::createPropertyAttribute(
214+
$attribute,
215+
$arguments,
216+
$class,
217+
$property
218+
), $class, $property);
219+
}
220+
}
154221
}
222+
223+
return $ar;
155224
}
156225

157226
/**
@@ -171,6 +240,16 @@ public function forClass(string $class): ForClass
171240
$methodAttributes[$targetMethod->name][] = $targetMethod->attribute;
172241
}
173242

174-
return new ForClass($classAttributes, $methodAttributes);
243+
$propertyAttributes = [];
244+
245+
foreach ($this->filterTargetProperties(fn($a, $c): bool => $c === $class) as $targetProperty) {
246+
$propertyAttributes[$targetProperty->name][] = $targetProperty->attribute;
247+
}
248+
249+
return new ForClass(
250+
$classAttributes,
251+
$methodAttributes,
252+
$propertyAttributes,
253+
);
175254
}
176255
}

0 commit comments

Comments
 (0)