Skip to content

Collect parameter attributes #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ None

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

Parameter attributes are now collected. Use the method `findTargetParameters()`
to find target parameters, and the method `filterTargetParameters()` to filter
target parameters according to a predicate.

### Deprecated Features

None
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ _discover_ attribute targets in a codebase—for known targets you can use refle
- Can cache discoveries to speed up consecutive runs.

> [!NOTE]
> Currently, the plugin supports class, method, and property targets.
> Currently, the plugin supports class, method, property, and parameter targets.
> You're welcome to [contribute](CONTRIBUTING.md) if you're interested in expending its support.

> [!WARNING]
> Attributes used on functions are ignored at this time.



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

// Find the target method parameters of the UserInput attribute.
foreach (Attributes::findTargetParameters(UserInput::class) as $target) {
var_dump($target->attribute, $target->class, $target->method, $target->name);
}

// Filter target methods using a predicate.
// You can also filter target classes and properties.
$predicate = fn($attribute) => is_a($attribute, Route::class, true);
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<exclude-pattern>tests/bootstrap.php</exclude-pattern>
</rule>
<rule ref="Generic.Files.LineLength.TooLong">
<exclude-pattern>src/Collection.php</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
Expand Down
22 changes: 22 additions & 0 deletions src/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public static function findTargetProperties(string $attribute): array
return self::getCollection()->findTargetProperties($attribute);
}

/**
* @template T of object
*
* @param class-string<T> $attribute
*
* @return TargetParameter<T>[]
*/
public static function findTargetParameters(string $attribute): array
{
return self::getCollection()->findTargetParameters($attribute);
}

/**
* @param callable(class-string $attribute, class-string $class):bool $predicate
*
Expand Down Expand Up @@ -91,6 +103,16 @@ public static function filterTargetProperties(callable $predicate): array
return self::getCollection()->filterTargetProperties($predicate);
}

/**
* @param callable(class-string $attribute, class-string $class, string $property, string $method):bool $predicate
*
* @return array<TargetParameter<object>>
*/
public static function filterTargetParameters(callable $predicate): array
{
return self::getCollection()->filterTargetParameters($predicate);
}

/**
* @param class-string $class
*
Expand Down
97 changes: 80 additions & 17 deletions src/ClassAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

/**
* @internal
* @readonly
*/
class ClassAttributeCollector
{
Expand All @@ -24,6 +25,7 @@ public function __construct(
* array<TransientTargetClass>,
* array<TransientTargetMethod>,
* array<TransientTargetProperty>,
* array<TransientTargetParameter>,
* }
*
* @throws ReflectionException
Expand All @@ -33,7 +35,7 @@ public function collectAttributes(string $class): array
$classReflection = new ReflectionClass($class);

if (self::isAttribute($classReflection)) {
return [ [], [], [] ];
return [ [], [], [], [] ];
}

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

/** @var array<TransientTargetMethod> $methodAttributes */
$methodAttributes = [];
/** @var array<TransientTargetParameter> $parameterAttributes */
$parameterAttributes = [];

foreach ($classReflection->getMethods() as $methodReflection) {
foreach ($methodReflection->getAttributes() as $attribute) {
if (self::isAttributeIgnored($attribute)) {
continue;
}

$method = $methodReflection->name;

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

$methodAttributes[] = new TransientTargetMethod(
$attribute->getName(),
$attribute->getArguments(),
$method,
);
}
$this->collectMethodAndParameterAttributes(
$class,
$methodReflection,
$methodAttributes,
$parameterAttributes,
);
}

$propertyAttributes = [];
Expand All @@ -93,7 +89,7 @@ public function collectAttributes(string $class): array
}
}

return [ $classAttributes, $methodAttributes, $propertyAttributes ];
return [ $classAttributes, $methodAttributes, $propertyAttributes, $parameterAttributes ];
}

/**
Expand All @@ -119,8 +115,75 @@ private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
{
static $ignored = [
\ReturnTypeWillChange::class => true,
\SensitiveParameter::class => true,
];

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

/**
* @param array<TransientTargetMethod> $methodAttributes
* @param array<TransientTargetParameter> $parameterAttributes
*/
private function collectMethodAndParameterAttributes(
string $class,
\ReflectionMethod $methodReflection,
array &$methodAttributes,
array &$parameterAttributes,
): void {
foreach ($methodReflection->getAttributes() as $attribute) {
if (self::isAttributeIgnored($attribute)) {
continue;
}

$method = $methodReflection->name;

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

$methodAttributes[] = new TransientTargetMethod(
$attribute->getName(),
$attribute->getArguments(),
$method,
);
}

$parameterAttributes = array_merge(
$parameterAttributes,
$this->collectParameterAttributes($methodReflection),
);
}

/**
* @return array<TransientTargetParameter>
*/
private function collectParameterAttributes(\ReflectionMethod $reflectionFunctionAbstract): array
{
$targets = [];
$class = $reflectionFunctionAbstract->class;
$method = $reflectionFunctionAbstract->name;

foreach ($reflectionFunctionAbstract->getParameters() as $parameter) {
/** @var non-empty-string $name */
$name = $parameter->name;

$paramLabel = $class . '::' . $method . '(' . $name . ')';

foreach ($parameter->getAttributes() as $attribute) {
if (self::isAttributeIgnored($attribute)) {
continue;
}

$this->log->debug("Found attribute {$attribute->getName()} on $paramLabel");

$targets[] = new TransientTargetParameter(
$attribute->getName(),
$attribute->getArguments(),
$method,
$name
);
}
}

return $targets;
}
}
74 changes: 74 additions & 0 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ final class Collection
* @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.
* @param array<class-string, array<array{ mixed[], class-string, non-empty-string, non-empty-string }>> $targetParameters
* Where _key_ is an attribute class and _value_ an array of arrays where 0 are the
* attribute arguments, 1 is a target class, 2 is the target method, and 3 is the target parameter.
*/
public function __construct(
private array $targetClasses,
private array $targetMethods,
private array $targetProperties,
private array $targetParameters,
) {
}

Expand Down Expand Up @@ -109,6 +113,50 @@ private static function createMethodAttribute(
}
}

/**
* @template T of object
*
* @param class-string<T> $attribute
*
* @return array<TargetParameter<T>>
*/
public function findTargetParameters(string $attribute): array
{
return array_map(
fn(array $t) => self::createParameterAttribute($attribute, ...$t),
$this->targetParameters[$attribute] ?? [],
);
}

/**
* @template T of object
*
* @param class-string<T> $attribute
* @param array<mixed> $arguments
* @param class-string $class
* @param non-empty-string $method
* @param non-empty-string $parameter
*
* @return TargetParameter<T>
*/
private static function createParameterAttribute(
string $attribute,
array $arguments,
string $class,
string $method,
string $parameter,
): object {
try {
$a = new $attribute(...$arguments);
return new TargetParameter($a, $class, $method, $parameter);
} catch (Throwable $e) {
throw new RuntimeException(
"An error occurred while instantiating attribute $attribute on parameter $class::$method($parameter)",
previous: $e,
);
}
}

/**
* @template T of object
*
Expand Down Expand Up @@ -196,6 +244,32 @@ public function filterTargetMethods(callable $predicate): array
return $ar;
}

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

foreach ($this->targetParameters as $attribute => $references) {
foreach ($references as [$arguments, $class, $method, $parameter]) {
if ($predicate($attribute, $class, $method, $parameter)) {
$ar[] = self::createParameterAttribute(
$attribute,
$arguments,
$class,
$method,
$parameter,
);
}
}
}

return $ar;
}

/**
* @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate
*
Expand Down
21 changes: 15 additions & 6 deletions src/MemoizeAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ class MemoizeAttributeCollector
* array<TransientTargetClass>,
* array<TransientTargetMethod>,
* array<TransientTargetProperty>,
* array<TransientTargetParameter>,
* }>
* Where _key_ is a class and _value is an array where:
* Where _key_ is a class and _value_ is an array where:
* - `0` is a timestamp
* - `1` is an array of class attributes
* - `2` is an array of method attributes
* - `3` is an array of property attributes
*/
private array $state;

Expand Down Expand Up @@ -58,7 +56,8 @@ public function collectAttributes(array $classMap): TransientCollection
$classAttributes,
$methodAttributes,
$propertyAttributes,
] = $this->state[$class] ?? [ 0, [], [], [] ];
$parameterAttributes,
] = $this->state[$class] ?? [ 0, [], [], [], [] ];

$mtime = filemtime($filepath);

Expand All @@ -75,14 +74,21 @@ public function collectAttributes(array $classMap): TransientCollection
$classAttributes,
$methodAttributes,
$propertyAttributes,
$parameterAttributes,
] = $classAttributeCollector->collectAttributes($class);
} catch (Throwable $e) {
$this->log->error(
"Attribute collection failed for $class: {$e->getMessage()}",
);
}

$this->state[$class] = [ time(), $classAttributes, $methodAttributes, $propertyAttributes ];
$this->state[$class] = [
time(),
$classAttributes,
$methodAttributes,
$propertyAttributes,
$parameterAttributes,
];
}

if (count($classAttributes)) {
Expand All @@ -91,6 +97,9 @@ public function collectAttributes(array $classMap): TransientCollection
if (count($methodAttributes)) {
$collector->addMethodAttributes($class, $methodAttributes);
}
if (count($parameterAttributes)) {
$collector->addParameterAttributes($class, $parameterAttributes);
}
if (count($propertyAttributes)) {
$collector->addTargetProperties($class, $propertyAttributes);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class Plugin implements PluginInterface, EventSubscriberInterface
{
public const CACHE_DIR = '.composer-attribute-collector';
public const VERSION_MAJOR = 2;
public const VERSION_MINOR = 0;
public const VERSION_MINOR = 1;

/**
* @uses onPostAutoloadDump
Expand Down
Loading