Note
This is a fork of olvlvl/composer-attribute-collector.
All the credit for the original design and implementation goes to Olivier Laviale and
the contributors of the upstream project—this fork carries a handful of opinionated changes on
top of their work. It is distributed as ekwi-tech/composer-attribute-collector, in the
Ekwi\ComposerAttributeCollector namespace, under the same
BSD-3-Clause license.
The fork has diverged for good: it no longer tracks upstream, and its public API is not compatible. Read Differences from upstream before switching.
composer-attribute-collector is a Composer plugin designed to effectively discover PHP 8 attribute targets, and later retrieve them at near zero cost, without runtime reflection. After the autoloader dump, it collects attributes and generates a static file for fast access. This provides a convenient way to discover attribute-backed classes, methods, or properties—ideal for codebase analysis. (For known targets, traditional reflection remains an option.)
A handful of deliberate changes; everything else—configuration, caching, the generated file, the
Attributes facade—behaves like upstream.
Upstream collects every attribute it finds. Here an attribute is collected only if its own class is
marked with #[CollectableAttribute], which keeps the generated file small and focused on the
attributes you actually query. See Mark collectable attributes.
Upstream var_exports the arguments of an attribute into the generated file so that $target->attribute
can be a ready-made instance. That breaks on any argument PHP cannot render as code—an object
without __set_state(), typically.
This fork records names only. When you need the arguments, getAttribute() instantiates the
attribute on demand by reflecting on the target it describes, and reuses the instance afterwards.
Finding targets stays reflection-free.
The properties of TargetClass, TargetMethod, TargetProperty, and TargetParameter are
private, and read through getAttributeClass(), getName(), getClass(), and getMethod().
Upstream exposes public properties, and its attribute property holds an instance where
getAttributeClass() holds a class-string.
Upstream supports PHP 8.0 and up; this fork requires PHP 8.4.
The package is ekwi-tech/composer-attribute-collector and its classes live in
Ekwi\ComposerAttributeCollector. Coming from upstream, rewrite your imports:
-use olvlvl\ComposerAttributeCollector\Attributes;
-use olvlvl\ComposerAttributeCollector\CollectableAttribute;
+use Ekwi\ComposerAttributeCollector\Attributes;
+use Ekwi\ComposerAttributeCollector\CollectableAttribute;The generated "attributes" file names those classes too, but it is rewritten on every
composer dump-autoload, so there is nothing to migrate by hand.
Important
The fork does not replace the upstream package: Composer will happily install both, and both
plugins would then write vendor/attributes.php, each undoing the other. Require one or the
other, never the two together.
- Almost zero configuration: mark the attributes you want collected, and you're done
- No reflection when finding targets
- Might improve performance
- No dependency (except Composer of course)
- A single interface to get attribute targets: classes, methods, properties, and parameters
- Only names are collected, so no attribute argument can break the generated file
- Attributes are still available, instantiated on demand with
getAttribute() - Can cache discoveries to speed up consecutive runs.
Note
Currently, the plugin supports class, method, property, and parameter targets. You're welcome to contribute if you're interested in expending its support.
Warning
Attributes used on functions are ignored at this time.
The following example demonstrates how targets and their attributes can be retrieved:
<?php
use Ekwi\ComposerAttributeCollector\Attributes;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\Mapping\Column;
require_once 'vendor/autoload.php';
require_once 'vendor/attributes.php'; // <-- the file created by the plugin
// Find the target classes of the AsMessageHandler attribute.
foreach (Attributes::findTargetClasses(AsMessageHandler::class) as $target) {
// getAttributeClass() is the name of the attribute class,
// getName() the name of the target class, and
// getAttribute() an instance of the attribute, created on demand.
var_dump($target->getAttributeClass(), $target->getName(), $target->getAttribute());
}
// Find the target methods of the Route attribute.
foreach (Attributes::findTargetMethods(Route::class) as $target) {
var_dump($target->getAttributeClass(), $target->getClass(), $target->getName());
}
// Find the target properties of the Column attribute.
foreach (Attributes::findTargetProperties(Column::class) as $target) {
var_dump($target->getAttributeClass(), $target->getClass(), $target->getName());
}
// Find the target method parameters of the UserInput attribute.
foreach (Attributes::findTargetParameters(UserInput::class) as $target) {
var_dump(
$target->getAttributeClass(),
$target->getClass(),
$target->getMethod(),
$target->getName(),
);
}
// Filter target methods using a predicate.
// You can also filter target classes and properties.
$predicate = fn($attribute) => is_a($attribute, Route::class, true);
# or
$predicate = Attributes::predicateForAttributeInstanceOf(Route::class);
foreach (Attributes::filterTargetMethods($predicate) as $target) {
var_dump($target->getAttributeClass(), $target->getClass(), $target->getName());
}
// Find class, method, and property attribute names for the ArticleController class.
$attributes = Attributes::forClass(ArticleController::class);
var_dump($attributes->classAttributes);
var_dump($attributes->methodsAttributes);
var_dump($attributes->propertyAttributes);Important
The plugin collects names, not attribute instances: the generated file records which classes, methods, properties, and parameters an attribute is used on, and nothing else. Attribute arguments are not collected, because they can hold arbitrary values—objects in particular—that cannot be rendered as PHP code in the generated file.
If you need the arguments of an attribute, getAttribute() instantiates it on demand, using
reflection on the target—the instance is created on first use, then reused:
foreach (Attributes::findTargetClasses(AsMessageHandler::class) as $target) {
$attribute = $target->getAttribute(); // an AsMessageHandler instance
var_dump($attribute->fromTransport);
}Reflection only happens when getAttribute() is called; finding targets remains reflection-free.
If an attribute is repeatable, getAttribute() returns the first one found on the target.
Here are a few steps to get you started.
The package is not published on packagist.org: it is resolved from the Git tags of this
repository. Declare the repository, then require the package with Composer. You will be asked
if you trust the plugin and wish to activate it, select y to proceed.
composer config repositories.composer-attribute-collector vcs https://github.com/ekwi-tech/composer-attribute-collector
composer require ekwi-tech/composer-attribute-collectorYou should see log messages similar to this:
Generating autoload files
Generating attributes file
Generated attributes file in 9.137 ms
Generated autoload files
Tip
See the Frequently Asked Questions section to automatically refresh the "attributes" file during development.
For an attribute to be collected, it must be marked with the #[CollectableAttribute] attribute.
This opt-in strategy ensures that only the attributes you're interested in are collected,
improving performance and reducing noise.
<?php
namespace App\Attribute;
use Attribute;
use Ekwi\ComposerAttributeCollector\CollectableAttribute;
#[CollectableAttribute]
#[Attribute(Attribute::TARGET_CLASS)]
final class MyAttribute
{
}The collector automatically scans autoload paths of the root composer.json for a
zero-configuration experience. You can override them via
extra.composer-attribute-collector.include.
{
"extra": {
"composer-attribute-collector": {
"include": [
"src"
]
}
}
}Check the Configuration options for more details.
You can require the "attributes" file using require_once 'vendor/attributes.php'; but you might
prefer to use Composer's autoloading feature:
{
"autoload": {
"files": [
"vendor/attributes.php"
]
}
}Here are a few ways you can configure the plugin.
Including paths or files (root-only)
The collector automatically scans autoload paths of the root composer.json, but you can override
them via the include property.
The specified paths are relative to the composer.json file, and the {vendor} placeholder is
replaced with the path to the vendor folder.
{
"extra": {
"composer-attribute-collector": {
"include": [
"path-or-file/to/include"
]
}
}
}Excluding paths or files (root-only)
Use the exclude property to exclude paths or files from scanning. This is handy when files
cause issues or have side effects.
The specified paths are relative to the composer.json file, and the {vendor} placeholder is
replaced with the path to the vendor folder.
{
"extra": {
"composer-attribute-collector": {
"exclude": [
"path-or-file/to/exclude"
]
}
}
}The plugin is able to maintain a cache to reuse discoveries between runs. To enable the cache,
set the environment variable COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE to 1, yes, or true.
Cache items are persisted in the .composer-attribute-collector directory, you might want to add
it to your .gitignore file.
COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=1 composer dump-autoloadUse cases are available to test the plugin in real conditions:
-
Incompatible signature The plugin is able to collect attributes, although the PSR Logger version used by Composer and the application are incompatible.
-
Symfony A Symfony application, created with
symfony new. -
Laravel A Laravel application, created with
laravel new.
Warning
The Symfony and Laravel cases still require olvlvl/composer-attribute-collector and Composer
resolves it from packagist.org rather than from the local path repository, so those two
currently exercise upstream, not this fork. They need to be rewired: requiring the fork by
its own name, and using attributes of their own that can be marked #[CollectableAttribute].
Do I need to generate an optimized autoloader?
You don't need to generate an optimized autoloader for this to work. The plugin uses code similar to Composer to find classes. Anything that works with Composer should work with the plugin.
Can I use the plugin during development?
Yes, you can use the plugin during development, but keep in mind the "attributes" file is only
generated after the autoloader is dumped. If you modify attributes you will have to run
composer dump-autoload to refresh the "attributes" file.
As a workaround you could have watchers on the directories that contain classes with attributes to
run XDEBUG_MODE=off composer dump-autoload when you make changes. PhpStorm offers file
watchers. You could also use spatie/file-system-watcher, it only requires
PHP. If the plugin is too slow for your liking, try running the command with
COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=yes, it will enable caching and speed up consecutive runs.
How do I include a class that inherits its attributes?
To speed up the collection process, the plugin first looks at PHP files as plain text for hints of
attribute usage. If a class inherits its attributes from traits, properties, or methods, but doesn't
use attributes itself, it will be ignored. Use the attribute
#[Ekwi\ComposerAttributeCollector\InheritsAttributes] to force the collection.
Note that the attributes you want to collect must still be marked with #[CollectableAttribute].
trait UrlTrait
{
#[UrlGetter]
public function get_url(): string
{
return '/url';
}
}
#[InheritsAttributes]
class InheritedAttributeSample
{
use UrlTrait;
}The project is continuously tested by GitHub actions.
This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.
See CONTRIBUTING for details. Issues and pull requests belong to this repository; please don't open them against upstream for changes that are specific to the fork.
This project is a fork of olvlvl/composer-attribute-collector by Olivier Laviale. The design, the implementation, and most of the code and documentation you are reading are theirs, and the BSD-3-Clause license and copyright of the original work are unchanged. Thank you for the plugin, and for making it free software.