Skip to content

Repository files navigation

composer-attribute-collector

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.)

Differences from upstream

A handful of deliberate changes; everything else—configuration, caching, the generated file, the Attributes facade—behaves like upstream.

1. Collection is opt-in

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.

2. Attribute arguments are never dumped

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.

3. Targets are accessor-based

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.

4. PHP >= 8.4

Upstream supports PHP 8.0 and up; this fork requires PHP 8.4.

5. New package name, new namespace

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.

Features

  • 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.

Usage

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.

Getting started

Here are a few steps to get you started.

1. Install the plugin

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-collector

You 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.

2. Mark collectable attributes

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
{
}

3. Configure the plugin (optional)

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.

4. Autoload the "attributes" file

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"
    ]
  }
}

Configuration

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"
      ]
    }
  }
}

Cache discoveries between runs

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-autoload

Use cases

Use 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].

Frequently Asked Questions

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;
}

Continuous Integration

The project is continuously tested by GitHub actions.

Cases Tests Static Analysis Code Style

Code of Conduct

This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.

Contributing

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.

Acknowledgements

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.

About

A convenient and near zero-cost way to retrieve targets of PHP 8 attributes

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages