Skip to content

Commit 85c45c5

Browse files
authored
Introduce caching in multiple steps (Fix #6) (#10)
* Cache class map * Cache filtered files * Cache class attributes
1 parent 3da4803 commit 85c45c5

24 files changed

+764
-114
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.composer-attribute-collector
12
.phpunit.result.cache
23
vendor
34
composer.lock

MIGRATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ None
1111
- [#11](https://github.com/olvlvl/composer-attribute-collector/pull/11) Attribute instantiation errors are decorated to help find origin (@withinboredom @olvlvl)
1212
- [#12](https://github.com/olvlvl/composer-attribute-collector/pull/12) `Attributes::filterTargetClasses()` can filter target classes using a predicate (@olvlvl)
1313
- [#12](https://github.com/olvlvl/composer-attribute-collector/pull/12) `Attributes::filterTargetMethods()` can filter target methods using a predicate (@olvlvl)
14+
- [#10](https://github.com/olvlvl/composer-attribute-collector/pull/10) 3 types of cache speed up generation by limiting updates to changed files (@xepozz @olvlvl)
1415

1516
### Backward Incompatible Changes
1617

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ test-coveralls: test-dependencies
2929

3030
.PHONY: test-cleanup
3131
test-cleanup:
32+
@rm -rf .composer-attribute-collector/*
3233
@rm -rf tests/sandbox/*
3334

3435
.PHONY: test-container

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Later, these targets can be retrieved through a convenient interface.
1818
- No dependency (except Composer of course).
1919
- A single interface to get attribute targets.
2020
- A single interface to get class attributes.
21+
- 3 types of cache speed up generation by limiting updates to changed files.
2122

2223

2324

@@ -73,21 +74,23 @@ The plugin is currently experimental and its interface subject to change. Also,
7374
class and method targets. Please [contribute](CONTRIBUTING.md) if you're interested in shaping its
7475
future.
7576

77+
**Note:** The plugin creates a `.composer-attribute-collector` directory to store caches, you might
78+
want to add it to your `.gitignore` file.
79+
7680

7781

7882
## Frequently Asked Questions
7983

8084
**Do I need to generate an optimized autoloader?**
8185

82-
You don't need to generate an optimized autoloader for this to work. The
83-
plugin uses code similar to Composer to find classes. Anything that works with Composer should work
84-
with the plugin.
86+
You don't need to generate an optimized autoloader for this to work. The plugin uses code similar
87+
to Composer to find classes. Anything that works with Composer should work with the plugin.
8588

8689
**Can I use the plugin during development?**
8790

8891
Yes, you can use the plugin during development, but keep in mind the attributes file is only
89-
generated after the autoloader is dumped. If you modify attributes you'll have to
90-
run `composer dump` to refresh the attributes file.
92+
generated after the autoloader is dumped. If you modify attributes you'll have to run
93+
`composer dump` to refresh the attributes file.
9194

9295
As a workaround you could have watchers on the directories that contain classes with attributes to
9396
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.
@@ -296,6 +299,8 @@ final class IsAdmin implements Voter
296299
}
297300
```
298301

302+
303+
299304
## Using Attributes
300305

301306
### Filtering target methods

phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<file>src</file>
55
<file>tests</file>
66
<exclude-pattern>tests/sandbox/*</exclude-pattern>
7+
<exclude-pattern>tests/sandbox-memoize-classmap/*</exclude-pattern>
78

89
<arg name="colors"/>
910

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
parameters:
2+
bootstrapFiles:
3+
- tests/bootstrap.php
24
level: max
35
paths:
46
- src

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" colors="true"
4-
bootstrap="./vendor/autoload.php">
4+
bootstrap="./tests/bootstrap.php">
55
<coverage>
66
<include>
77
<directory suffix=".php">./src</directory>

src/ClassAttributeCollector.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace olvlvl\ComposerAttributeCollector;
4+
5+
use Attribute;
6+
use Composer\IO\IOInterface;
7+
use ReflectionAttribute;
8+
use ReflectionClass;
9+
use ReflectionException;
10+
11+
/**
12+
* @internal
13+
*/
14+
class ClassAttributeCollector
15+
{
16+
public function __construct(
17+
private IOInterface $io,
18+
) {
19+
}
20+
21+
/**
22+
* @param class-string $class
23+
*
24+
* @return array{
25+
* array<array{ class-string, array<int|string, mixed> }>,
26+
* array<array{ class-string, array<int|string, mixed>, non-empty-string }>
27+
* }
28+
* Where `0` is an array of class attributes, and `1` is an array of method attributes.
29+
* @throws ReflectionException
30+
*/
31+
public function collectAttributes(string $class): array
32+
{
33+
$classReflection = new ReflectionClass($class);
34+
35+
if (self::isAttribute($classReflection)) {
36+
return [ [], [] ];
37+
}
38+
39+
$classAttributes = [];
40+
$attributes = $classReflection->getAttributes();
41+
42+
foreach ($attributes as $attribute) {
43+
if (self::isAttributeIgnored($attribute)) {
44+
continue;
45+
}
46+
47+
$this->io->debug("Found attribute {$attribute->getName()} on $class");
48+
49+
$classAttributes[] = [ $attribute->getName(), $attribute->getArguments() ];
50+
}
51+
52+
$methodAttributes = [];
53+
54+
foreach ($classReflection->getMethods() as $methodReflection) {
55+
foreach ($methodReflection->getAttributes() as $attribute) {
56+
if (self::isAttributeIgnored($attribute)) {
57+
continue;
58+
}
59+
60+
$method = $methodReflection->name;
61+
assert($method !== '');
62+
63+
$this->io->debug("Found attribute {$attribute->getName()} on $class::$method");
64+
65+
$methodAttributes[] = [ $attribute->getName(), $attribute->getArguments(), $method ];
66+
}
67+
}
68+
69+
return [ $classAttributes, $methodAttributes ];
70+
}
71+
72+
/**
73+
* Determines if a class is an attribute.
74+
*
75+
* @param ReflectionClass<object> $classReflection
76+
*/
77+
private static function isAttribute(ReflectionClass $classReflection): bool
78+
{
79+
foreach ($classReflection->getAttributes() as $attribute) {
80+
if ($attribute->getName() === Attribute::class) {
81+
return true;
82+
}
83+
}
84+
85+
return false;
86+
}
87+
88+
/**
89+
* @param ReflectionAttribute<object> $attribute
90+
*/
91+
private static function isAttributeIgnored(ReflectionAttribute $attribute): bool
92+
{
93+
static $ignored = [
94+
\ReturnTypeWillChange::class => true,
95+
];
96+
97+
return isset($ignored[$attribute->getName()]);
98+
}
99+
}

src/ClassMapBuilder.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
namespace olvlvl\ComposerAttributeCollector;
1111

12-
use Composer\ClassMapGenerator\ClassMapGenerator;
1312
use Composer\Pcre\Preg;
1413
use Composer\Util\Filesystem;
1514
use Composer\Util\Platform;
@@ -27,6 +26,11 @@
2726
*/
2827
class ClassMapBuilder
2928
{
29+
public function __construct(
30+
private MemoizeClassMapGenerator $classMapGenerator
31+
) {
32+
}
33+
3034
/**
3135
* @param array{
3236
* 'psr-0': array<string, array<string>>,
@@ -40,9 +44,9 @@ class ClassMapBuilder
4044
*/
4145
public function buildClassMap(array $autoloads): array
4246
{
47+
$classMapGenerator = $this->classMapGenerator;
48+
4349
$excluded = $autoloads['exclude-from-classmap'];
44-
$classMapGenerator = new ClassMapGenerator();
45-
$classMapGenerator->avoidDuplicateScans();
4650

4751
foreach ($autoloads['classmap'] as $dir) {
4852
// @phpstan-ignore-next-line
@@ -70,7 +74,7 @@ public function buildClassMap(array $autoloads): array
7074
$dir = $filesystem->normalizePath(
7175
$filesystem->isAbsolutePath($dir) ? $dir : $basePath . '/' . $dir
7276
);
73-
if (!is_dir($dir)) {
77+
if ($dir === '' || !is_dir($dir)) {
7478
continue; // @codeCoverageIgnore
7579
}
7680

@@ -85,7 +89,7 @@ public function buildClassMap(array $autoloads): array
8589
}
8690
}
8791

88-
return $classMapGenerator->getClassMap()->getMap();
92+
return $classMapGenerator->getMap();
8993
}
9094

9195
/**

src/Collector.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99

1010
namespace olvlvl\ComposerAttributeCollector;
1111

12-
use ReflectionAttribute;
13-
use ReflectionClass;
14-
use ReflectionMethod;
15-
1612
/**
1713
* Collects classes and methods with attributes.
1814
*
@@ -31,21 +27,29 @@ final class Collector
3127
public array $methods = [];
3228

3329
/**
34-
* @param ReflectionAttribute<object> $attribute
35-
* @param ReflectionClass<object> $class
30+
* @param array<array{ class-string, array<int|string, mixed> }> $attributes
31+
* An array of method attributes, where `0` is an attribute class, `1` the attributes arguments.
32+
* @param class-string $class
33+
* The target class.
3634
*/
37-
public function addTargetClass(ReflectionAttribute $attribute, ReflectionClass $class): void
35+
public function addClassAttributes(array $attributes, string $class): void
3836
{
39-
$this->classes[$attribute->getName()][]
40-
= new TargetClassRaw($attribute->getArguments(), $class->name);
37+
foreach ($attributes as [ $attribute, $arguments ]) {
38+
$this->classes[$attribute][] = new TargetClassRaw($arguments, $class);
39+
}
4140
}
4241

4342
/**
44-
* @param ReflectionAttribute<object> $attribute
43+
* @param array<array{ class-string, array<int|string, mixed>, string }> $attributes
44+
* An array of method attributes, where `0` is an attribute class, `1` the attributes arguments,
45+
* and `2` the method.
46+
* @param class-string $class
47+
* The target class.
4548
*/
46-
public function addTargetMethod(ReflectionAttribute $attribute, ReflectionMethod $method): void
49+
public function addMethodAttributes(array $attributes, string $class): void
4750
{
48-
$this->methods[$attribute->getName()][]
49-
= new TargetMethodRaw($attribute->getArguments(), $method->class, $method->name);
51+
foreach ($attributes as [ $attribute, $arguments, $method ]) {
52+
$this->methods[$attribute][] = new TargetMethodRaw($arguments, $class, $method);
53+
}
5054
}
5155
}

0 commit comments

Comments
 (0)