Skip to content

Commit dd5eef3

Browse files
authored
Resolve include from autoload (#44)
1 parent 7dffa02 commit dd5eef3

File tree

4 files changed

+115
-26
lines changed

4 files changed

+115
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ None
3131
}
3232
```
3333

34+
- [#44](https://github.com/olvlvl/composer-attribute-collector/pull/44) The collector automatically scans `autoload` paths of the root `composer.json` for a zero-configuration experience. (@olvlvl)
35+
3436
### Deprecated Features
3537

3638
None

README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
[![Code Coverage](https://coveralls.io/repos/github/olvlvl/composer-attribute-collector/badge.svg?branch=main)](https://coveralls.io/r/olvlvl/composer-attribute-collector?branch=main)
55
[![Downloads](https://img.shields.io/packagist/dt/olvlvl/composer-attribute-collector.svg)](https://packagist.org/packages/olvlvl/composer-attribute-collector)
66

7-
**composer-attribute-collector** is a plugin for [Composer][]. Its ambition is to provide a
8-
convenient way—and near zero cost—to retrieve targets of PHP 8 attributes. After the autoloader has
9-
been dumped, the plugin collects attribute targets and generates a static file. These targets can be
10-
retrieved through a convenient interface, without reflection. The plugin is useful when you need to
11-
_discover_ attribute targets in a codebase—for known targets you can use reflection.
7+
**composer-attribute-collector** is a [Composer][] plugin designed to effectively _discover_ PHP 8
8+
attribute targets, and later retrieve them at near zero cost, without runtime reflection. After the
9+
autoloader dump, it collects attributes and generates a static file for fast access. This provides a
10+
convenient way to _discover_ attribute-backed classes, methods, or properties—ideal for codebase
11+
analysis. (For known targets, traditional reflection remains an option.)
1212

1313

1414

1515
#### Features
1616

17-
- Little configuration
17+
- Zero configuration
1818
- No reflection in the generated file
19-
- No impact on performance
19+
- Might improve performance
2020
- No dependency (except Composer of course)
2121
- A single interface to get attribute targets: classes, methods, and properties
2222
- Can cache discoveries to speed up consecutive runs.
@@ -91,11 +91,11 @@ var_dump($attributes->propertyAttributes);
9191

9292
Here are a few steps to get you started.
9393

94-
### 1\. Configure the plugin
94+
### 1\. Configure the plugin (optional)
9595

96-
The plugin only inspects paths and files specified in the configuration with the `include` property.
97-
That is usually your "src" directory. Add this section to your `composer.json` file to enable the
98-
generation of the "attributes" file when the autoloader is dumped.
96+
The collector automatically scans `autoload` paths of the root `composer.json` for a
97+
zero-configuration experience. You can override them via
98+
`extra.composer-attribute-collector.include`.
9999

100100
```json
101101
{
@@ -162,8 +162,8 @@ Here are a few ways you can configure the plugin.
162162

163163
### Including paths or files ([root-only][])
164164

165-
Use the `include` property to define the paths or files to inspect for attributes. Without this
166-
property, the "attributes" file will be empty.
165+
The collector automatically scans `autoload` paths of the root `composer.json`, but you can override
166+
them via the `include` property.
167167

168168
The specified paths are relative to the `composer.json` file, and the `{vendor}` placeholder is
169169
replaced with the path to the vendor folder.
@@ -182,7 +182,7 @@ replaced with the path to the vendor folder.
182182

183183
### Excluding paths or files ([root-only][])
184184

185-
Use the `exclude` property to exclude paths or files from inspection. This is handy when files
185+
Use the `exclude` property to exclude paths or files from scanning. This is handy when files
186186
cause issues or have side effects.
187187

188188
The specified paths are relative to the `composer.json` file, and the `{vendor}` placeholder is
@@ -277,9 +277,9 @@ PHP. If the plugin is too slow for your liking, try running the command with
277277
**How do I include a class that inherits its attributes?**
278278

279279
To speed up the collection process, the plugin first looks at PHP files as plain text for hints of
280-
attribute usage. If a class inherits its attributes from traits, properties, or methods, it is
281-
ignored. Use the attribute `[#\olvlvl\ComposerAttributeCollector\InheritsAttributes]` to force the
282-
collection.
280+
attribute usage. If a class inherits its attributes from traits, properties, or methods, but doesn't
281+
use attributes itself, it will be ignored. Use the attribute
282+
`[#\olvlvl\ComposerAttributeCollector\InheritsAttributes]` to force the collection.
283283

284284
```php
285285
trait UrlTrait

src/Config.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace olvlvl\ComposerAttributeCollector;
44

55
use Composer\Factory;
6+
use Composer\Package\PackageInterface;
67
use Composer\PartialComposer;
78
use Composer\Util\Platform;
89
use InvalidArgumentException;
@@ -31,6 +32,7 @@ final class Config
3132
public const EXTRA_INCLUDE = 'include';
3233
public const EXTRA_EXCLUDE = 'exclude';
3334
public const ENV_USE_CACHE = 'COMPOSER_ATTRIBUTE_COLLECTOR_USE_CACHE';
35+
public const FILENAME = 'attributes.php';
3436

3537
/**
3638
* If a path starts with this placeholder, it is replaced with the absolute path to the vendor directory.
@@ -48,18 +50,24 @@ public static function from(PartialComposer $composer, bool $isDebug = false): s
4850
}
4951

5052
$rootDir .= DIRECTORY_SEPARATOR;
53+
$attributesFile = $vendorDir . DIRECTORY_SEPARATOR . self::FILENAME;
5154

55+
$package = $composer->getPackage();
5256
/** @var array{ include?: non-empty-string[], exclude?: non-empty-string[] } $extra */
53-
$extra = $composer->getPackage()->getExtra()[self::EXTRA] ?? [];
57+
$extra = $package->getExtra()[self::EXTRA] ?? [];
5458

55-
$include = self::expandPaths($extra[self::EXTRA_INCLUDE] ?? [], $vendorDir, $rootDir);
59+
$include = self::expandPaths(
60+
$extra[self::EXTRA_INCLUDE] ?? self::resolveInclude($package, $attributesFile),
61+
$vendorDir,
62+
$rootDir,
63+
);
5664
$exclude = self::expandPaths($extra[self::EXTRA_EXCLUDE] ?? [], $vendorDir, $rootDir);
5765

5866
$useCache = filter_var(Platform::getEnv(self::ENV_USE_CACHE), FILTER_VALIDATE_BOOL);
5967

6068
return new self(
6169
$vendorDir,
62-
attributesFile: "$vendorDir/attributes.php",
70+
attributesFile: $attributesFile,
6371
include: $include,
6472
exclude: $exclude,
6573
useCache: $useCache,
@@ -81,6 +89,29 @@ public static function resolveVendorDir(PartialComposer $composer): string
8189
return $vendorDir;
8290
}
8391

92+
/**
93+
* @param non-empty-string $attributesFile
94+
*
95+
* @return non-empty-string[]
96+
*/
97+
public static function resolveInclude(PackageInterface $package, string $attributesFile): array
98+
{
99+
$include = [];
100+
101+
foreach ($package->getAutoload() as $paths) {
102+
/** @var non-empty-string[] $paths */
103+
foreach ($paths as $path) {
104+
if (realpath($path) === $attributesFile) {
105+
continue;
106+
}
107+
108+
$include[] = $path;
109+
}
110+
}
111+
112+
return $include;
113+
}
114+
84115
/**
85116
* @readonly
86117
* @var non-empty-string|null
@@ -142,6 +173,10 @@ private static function expandPaths(array $paths, string $vendorDir, string $roo
142173
$expanded = [];
143174

144175
foreach ($paths as $path) {
176+
if (str_starts_with($path, "./")) {
177+
$path = substr($path, 2);
178+
}
179+
145180
if (str_starts_with($path, self::VENDOR_PLACEHOLDER)) {
146181
$path = $vendorDir . substr($path, strlen(self::VENDOR_PLACEHOLDER));
147182
} else {

tests/ConfigTest.php

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@
44

55
use Composer\Package\RootPackageInterface;
66
use Composer\PartialComposer;
7+
use Composer\Util\Platform;
78
use olvlvl\ComposerAttributeCollector\Config;
89
use PHPUnit\Framework\TestCase;
910
use RuntimeException;
1011

11-
use function assert;
12-
use function getcwd;
13-
use function is_string;
14-
1512
final class ConfigTest extends TestCase
1613
{
1714
public function testFrom(): void
@@ -34,8 +31,7 @@ public function testFrom(): void
3431
->method('getExtra')
3532
->willReturn($extra);
3633

37-
$cwd = getcwd();
38-
assert(is_string($cwd));
34+
$cwd = Platform::getCwd();
3935
$config = $this->createMock(\Composer\Config::class);
4036
$config
4137
->method('get')
@@ -66,6 +62,62 @@ public function testFrom(): void
6662
$this->assertEquals($expected, $actual);
6763
}
6864

65+
public function testResolveIncludeFromAutoload(): void
66+
{
67+
$package = $this->createMock(RootPackageInterface::class);
68+
$package
69+
->method('getExtra')
70+
->willReturn([]);
71+
$package
72+
->expects($this->once())
73+
->method('getAutoload')
74+
->willReturn([
75+
'classmap' => [
76+
'src/classmap',
77+
'src/bootstrap.php',
78+
],
79+
'psr-0' => [
80+
'Acme/PSR4' => './src/psr-0',
81+
],
82+
'psr-4' => [
83+
'Acme/PSR4' => 'src/psr-4',
84+
],
85+
'files' => [
86+
'./src/files'
87+
]
88+
]);
89+
90+
$cwd = Platform::getCwd();
91+
$config = $this->createMock(\Composer\Config::class);
92+
$config
93+
->method('get')
94+
->with('vendor-dir')
95+
->willReturn("$cwd/vendor");
96+
97+
$composer = new PartialComposer();
98+
$composer->setConfig($config);
99+
$composer->setPackage($package);
100+
101+
$expected = new Config(
102+
vendorDir: "$cwd/vendor",
103+
attributesFile: "$cwd/vendor/attributes.php",
104+
include: [
105+
"$cwd/src/classmap",
106+
"$cwd/src/bootstrap.php",
107+
"$cwd/src/psr-0",
108+
"$cwd/src/psr-4",
109+
"$cwd/src/files",
110+
],
111+
exclude: [],
112+
useCache: false,
113+
isDebug: false,
114+
);
115+
116+
$actual = Config::from($composer);
117+
118+
$this->assertEquals($expected, $actual);
119+
}
120+
69121
public function testFromFailsOnMissingVendorDir(): void
70122
{
71123
$config = $this->createMock(\Composer\Config::class);

0 commit comments

Comments
 (0)