Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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::findTargetMethodParameters(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
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 TargetMethodParameter<T>[]
*/
public static function findTargetMethodParameters(string $attribute): array
{
return self::getCollection()->findTargetMethodParameters($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<TargetMethodParameter<object>>
*/
public static function filterTargetMethodParameters(callable $predicate): array
{
return self::getCollection()->filterTargetMethodParameters($predicate);
}

/**
* @param class-string $class
*
Expand Down
12 changes: 10 additions & 2 deletions src/ClassAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function __construct(IOInterface $io, Parser $parser)
* array<TransientTargetClass>,
* array<TransientTargetMethod>,
* array<TransientTargetProperty>,
* array<non-empty-string, array<TransientTargetMethodParameter>>,
* }
*
* @throws ReflectionException
Expand All @@ -54,7 +55,7 @@ public function collectAttributes(string $class): array
$classReflection = new ReflectionClass($class);

if ($this->isAttribute($classReflection)) {
return [ [], [], [] ];
return [ [], [], [], [] ];
}

$classAttributes = [];
Expand All @@ -74,7 +75,9 @@ public function collectAttributes(string $class): array
}

$methodAttributes = [];
$methodParameterAttributes = [];

$parameterAttributeCollector = new ParameterAttributeCollector($this->io, $this->parser);
foreach ($classReflection->getMethods() as $methodReflection) {
foreach ($this->getMethodAttributes($methodReflection) as $attribute) {
if (self::isAttributeIgnored($attribute->getName())) {
Expand All @@ -91,6 +94,11 @@ public function collectAttributes(string $class): array
$method,
);
}

$parameterAttributes = $parameterAttributeCollector->collectAttributes($methodReflection);
if ($parameterAttributes !== []) {
$methodParameterAttributes[$methodReflection->getName()] = $parameterAttributes;
}
}

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

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

/**
Expand Down
76 changes: 75 additions & 1 deletion src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class Collection
* @var array<class-string, array<array{mixed[], class-string, non-empty-string}>>
*/
private array $targetProperties;
/**
* @var array<class-string, array<array{mixed[], class-string, non-empty-string, non-empty-string}>>
*/
private array $targetMethodParameters;
/**
* @param array<class-string, array<array{ mixed[], class-string }>> $targetClasses
* Where _key_ is an attribute class and _value_ an array of arrays
Expand All @@ -34,12 +38,16 @@ 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 }>> $targetMethodParameters
* 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(array $targetClasses, array $targetMethods, array $targetProperties)
public function __construct(array $targetClasses, array $targetMethods, array $targetProperties, array $targetMethodParameters)
{
$this->targetClasses = $targetClasses;
$this->targetMethods = $targetMethods;
$this->targetProperties = $targetProperties;
$this->targetMethodParameters = $targetMethodParameters;
}
/**
* @template T of object
Expand Down Expand Up @@ -118,6 +126,46 @@ private static function createMethodAttribute(string $attribute, array $argument
}
}

/**
* @template T of object
*
* @param class-string<T> $attribute
*
* @return array<TargetMethodParameter<T>>
*/
public function findTargetMethodParameters(string $attribute): array
{
return array_map(
fn(array $t) => self::createMethodParameterAttribute($attribute, ...$t),
$this->targetMethodParameters[$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 TargetMethodParameter<T>
*/
private static function createMethodParameterAttribute(string $attribute, array $arguments, string $class, string $method, string $parameter): object
{
try {
$a = new $attribute(...$arguments);
return new TargetMethodParameter($a, $class, $method, $parameter);
} catch (Throwable $e) {
throw new RuntimeException(
"An error occurred while instantiating attribute $attribute on method $class::$method($parameter)",
0,
$e,
);
}
}

/**
* @template T of object
*
Expand Down Expand Up @@ -202,6 +250,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<TargetMethodParameter<object>>
*/
public function filterTargetMethodParameters(callable $predicate): array
{
$ar = [];

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

return $ar;
}

/**
* @param callable(class-string $attribute, class-string $class, non-empty-string $property):bool $predicate
*
Expand Down
11 changes: 9 additions & 2 deletions src/MemoizeAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ class MemoizeAttributeCollector
* array<TransientTargetClass>,
* array<TransientTargetMethod>,
* array<TransientTargetProperty>,
* array<non-empty-string, array<TransientTargetMethodParameter>>,
* }>
* 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
* - `4` is an array of arrays. _key_ is a method name and _value_ parameter attributes
*/
private array $state;

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

$mtime = filemtime($filepath);

Expand All @@ -79,14 +82,15 @@ public function collectAttributes(array $classMap): TransientCollection
$classAttributes,
$methodAttributes,
$propertyAttributes,
$methodParameterAttributes,
] = $classAttributeCollector->collectAttributes($class);
} catch (Throwable $e) {
$this->io->error(
"Attribute collection failed for $class: {$e->getMessage()}",
);
}

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

if (count($classAttributes)) {
Expand All @@ -95,6 +99,9 @@ public function collectAttributes(array $classMap): TransientCollection
if (count($methodAttributes)) {
$collector->addMethodAttributes($class, $methodAttributes);
}
if (count($methodParameterAttributes)) {
$collector->addMethodParameterAttributes($class, $methodParameterAttributes);
}
if (count($propertyAttributes)) {
$collector->addTargetProperties($class, $propertyAttributes);
}
Expand Down
70 changes: 70 additions & 0 deletions src/ParameterAttributeCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace olvlvl\ComposerAttributeCollector;

Check failure on line 3 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Header blocks must be separated by a single blank line


use Composer\IO\IOInterface;
use PhpParser\Parser;

/**
* @internal
*/
class ParameterAttributeCollector
{
private IOInterface $io;
private Parser $parser;

Check failure on line 15 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpstan

Property olvlvl\ComposerAttributeCollector\ParameterAttributeCollector::$parser is never read, only written.

public function __construct(IOInterface $io, Parser $parser)
{
$this->io = $io;
$this->parser = $parser;
}

/**
* @return array<TransientTargetMethodParameter>
*/
public function collectAttributes(\ReflectionFunctionAbstract $reflectionFunctionAbstract): array
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have build this collector on ReflectionFunctionAbstract, so we can easily add support for function parameters

{
$funcParameterAttributes = [];
foreach($reflectionFunctionAbstract->getParameters() as $parameter) {

Check failure on line 29 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 1 space after FOREACH keyword; 0 found
$attributes = $this->getParameterAttributes($parameter);
$functionName = $reflectionFunctionAbstract->name;
$parameterName = $parameter->name;
assert($functionName !== '');
assert($parameterName !== '');

$paramLabel = '';
if ($reflectionFunctionAbstract instanceof \ReflectionMethod) {
$paramLabel = $reflectionFunctionAbstract->class .'::'.$functionName .'(' . $parameterName .')';

Check failure on line 38 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space after "."; 0 found

Check failure on line 38 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space before "."; 0 found

Check failure on line 38 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space after "."; 0 found

Check failure on line 38 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space after "."; 0 found

Check failure on line 38 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space after "."; 0 found
} elseif ($reflectionFunctionAbstract instanceof \ReflectionFunction) {
$paramLabel = $functionName . '(' . $parameterName .')';

Check failure on line 40 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space after "."; 0 found
}

foreach($attributes as $attribute) {

Check failure on line 43 in src/ParameterAttributeCollector.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 1 space after FOREACH keyword; 0 found
$this->io->debug("Found attribute {$attribute->getName()} on $paramLabel");

$funcParameterAttributes[] = new TransientTargetMethodParameter(
$attribute->getName(),
$attribute->getArguments(),
$functionName,
$parameterName
);
}
}

return $funcParameterAttributes;
}

/**
* @return \ReflectionAttribute<object>[]
*/
private function getParameterAttributes(\ReflectionParameter $parameterReflection): array
{
if (PHP_VERSION_ID >= 80000) {
return $parameterReflection->getAttributes();
}

// todo: implement PHPParser based inspection
throw new \LogicException();
}
}
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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;
Copy link
Author

@staabm staabm May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use new version to make sure a fresh attributes.php is generated and we don't need to invalide existing caches in tests.

attributes.php in consuming projects currently don't have a cache key or similar


/**
* @uses onPostAutoloadDump
Expand Down
44 changes: 44 additions & 0 deletions src/TargetMethodParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace olvlvl\ComposerAttributeCollector;

/**
* @readonly
*
* @template T of object
*/
final class TargetMethodParameter
{
/**
* @var T
*/
public object $attribute;
/**
* @var class-string
*/
public string $class;
/**
* @var non-empty-string
*/
public string $method;
/**
* @var non-empty-string
*/
public string $name;
/**
* @param T $attribute
* @param class-string $class
* The name of the target class.
* @param non-empty-string $name
* The name of the target parameter.
* @param non-empty-string $method
* The name of the target method.
*/
public function __construct(object $attribute, string $class, string $name, string $method)
{
$this->attribute = $attribute;
$this->class = $class;
$this->name = $name;
$this->method = $method;
}
}
Loading
Loading