Skip to content

Commit

Permalink
Generate stub file during activation
Browse files Browse the repository at this point in the history
Composer fails to generate the autoloader if "vendor/attributes.php" is in the autoloading but is not available yet. As a workaround, a stub file is created when the plugin is activated.
  • Loading branch information
olvlvl committed Sep 1, 2023
1 parent b60c319 commit fde20ed
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 232 deletions.
230 changes: 8 additions & 222 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ foreach (Attributes::findTargetProperties(Column::class) as $target) {
}

// 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) {
// 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->attribute, $target->class, $target->name);
}

Expand Down Expand Up @@ -252,9 +254,8 @@ use Symfony\Component\Routing\Annotation\Route;

require_once 'vendor/autoload.php';

$targets = Attributes::filterTargetMethods(
Attributes::predicateForAttributeInstanceOf(Route::class)
);
$predicate = Attributes::predicateForAttributeInstanceOf(Route::class);
$targets = Attributes::filterTargetMethods($predicate);

foreach ($targets as $target) {
echo "action: $target->class#$target->name, path: {$target->attribute->getPath()}\n";
Expand All @@ -276,221 +277,6 @@ The demo application configured with the plugin is [available on GitHub](https:/



## Use cases

### Get attributes without using reflection

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.

```php
// Find attributes for the ArticleController class.
$attributes = Attributes::forClass(ArticleController::class);

var_dump($attributes->classAttributes);
var_dump($attributes->methodsAttributes);
var_dump($attributes->propertyAttributes);
```



### A simpler way to configure your Dependency Injection Container

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](https://github.com/ICanBoogie/MessageBus#using-php-8-attributes-instead-of-yaml).

```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
<?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
{
// ...
}
```



### Configure components from attributes

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

```php
/** @var TargetMethod<Route>[] $target_methods */
$target_methods = Attributes::filterTargetMethods(
Attributes::predicateForAttributeInstanceOf(Route::class)
);
```

Now then, configuring the router looks as simple as this:

```php
<?php

use ICanBoogie\Binding\Routing\ConfigBuilder;

/* @var ConfigBuilder $config */

$config->from_attributes();
```



## Using Attributes

### Filtering target methods

`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
<?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)
);
```



----------


Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@
"extra": {
"class": "olvlvl\\ComposerAttributeCollector\\Plugin",
"composer-attribute-collector": {
"ignore-paths": [
"IncompatibleSignature"
"include": [
"tests"
],
"exclude": [
"tests/Acme/PSR4/IncompatibleSignature.php"
]
}
}
Expand Down
22 changes: 16 additions & 6 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace olvlvl\ComposerAttributeCollector;

use Composer\Composer;
use Composer\Factory;
use Composer\PartialComposer;
use Composer\Util\Platform;
Expand Down Expand Up @@ -39,12 +40,7 @@ final class Config

public static function from(PartialComposer $composer): self
{
$vendorDir = $composer->getConfig()->get('vendor-dir');

if (!is_string($vendorDir) || !$vendorDir) {
throw new RuntimeException("Unable to determine vendor directory");
}

$vendorDir = self::resolveVendorDir($composer);
$composerFile = Factory::getComposerFile();
$rootDir = realpath(dirname($composerFile));

Expand All @@ -71,6 +67,20 @@ public static function from(PartialComposer $composer): self
);
}

/**
* @return non-empty-string
*/
public static function resolveVendorDir(PartialComposer $composer): string
{
$vendorDir = $composer->getConfig()->get('vendor-dir');

if (!is_string($vendorDir) || !$vendorDir) {
throw new RuntimeException("Unable to determine vendor directory");
}

return $vendorDir;
}

/**
* @readonly
* @var non-empty-string|null
Expand Down
4 changes: 2 additions & 2 deletions src/MemoizeClassMapFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public function filter(array $classMap, Closure $filter): array
if ($timestamp < $mtime) {
if ($timestamp) {
$diff = $mtime - $timestamp;
$this->io->debug("Refresh filtered files in '$pathname' ($diff sec ago)");
$this->io->debug("Refresh filtered file '$pathname' ($diff sec ago)");
} else {
$this->io->debug("Filter files in '$pathname'");
$this->io->debug("Filter '$pathname'");
}

$keep = $filter($class, $pathname);
Expand Down
20 changes: 20 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
use olvlvl\ComposerAttributeCollector\Filter\ContentFilter;
use olvlvl\ComposerAttributeCollector\Filter\InterfaceFilter;

Check failure on line 14 in src/Plugin.php

View workflow job for this annotation

GitHub Actions / phpcs

Header blocks must not contain blank lines

use RuntimeException;

use function file_exists;
use function file_put_contents;
use function is_string;
use function microtime;
use function sprintf;

Expand Down Expand Up @@ -45,6 +49,22 @@ public static function getSubscribedEvents(): array
*/
public function activate(Composer $composer, IOInterface $io): void
{
$vendorDir = Config::resolveVendorDir($composer);
$filename = $vendorDir . DIRECTORY_SEPARATOR . "attributes.php";

if (file_exists($filename)) {
return;
}

$stub = <<<PHP
<?php
// attributes.php @generated by https://github.com/olvlvl/composer-attribute-collector
// This is a placeholder to enable the rendering of the autoloader.
PHP;

file_put_contents($filename, $stub);
}

/**
Expand Down

0 comments on commit fde20ed

Please sign in to comment.