Skip to content

Commit 1b262eb

Browse files
committed
Add AttributeReader* and AttributeInstantiator*.
1 parent 72ba3ad commit 1b262eb

10 files changed

Lines changed: 368 additions & 11 deletions

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,62 @@ $attribute_instances = get_attributes($reflection_function, MyReflectorAwareAttr
7878
assert($attribute_instances[0] instanceof MyReflectorAwareAttribute);
7979
assert($attribute_instances[0]->reflector === $reflection_function);
8080
```
81+
82+
### Using AttributeReader.
83+
84+
The AttributeReader class allows to support attribute types that do not use the mechanisms from this package.
85+
86+
Assume you have an attribute class like this, coming from a 3rd party package:
87+
88+
```php
89+
#[\Attribute(\Attribute::TARGET_CLASS)]
90+
class ThirdPartyAttribute {
91+
public readonly \Reflector $reflector;
92+
}
93+
```
94+
95+
You can create a custom instantiator that will populate the property.
96+
97+
Ideally this should use the decorator pattern, so that multiple operations can be applied.
98+
99+
```php
100+
use Ock\ReflectorAwareAttributes\Instantiator\AttributeInstantiatorInterface;
101+
use Ock\ReflectorAwareAttributes\Reader\AttributeReader;
102+
103+
class MyInstantiator implements AttributeInstantiatorInterface {
104+
105+
public function __construct(
106+
private readonly AttributeInstantiatorInterface $decorated,
107+
) {}
108+
109+
public function newInstance(
110+
\ReflectionAttribute $attribute,
111+
\ReflectionClassConstant|\ReflectionParameter|\ReflectionClass|\ReflectionProperty|\ReflectionFunctionAbstract $reflector,
112+
): object {
113+
$instance = $this->decorated->newInstance($attribute, $reflector);
114+
if ($instance instanceof ThirdPartyAttribute) {
115+
$instance->reflector = $reflector;
116+
}
117+
return $instance;
118+
}
119+
120+
}
121+
```
122+
123+
Now we can use a reader with this instantiator decorator to get the attribute instances.
124+
125+
```php
126+
#[ThirdPartyAttribute]
127+
class C {}
128+
129+
$reader = AttributeReader::basic()
130+
->withDecoratingInstantiator(
131+
fn (AttributeInstantiatorInterface $decorated) => new MyInstantiator($decorated),
132+
);
133+
134+
$reflection_class = new \ReflectionClass(C::class);
135+
$instances = $reader->getInstances($reflection_class, ThirdPartyAttribute::class);
136+
137+
assert($instances[0] instanceof ThirdPartyAttribute);
138+
assert($instances[0]->reflector === $reflection_class);
139+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Instantiator;
6+
7+
/**
8+
* Static factories for attribute instantiators.
9+
*/
10+
class AttributeInstantiator {
11+
12+
/**
13+
* Creates an instantiator with decorators.
14+
*
15+
* @param array<\Closure(AttributeInstantiatorInterface): AttributeInstantiatorInterface> $decorators
16+
* Callbacks that decorate the instantiator.
17+
*/
18+
public static function compose(array $decorators): AttributeInstantiatorInterface {
19+
$instantiator = new BasicAttributeInstantiator();
20+
foreach ($decorators as $decorator) {
21+
$instantiator = $decorator($instantiator);
22+
assert($instantiator instanceof AttributeInstantiatorInterface);
23+
}
24+
return $instantiator;
25+
}
26+
27+
/**
28+
* Creates an instantiator with the default decorators from this package.
29+
*/
30+
public static function createDefault(): AttributeInstantiatorInterface {
31+
$instantiator = new BasicAttributeInstantiator();
32+
$instantiator = new ReflectorProvidingDecorator($instantiator);
33+
$instantiator = new ReflectorSettingDecorator($instantiator);
34+
return $instantiator;
35+
}
36+
37+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Instantiator;
6+
7+
interface AttributeInstantiatorInterface {
8+
9+
public function newInstance(
10+
\ReflectionAttribute $attribute,
11+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
12+
): object;
13+
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Instantiator;
6+
7+
/**
8+
* Basic attribute instantiator without any additional functionality.
9+
*/
10+
class BasicAttributeInstantiator implements AttributeInstantiatorInterface {
11+
12+
#[\Override]
13+
public function newInstance(
14+
\ReflectionAttribute $attribute,
15+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
16+
): object {
17+
return $attribute->newInstance();
18+
}
19+
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Instantiator;
6+
7+
use Ock\ReflectorAwareAttributes\AttributeConstructor;
8+
9+
/**
10+
* Decorator that makes the reflector available to the attribute constructor.
11+
*
12+
* @see AttributeConstructor::getReflector()
13+
*/
14+
class ReflectorProvidingDecorator implements AttributeInstantiatorInterface {
15+
16+
public function __construct(
17+
private readonly AttributeInstantiatorInterface $decorated,
18+
) {}
19+
20+
#[\Override]
21+
public function newInstance(
22+
\ReflectionAttribute $attribute,
23+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
24+
): object {
25+
return AttributeConstructor::callWithReflector(
26+
fn () => $this->decorated->newInstance($attribute, $reflector),
27+
$reflector,
28+
);
29+
}
30+
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Instantiator;
6+
7+
use Ock\ReflectorAwareAttributes\ReflectorAwareAttributeInterface;
8+
9+
/**
10+
* Decorator that calls ->setReflector() on the attribute instance.
11+
*
12+
* @see ReflectorAwareAttributeInterface::setReflector()
13+
*/
14+
class ReflectorSettingDecorator implements AttributeInstantiatorInterface {
15+
16+
public function __construct(
17+
private readonly AttributeInstantiatorInterface $decorated,
18+
) {}
19+
20+
#[\Override]
21+
public function newInstance(
22+
\ReflectionAttribute $attribute,
23+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
24+
): object {
25+
$instance = $this->decorated->newInstance($attribute, $reflector);
26+
if ($instance instanceof ReflectorAwareAttributeInterface) {
27+
$instance->setReflector($reflector);
28+
}
29+
return $instance;
30+
}
31+
32+
}

src/Reader/AttributeReader.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Reader;
6+
7+
use Ock\ReflectorAwareAttributes\Instantiator\AttributeInstantiator;
8+
use Ock\ReflectorAwareAttributes\Instantiator\AttributeInstantiatorInterface;
9+
use Ock\ReflectorAwareAttributes\Instantiator\BasicAttributeInstantiator;
10+
11+
class AttributeReader implements AttributeReaderInterface {
12+
13+
/**
14+
* Static cache for the basic reader.
15+
*/
16+
private static self $basicInstance;
17+
18+
/**
19+
* Static cache for the default reader.
20+
*/
21+
private static self $defaultInstance;
22+
23+
public function __construct(
24+
private AttributeInstantiatorInterface $instantiator,
25+
) {}
26+
27+
/**
28+
* Creates a reader with no extra behaviors.
29+
*/
30+
public static function basic(): self {
31+
return self::$basicInstance ??= new self(new BasicAttributeInstantiator());
32+
}
33+
34+
/**
35+
* Creates a reader with the default extra behaviors from this package.
36+
*/
37+
public static function default(): self {
38+
return self::$defaultInstance ??= new self(AttributeInstantiator::createDefault());
39+
}
40+
41+
/**
42+
* @param \Closure(AttributeInstantiatorInterface): AttributeInstantiatorInterface $decorate
43+
*
44+
* @return static
45+
*/
46+
public function withDecoratingInstantiator(\Closure $decorate): static {
47+
$clone = clone $this;
48+
$clone->instantiator = $decorate($this->instantiator);
49+
return $clone;
50+
}
51+
52+
#[\Override]
53+
public function getInstances(
54+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
55+
string $name,
56+
int $flags = \ReflectionAttribute::IS_INSTANCEOF,
57+
): array {
58+
$attributes = $reflector->getAttributes($name, $flags);
59+
return array_map(
60+
fn (\ReflectionAttribute $attribute) => $this->instantiator->newInstance($attribute, $reflector),
61+
$attributes,
62+
);
63+
}
64+
65+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
*/
6+
7+
declare(strict_types = 1);
8+
9+
namespace Ock\ReflectorAwareAttributes\Reader;
10+
11+
interface AttributeReaderInterface {
12+
13+
/**
14+
* @template T of object
15+
*
16+
* @param \ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector
17+
* @param class-string<T> $name
18+
* @param int $flags
19+
*
20+
* @return list<T&object>
21+
*/
22+
public function getInstances(
23+
\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant $reflector,
24+
string $name,
25+
int $flags = \ReflectionAttribute::IS_INSTANCEOF,
26+
): array;
27+
28+
}

tests/src/AttributeReaderTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ReflectorAwareAttributes\Tests;
6+
7+
use Ock\ReflectorAwareAttributes\Instantiator\BasicAttributeInstantiator;
8+
use Ock\ReflectorAwareAttributes\Instantiator\ReflectorProvidingDecorator;
9+
use Ock\ReflectorAwareAttributes\Reader\AttributeReader;
10+
use PHPUnit\Framework\Assert;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\TestCase;
13+
14+
#[CoversClass(AttributeReader::class)]
15+
class AttributeReaderTest extends TestCase {
16+
17+
public function testWithDecorator(): void {
18+
$instantiator = new BasicAttributeInstantiator();
19+
$reader = new AttributeReader($instantiator);
20+
$reader = $reader->withDecoratingInstantiator(function ($decorated) use ($instantiator, &$decorating) {
21+
Assert::assertSame($instantiator, $decorated);
22+
$decorating = new ReflectorProvidingDecorator($decorated);
23+
return $decorating;
24+
});
25+
$reader = $reader->withDecoratingInstantiator(function ($decorated) use ($decorating) {
26+
Assert::assertSame($decorating, $decorated);
27+
return $decorated;
28+
});
29+
$this->assertEquals(
30+
new AttributeReader(
31+
new ReflectorProvidingDecorator($instantiator),
32+
),
33+
$reader,
34+
);
35+
}
36+
37+
}

0 commit comments

Comments
 (0)