Skip to content

Commit ff062a3

Browse files
committed
Generate stub file during activation
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.
1 parent b9d76a3 commit ff062a3

File tree

5 files changed

+64
-253
lines changed

5 files changed

+64
-253
lines changed

README.md

Lines changed: 24 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ foreach (Attributes::findTargetProperties(Column::class) as $target) {
5656
}
5757

5858
// Filter target methods using a predicate.
59-
// This is also available for classes and properties.
60-
foreach (Attributes::filterTargetMethods(
61-
fn($attribute) => is_a($attribute, Route::class, true)
62-
) as $target) {
59+
// You can also filter target classes and properties.
60+
$predicate = fn($attribute) => is_a($attribute, Route::class, true);
61+
# or
62+
$predicate = Attributes::predicateForAttributeInstanceOf(Route::class);
63+
64+
foreach (Attributes::filterTargetMethods($predicate) as $target) {
6365
var_dump($target->attribute, $target->class, $target->name);
6466
}
6567

@@ -178,36 +180,10 @@ it to your `.gitignore` file.
178180

179181

180182

181-
## Frequently Asked Questions
182-
183-
**Do I need to generate an optimized autoloader?**
184-
185-
You don't need to generate an optimized autoloader for this to work. The plugin uses code similar
186-
to Composer to find classes. Anything that works with Composer should work with the plugin.
187-
188-
**Can I use the plugin during development?**
189-
190-
Yes, you can use the plugin during development, but keep in mind the attributes file is only
191-
generated after the autoloader is dumped. If you modify attributes you'll have to run
192-
`composer dump` to refresh the attributes file.
193-
194-
As a workaround you could have watchers on the directories that contain classes with attributes to
195-
run `XDEBUG_MODE=off composer dump` when you make changes. [PhpStorm offers file watchers][phpstorm-watchers]. You could also use [spatie/file-system-watcher][], it only requires PHP. If the plugin is too slow for your liking,
196-
try running the command with `COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=yes`, it will enable caching
197-
and speed up consecutive runs.
198-
199-
200-
201183
## Test drive with the Symfony Demo
202184

203185
You can try the plugin with a fresh installation of the [Symfony Demo Application](https://github.com/symfony/demo).
204186

205-
After you followed the instruction to install the demo, get into the project's directory 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`.
206-
207-
```shell
208-
composer require olvlvl/composer-attribute-collector
209-
```
210-
211187
Add the `composer-attribute-collector` node to `extra` and the autoload item to the `composer.json` file:
212188

213189
```json
@@ -227,10 +203,11 @@ Add the `composer-attribute-collector` node to `extra` and the autoload item to
227203
}
228204
```
229205

230-
Now dump the autoload:
206+
Use Composer to install the plugin. You'll be asked if you trust the plugin and wish to activate it.
207+
If you wish to continue, choose `y`.
231208

232209
```shell
233-
composer dump
210+
composer require olvlvl/composer-attribute-collector
234211
```
235212

236213
You should see log messages similar to this:
@@ -252,9 +229,8 @@ use Symfony\Component\Routing\Annotation\Route;
252229

253230
require_once 'vendor/autoload.php';
254231

255-
$targets = Attributes::filterTargetMethods(
256-
Attributes::predicateForAttributeInstanceOf(Route::class)
257-
);
232+
$predicate = Attributes::predicateForAttributeInstanceOf(Route::class);
233+
$targets = Attributes::filterTargetMethods($predicate);
258234

259235
foreach ($targets as $target) {
260236
echo "action: $target->class#$target->name, path: {$target->attribute->getPath()}\n";
@@ -276,218 +252,24 @@ The demo application configured with the plugin is [available on GitHub](https:/
276252

277253

278254

279-
## Use cases
280-
281-
### Get attributes without using reflection
282-
283-
The method `forClass()` returns the attributes attached to a class, without using reflection. This
284-
can improve the performance of your application if it relies on reflection on hot paths.
285-
286-
```php
287-
// Find attributes for the ArticleController class.
288-
$attributes = Attributes::forClass(ArticleController::class);
289-
290-
var_dump($attributes->classAttributes);
291-
var_dump($attributes->methodsAttributes);
292-
var_dump($attributes->propertyAttributes);
293-
```
294-
295-
296-
297-
### A simpler way to configure your Dependency Injection Container
298-
299-
composer-attribute-collector can help simplify DIC (Dependency Injection Container) configuration.
300-
Long error-prone YAML can be completely replaced with attributes and a compiler pass to use them.
301-
You can still support both YAML and attributes, the "attribute" compiler pass would just configure
302-
the services and tag them automatically.
303-
304-
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).
305-
306-
```yaml
307-
services:
308-
Acme\MenuService\Application\MessageBus\CreateMenuHandler:
309-
tags:
310-
- name: message_bus.handler
311-
message: Acme\MenuService\Application\MessageBus\CreateMenu
312-
- name: message_bus.permission
313-
permission: is_admin
314-
- name: message_bus.permission
315-
permission: can_write_menu
316-
317-
Acme\MenuService\Application\MessageBus\DeleteMenuHandler:
318-
tags:
319-
- name: message_bus.handler
320-
message: Acme\MenuService\Application\MessageBus\DeleteMenu
321-
- name: message_bus.permission
322-
permission: is_admin
323-
- name: message_bus.permission
324-
permission: can_manage_menu
325-
326-
Acme\MenuService\Presentation\Security\Voters\IsAdmin:
327-
tags:
328-
- name: message_bus.voter
329-
permission: is_admin
330-
331-
Acme\MenuService\Presentation\Security\Voters\CanWriteMenu:
332-
tags:
333-
- name: message_bus.voter
334-
permission: can_write_menu
335-
336-
Acme\MenuService\Presentation\Security\Voters\CanManageMenu:
337-
tags:
338-
- name: message_bus.voter
339-
permission: can_manage_menu
340-
```
341-
342-
```php
343-
<?php
344-
345-
// ...
346-
347-
final class Permissions
348-
{
349-
public const IS_ADMIN = 'is_admin';
350-
public const CAN_WRITE_MENU = 'can_write_menu';
351-
public const CAN_MANAGE_MENU = 'can_manage_menu';
352-
}
353-
354-
// ...
355-
356-
use ICanBoogie\MessageBus\Attribute\Permission;
357-
358-
#[Permission(Permissions::IS_ADMIN)]
359-
#[Permission(Permissions::CAN_WRITE_MENU)]
360-
final class CreateMenu
361-
{
362-
public function __construct(
363-
public readonly array $payload
364-
)// ...
365-
}
366-
367-
// ...
368-
369-
use ICanBoogie\MessageBus\Attribute\Handler;
370-
371-
#[Handler]
372-
final class CreateMenuHandler
373-
{
374-
public function __invoke(CreateMenu $message)// ...
375-
}
376-
377-
// ...
378-
379-
use ICanBoogie\MessageBus\Attribute\Vote;
380-
381-
#[Vote(Permissions::IS_ADMIN)]
382-
final class IsAdmin implements Voter
383-
{
384-
// ...
385-
}
386-
```
387-
388-
389-
390-
### Configure components from attributes
391-
392-
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`.
393-
394-
```php
395-
<?php
396-
397-
// …
398-
399-
#[Route('/skills')]
400-
final class SkillController extends ControllerAbstract
401-
{
402-
#[Post]
403-
private function create(): void
404-
{
405-
// …
406-
}
407-
408-
#[Get('.html')]
409-
private function list(): void
410-
{
411-
// …
412-
}
413-
414-
#[Get('/summonable.html')]
415-
private function summonable(): void
416-
{
417-
// …
418-
}
419-
420-
#[Get('/learnable.html')]
421-
private function learnable(): void
422-
{
423-
// …
424-
}
425-
426-
#[Get('/:slug.html')]
427-
private function show(string $slug): void
428-
{
429-
// …
430-
}
431-
}
432-
```
433-
434-
Because the `Get` and `Post` attributes extend `Route`, all action methods can be retrieved with the `filterTargetMethods()` method.
435-
436-
```php
437-
/** @var TargetMethod<Route>[] $target_methods */
438-
$target_methods = Attributes::filterTargetMethods(
439-
Attributes::predicateForAttributeInstanceOf(Route::class)
440-
);
441-
```
442-
443-
Now then, configuring the router looks as simple as this:
444-
445-
```php
446-
<?php
447-
448-
use ICanBoogie\Binding\Routing\ConfigBuilder;
449-
450-
/* @var ConfigBuilder $config */
451-
452-
$config->from_attributes();
453-
```
454-
455-
456-
457-
## Using Attributes
458-
459-
### Filtering target methods
255+
## Frequently Asked Questions
460256

461-
`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.
257+
**Do I need to generate an optimized autoloader?**
462258

463-
Let's say we have a `Route` attribute extended by `Get`, `Post`, `Put`
259+
You don't need to generate an optimized autoloader for this to work. The plugin uses code similar
260+
to Composer to find classes. Anything that works with Composer should work with the plugin.
464261

465-
```php
466-
<?php
262+
**Can I use the plugin during development?**
467263

468-
use olvlvl\ComposerAttributeCollector\Attributes;
264+
Yes, you can use the plugin during development, but keep in mind the attributes file is only
265+
generated after the autoloader is dumped. If you modify attributes you'll have to run
266+
`composer dump` to refresh the attributes file.
469267

470-
/** @var TargetMethod<Route>[] $target_methods */
471-
$target_methods = [
472-
...Attributes::findTargetMethods(Get::class),
473-
...Attributes::findTargetMethods(Head::class),
474-
...Attributes::findTargetMethods(Post::class),
475-
...Attributes::findTargetMethods(Put::class),
476-
...Attributes::findTargetMethods(Delete::class),
477-
...Attributes::findTargetMethods(Connect::class),
478-
...Attributes::findTargetMethods(Options::class),
479-
...Attributes::findTargetMethods(Trace::class),
480-
...Attributes::findTargetMethods(Patch::class),
481-
...Attributes::findTargetMethods(Route::class),
482-
];
483-
484-
// Can be replaced by:
485-
486-
/** @var TargetMethod<Route>[] $target_methods */
487-
$target_methods = Attributes::filterTargetMethods(
488-
Attributes::predicateForAttributeInstanceOf(Route::class)
489-
);
490-
```
268+
As a workaround you could have watchers on the directories that contain classes with attributes to
269+
run `XDEBUG_MODE=off composer dump` when you make changes. [PhpStorm offers file watchers][phpstorm-watchers].
270+
You could also use [spatie/file-system-watcher][], it only requires PHP. If the plugin is too slow
271+
for your liking, try running the command with `COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE=yes`, it will
272+
enable caching and speed up consecutive runs.
491273

492274

493275

@@ -526,6 +308,5 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
526308

527309
[Composer]: https://getcomposer.org/
528310
[root-only]: https://getcomposer.org/doc/04-schema.md#root-package
529-
[ICanBoogie/MessageBus]: https://github.com/ICanBoogie/MessageBus
530311
[spatie/file-system-watcher]: https://github.com/spatie/file-system-watcher
531312
[phpstorm-watchers]: https://www.jetbrains.com/help/phpstorm/using-file-watchers.html

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@
4040
"extra": {
4141
"class": "olvlvl\\ComposerAttributeCollector\\Plugin",
4242
"composer-attribute-collector": {
43-
"ignore-paths": [
44-
"IncompatibleSignature"
43+
"include": [
44+
"tests"
45+
],
46+
"exclude": [
47+
"tests/Acme/PSR4/IncompatibleSignature.php"
4548
]
4649
}
4750
}

src/Config.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace olvlvl\ComposerAttributeCollector;
44

5+
use Composer\Composer;
56
use Composer\Factory;
67
use Composer\PartialComposer;
78
use Composer\Util\Platform;
@@ -39,12 +40,7 @@ final class Config
3940

4041
public static function from(PartialComposer $composer): self
4142
{
42-
$vendorDir = $composer->getConfig()->get('vendor-dir');
43-
44-
if (!is_string($vendorDir) || !$vendorDir) {
45-
throw new RuntimeException("Unable to determine vendor directory");
46-
}
47-
43+
$vendorDir = self::resolveVendorDir($composer);
4844
$composerFile = Factory::getComposerFile();
4945
$rootDir = realpath(dirname($composerFile));
5046

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

70+
/**
71+
* @return non-empty-string
72+
*/
73+
public static function resolveVendorDir(PartialComposer $composer): string
74+
{
75+
$vendorDir = $composer->getConfig()->get('vendor-dir');
76+
77+
if (!is_string($vendorDir) || !$vendorDir) {
78+
throw new RuntimeException("Unable to determine vendor directory");
79+
}
80+
81+
return $vendorDir;
82+
}
83+
7484
/**
7585
* @readonly
7686
* @var non-empty-string|null

src/MemoizeClassMapFilter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ public function filter(array $classMap, Closure $filter): array
5555
if ($timestamp < $mtime) {
5656
if ($timestamp) {
5757
$diff = $mtime - $timestamp;
58-
$this->io->debug("Refresh filtered files in '$pathname' ($diff sec ago)");
58+
$this->io->debug("Refresh filtered file '$pathname' ($diff sec ago)");
5959
} else {
60-
$this->io->debug("Filter files in '$pathname'");
60+
$this->io->debug("Filter '$pathname'");
6161
}
6262

6363
$keep = $filter($class, $pathname);

0 commit comments

Comments
 (0)