Skip to content

Commit

Permalink
Use reflection (alt)
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Nov 23, 2024
1 parent 01534d0 commit efe9ca2
Show file tree
Hide file tree
Showing 21 changed files with 700 additions and 265 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ None
### New features

- The `InheritsAttributes` attribute can be used on classes that inherit their attributes from traits, properties, or methods, and were previously ignored by the collection process.
- The plugin can generate a file that uses reflection to create attributes instead of embedding their arguments.

### Backward Incompatible Changes

Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ _discover_ attribute targets in a codebase—for known targets you can use refle
#### Features

- Little configuration
- No reflection in the generated file
- No reflection in the generated file (as an option)
- No impact on performance
- No dependency (except Composer of course)
- A single interface to get attribute targets: classes, methods, and properties
Expand Down Expand Up @@ -77,6 +77,10 @@ var_dump($attributes->methodsAttributes);
var_dump($attributes->propertyAttributes);
```

> [!NOTE]
> The plugin supports class, method, and property targets. [Contribute](CONTRIBUTING.md) if you're
> interested in expending its support.


## Getting started
Expand Down Expand Up @@ -192,6 +196,30 @@ replaced with the path to the vendor folder.
}
```

### Use reflection

> [!NOTE]
> New in v2.1
By default, the "attributes" file embeds arguments to instantiate attributes without using
reflection. This can cause issues when the arguments are sophisticated types that don't support var
export. Alternatively, attributes can be instantiated using reflection.

The API remains the same whether reflections are used or not. You can safely switch between modes
and see what works best for you.

Use the `use-reflection` property for the "attributes" file to use reflection.

```json
{
"extra": {
"composer-attribute-collector": {
"use-reflection": true
}
}
}
```

### Cache discoveries between runs

The plugin is able to maintain a cache to reuse discoveries between runs. To enable the cache,
Expand All @@ -205,6 +233,7 @@ COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=1 composer dump-autoload




## Test drive with the Symfony Demo

You can try the plugin with a fresh installation of the [Symfony Demo Application](https://github.com/symfony/demo).
Expand Down
2 changes: 2 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<exclude-pattern>tests/bootstrap.php</exclude-pattern>
</rule>
<rule ref="Generic.Files.LineLength.TooLong">
<exclude-pattern>src/Static/TransientCollectionRenderer.php</exclude-pattern>
<exclude-pattern>src/Reflexive/TransientCollectionRenderer.php</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
parameters:
bootstrapFiles:
- tests/bootstrap.php
level: max
level: 9
paths:
- src
28 changes: 24 additions & 4 deletions src/ClassAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
*/
class ClassAttributeCollector
{
/**
* @param bool $ignoreArguments
* Attribute arguments aren't used when generating a file that uses reflection.
* Setting `$ignoreArguments` to `true` ignores arguments during the attribute collection.
*/
public function __construct(
private IOInterface $io,
private bool $ignoreArguments,
) {
}

Expand Down Expand Up @@ -49,7 +55,7 @@ public function collectAttributes(string $class): array

$classAttributes[] = new TransientTargetClass(
$attribute->getName(),
$attribute->getArguments(),
$this->getArguments($attribute),
);
}

Expand All @@ -67,7 +73,7 @@ public function collectAttributes(string $class): array

$methodAttributes[] = new TransientTargetMethod(
$attribute->getName(),
$attribute->getArguments(),
$this->getArguments($attribute),
$method,
);
}
Expand All @@ -88,7 +94,7 @@ public function collectAttributes(string $class): array

$propertyAttributes[] = new TransientTargetProperty(
$attribute->getName(),
$attribute->getArguments(),
$this->getArguments($attribute),
$property,
);
}
Expand Down Expand Up @@ -123,6 +129,20 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
InheritsAttributes::class => true,
];

return isset($ignored[$attribute->getName()]); // @phpstan-ignore offsetAccess.nonOffsetAccessible
return isset($ignored[$attribute->getName()]);
}

/**
* @param ReflectionAttribute<object> $attribute
*
* @return array<string, mixed>
*/
private function getArguments(ReflectionAttribute $attribute): array
{
if ($this->ignoreArguments) {
return [];
}

return $attribute->getArguments();
}
}
178 changes: 7 additions & 171 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,16 @@

namespace olvlvl\ComposerAttributeCollector;

use RuntimeException;
use Throwable;

use function array_map;

/**
* @internal
*/
final class Collection
abstract class Collection
{
/**
* @param array<class-string, array<array{ mixed[], class-string }>> $targetClasses
* Where _key_ is an attribute class and _value_ an array of arrays
* where 0 are the attribute arguments and 1 is a target class.
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string }>> $targetMethods
* Where _key_ is an attribute class and _value_ an array of arrays
* where 0 are the attribute arguments, 1 is a target class, and 2 is the target method.
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string }>> $targetProperties
* Where _key_ is an attribute class and _value_ an array of arrays
* where 0 are the attribute arguments, 1 is a target class, and 2 is the target property.
*/
public function __construct(
private array $targetClasses,
private array $targetMethods,
private array $targetProperties,
) {
}

/**
* @template T of object
*
* @param class-string<T> $attribute
*
* @return array<TargetClass<T>>
*/
public function findTargetClasses(string $attribute): array
{
return array_map(
fn(array $a) => new TargetClass(self::createClassAttribute($attribute, ...$a), $a[1]),
$this->targetClasses[$attribute] ?? []
);
}

/**
* @template T of object
*
* @param class-string<T> $attribute
* @param array<int|string, mixed> $arguments
* @param class-string $class
*
* @return T
*/
private static function createClassAttribute(string $attribute, array $arguments, string $class): object
{
try {
return new $attribute(...$arguments);
} catch (Throwable $e) {
throw new RuntimeException(
"An error occurred while instantiating attribute $attribute on class $class",
previous: $e
);
}
}
abstract public function findTargetClasses(string $attribute): array;

/**
* @template T of object
Expand All @@ -73,38 +20,7 @@ private static function createClassAttribute(string $attribute, array $arguments
*
* @return array<TargetMethod<T>>
*/
public function findTargetMethods(string $attribute): array
{
return array_map(
fn(array $a) => new TargetMethod(self::createMethodAttribute($attribute, ...$a), $a[1], $a[2]),
$this->targetMethods[$attribute] ?? []
);
}

/**
* @template T of object
*
* @param class-string<T> $attribute
* @param array<int|string, mixed> $arguments
* @param class-string $class
*
* @return T
*/
private static function createMethodAttribute(
string $attribute,
array $arguments,
string $class,
string $method
): object {
try {
return new $attribute(...$arguments);
} catch (Throwable $e) {
throw new RuntimeException(
"An error occurred while instantiating attribute $attribute on method $class::$method",
previous: $e
);
}
}
abstract public function findTargetMethods(string $attribute): array;

/**
* @template T of object
Expand All @@ -113,108 +29,28 @@ private static function createMethodAttribute(
*
* @return array<TargetProperty<T>>
*/
public function findTargetProperties(string $attribute): array
{
return array_map(
fn(array $a) => new TargetProperty(self::createPropertyAttribute($attribute, ...$a), $a[1], $a[2]),
$this->targetProperties[$attribute] ?? []
);
}

/**
* @template T of object
*
* @param class-string<T> $attribute
* @param array<int|string, mixed> $arguments
* @param class-string $class
*
* @return T
*/
private static function createPropertyAttribute(
string $attribute,
array $arguments,
string $class,
string $property
): object {
try {
return new $attribute(...$arguments);
} catch (Throwable $e) {
throw new RuntimeException(
"An error occurred while instantiating attribute $attribute on property $class::$property",
previous: $e
);
}
}
abstract public function findTargetProperties(string $attribute): array;

/**
* @param callable(class-string $attribute, class-string $class):bool $predicate
*
* @return array<TargetClass<object>>
*/
public function filterTargetClasses(callable $predicate): array
{
$ar = [];

foreach ($this->targetClasses as $attribute => $references) {
foreach ($references as [ $arguments, $class ]) {
if ($predicate($attribute, $class)) {
$ar[] = new TargetClass(self::createClassAttribute($attribute, $arguments, $class), $class);
}
}
}

return $ar;
}
abstract public function filterTargetClasses(callable $predicate): array;

/**
* @param callable(class-string $attribute, class-string $class, non-empty-string $method):bool $predicate
*
* @return array<TargetMethod<object>>
*/
public function filterTargetMethods(callable $predicate): array
{
$ar = [];

foreach ($this->targetMethods as $attribute => $references) {
foreach ($references as [ $arguments, $class, $method ]) {
if ($predicate($attribute, $class, $method)) {
$ar[] = new TargetMethod(self::createMethodAttribute(
$attribute,
$arguments,
$class,
$method
), $class, $method);
}
}
}

return $ar;
}
abstract public function filterTargetMethods(callable $predicate): array;

/**
* @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate
*
* @return array<TargetProperty<object>>
*/
public function filterTargetProperties(callable $predicate): array
{
$ar = [];

foreach ($this->targetProperties as $attribute => $references) {
foreach ($references as [ $arguments, $class, $property ]) {
if ($predicate($attribute, $class, $property)) {
$ar[] = new TargetProperty(self::createPropertyAttribute(
$attribute,
$arguments,
$class,
$property
), $class, $property);
}
}
}

return $ar;
}
abstract public function filterTargetProperties(callable $predicate): array;

/**
* @param class-string $class
Expand Down
Loading

0 comments on commit efe9ca2

Please sign in to comment.