Stars: 111
Forks: 3
Pull Requests: 13
Issues: 10
Watchers: 4
Last Updated: 2023-09-17 17:53:23
A convenient and near zero-cost way to retrieve targets of PHP 8 attributes
License: Other
Languages: PHP, Dockerfile, Makefile
composer-attribute-collector is a plugin for Composer. Its ambition is to provide a convenient and near zero-cost way to retrieve targets of PHP 8 attributes. In order to do that, after the autoloader has been dumped, the plugin collects attribute targets and generates a static file. Later, these targets can be retrieved through a convenient interface.
The following example demonstrates how targets and their attributes can be retrieved:
<?php
use olvlvl\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) {
// $target->attribute is an instance of the specified attribute
// with the actual data.
var_dump($target->attribute, $target->name);
}
// Find the target methods of the Route attribute.
foreach (Attributes::findTargetMethods(Route::class) as $target) {
var_dump($target->attribute, $target->class, $target->name);
}
// Find the target properties of the Column attribute.
foreach (Attributes::findTargetProperties(Column::class) as $target) {
var_dump($target->attribute, $target->class, $target->name);
}
// Filter target methods using a predicate.
// This is also available for classes and properties.
foreach (Attributes::filterTargetMethods(
fn($attribute) => is_a($attribute, Route::class, true)
) as $target) {
var_dump($target->attribute, $target->class, $target->name);
}
// Find class, method, and property attributes for the ArticleController class.
$attributes = Attributes::forClass(ArticleController::class);
var_dump($attributes->classAttributes);
var_dump($attributes->methodsAttributes);
var_dump($attributes->propertyAttributes);
composer require olvlvl/composer-attribute-collector
The plugin is currently experimental and its interface subject to change. Also, it only supports class and method targets. Please contribute if you're interested in shaping its future.
Note: The plugin creates a .composer-attribute-collector
directory to store caches, you might
want to add it to your .gitignore
file.
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'll have to run
composer dump
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
when you make changes. PhpStorm offers file watchers. You could also use spatie/file-system-watcher, it only requires PHP.
You can require the attributes file as shown in the usage example, but it's preferable to use Composer's autoloading feature:
{
"autoloading": {
"files": [
"vendor/attributes.php"
]
}
}
composer-attribute-collector inspects files that participate in the autoload process. This can cause
issues with files that have side effects. For instance, symfony/cache
is known to cause issues, so
we're excluding paths matching {vendor}/symfony/cache/Traits
from inspection. Additional paths can
be specified using the extra
section of composer.json
. 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": {
"ignore-paths": [
"path/to/ignore"
]
}
}
}
For instance, if you are only concerned about the attributes of your own project, you could ignore the vendor directory:
{
"extra": {
"composer-attribute-collector": {
"ignore-paths": [
"vendor"
]
}
}
}
You can try the plugin with a fresh installation of Symfony.
Use the symfony
command to create a new project. If you don't have it yet, you can download it.
symfony new --webapp my_project
Now get into that project and install the plugin. You'll be asked if you trust the plugin and wish
to activate it. If you wish to continue, choose y
.
cd my_project
composer require olvlvl/composer-attribute-collector
The plugin should have generated the file vendor/attributes.php
. It should look something like
this excerpt:
<?php
// attributes.php @generated by https://github.com/olvlvl/composer-attribute-collector
namespace olvlvl\ComposerAttributeCollector;
Attributes::with(fn () => new Collection(
targetClasses: [
\Symfony\Component\Console\Attribute\AsCommand::class => [
[ ['lint:yaml', 'Lint a YAML file and outputs encountered errors'], \Symfony\Component\Yaml\Command\LintCommand::class ],
[ ['server:dump', 'Start a dump server that collects and displays dumps in a single place'], \Symfony\Component\VarDumper\Command\ServerDumpCommand::class ],
[ ['debug:validator', 'Display validation constraints for classes'], \Symfony\Component\Validator\Command\DebugCommand::class ],
[ ['translation:pull', 'Pull translations from a given provider.'], \Symfony\Component\Translation\Command\TranslationPullCommand::class ],
We also have a repository to test the Symfony usecase.
The method forClass()
returns the attributes attached to a class, without using reflection. This
can improve the performance of your application if it relies on reflection on hot paths.
// Find attributes for the ArticleController class.
$attributes = Attributes::forClass(ArticleController::class);
var_dump($attributes->classAttributes);
var_dump($attributes->methodsAttributes);
var_dump($attributes->propertyAttributes);
composer-attribute-collector can help simplify DIC (Dependency Injection Container) configuration. Long error-prone YAML can be completely replaced with attributes and a compiler pass to use them. You can still support both YAML and attributes, the "attribute" compiler pass would just configure the services and tag them automatically.
For example, the package ICanBoogie/MessageBus offers PHP 8 attributes as an alternative to YAML.
services:
Acme\MenuService\Application\MessageBus\CreateMenuHandler:
tags:
- name: message_bus.handler
message: Acme\MenuService\Application\MessageBus\CreateMenu
- name: message_bus.permission
permission: is_admin
- name: message_bus.permission
permission: can_write_menu
Acme\MenuService\Application\MessageBus\DeleteMenuHandler:
tags:
- name: message_bus.handler
message: Acme\MenuService\Application\MessageBus\DeleteMenu
- name: message_bus.permission
permission: is_admin
- name: message_bus.permission
permission: can_manage_menu
Acme\MenuService\Presentation\Security\Voters\IsAdmin:
tags:
- name: message_bus.voter
permission: is_admin
Acme\MenuService\Presentation\Security\Voters\CanWriteMenu:
tags:
- name: message_bus.voter
permission: can_write_menu
Acme\MenuService\Presentation\Security\Voters\CanManageMenu:
tags:
- name: message_bus.voter
permission: can_manage_menu
<?php
// ...
final class Permissions
{
public const IS_ADMIN = 'is_admin';
public const CAN_WRITE_MENU = 'can_write_menu';
public const CAN_MANAGE_MENU = 'can_manage_menu';
}
// ...
use ICanBoogie\MessageBus\Attribute\Permission;
#[Permission(Permissions::IS_ADMIN)]
#[Permission(Permissions::CAN_WRITE_MENU)]
final class CreateMenu
{
public function __construct(
public readonly array $payload
)// ...
}
// ...
use ICanBoogie\MessageBus\Attribute\Handler;
#[Handler]
final class CreateMenuHandler
{
public function __invoke(CreateMenu $message)// ...
}
// ...
use ICanBoogie\MessageBus\Attribute\Vote;
#[Vote(Permissions::IS_ADMIN)]
final class IsAdmin implements Voter
{
// ...
}
Using attributes simplifies configuration, placing definition closer to the code, where it's used. ICanBoogie's router can be configured automatically from attributes. The following example demonstrates how the Route
attribute can be used at the class level to define a prefix for the route attributes such as Get
that are used to tag actions. Action identifiers can be inferred from the controller class and the method names e.g. skills:list
.
<?php
// …
#[Route('/skills')]
final class SkillController extends ControllerAbstract
{
#[Post]
private function create(): void
{
// …
}
#[Get('.html')]
private function list(): void
{
// …
}
#[Get('/summonable.html')]
private function summonable(): void
{
// …
}
#[Get('/learnable.html')]
private function learnable(): void
{
// …
}
#[Get('/:slug.html')]
private function show(string $slug): void
{
// …
}
}
Because the Get
and Post
attributes extend Route
, all action methods can be retrieved with the filterTargetMethods()
method.
/** @var TargetMethod<Route>[] $target_methods */
$target_methods = Attributes::filterTargetMethods(
Attributes::predicateForAttributeInstanceOf(Route::class)
);
Now then, configuring the router looks as simple as this:
<?php
use ICanBoogie\Binding\Routing\ConfigBuilder;
/* @var ConfigBuilder $config */
$config->from_attributes();
filterTargetMethods()
can filter target methods using a predicate. This can be helpful when a number of attributes extend another one, and you are interested in collecting any instance of that attribute. The filerTargetClasses()
and filterTargetProperties()
methods provide similar feature for classes and properties.
Let's say we have a Route
attribute extended by Get
, Post
, Put
…
<?php
use olvlvl\ComposerAttributeCollector\Attributes;
/** @var TargetMethod<Route>[] $target_methods */
$target_methods = [
...Attributes::findTargetMethods(Get::class),
...Attributes::findTargetMethods(Head::class),
...Attributes::findTargetMethods(Post::class),
...Attributes::findTargetMethods(Put::class),
...Attributes::findTargetMethods(Delete::class),
...Attributes::findTargetMethods(Connect::class),
...Attributes::findTargetMethods(Options::class),
...Attributes::findTargetMethods(Trace::class),
...Attributes::findTargetMethods(Patch::class),
...Attributes::findTargetMethods(Route::class),
];
// Can be replaced by:
/** @var TargetMethod<Route>[] $target_methods */
$target_methods = Attributes::filterTargetMethods(
Attributes::predicateForAttributeInstanceOf(Route::class)
);
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 are expected to uphold this code.
Please see CONTRIBUTING for details.
olvlvl/composer-attribute-collector is released under the BSD-3-Clause.