Skip to content

Commit

Permalink
Merge pull request #221 from auraphp/class_scanner_improve
Browse files Browse the repository at this point in the history
Improve class scanner by using a binary to execute scans
  • Loading branch information
frederikbosch authored Aug 13, 2024
2 parents 43642d9 + c6a1f30 commit 44fdd6f
Show file tree
Hide file tree
Showing 29 changed files with 1,097 additions and 271 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
## Unreleased

- (ADD) $container->lazyLazy() to create a callable from a LazyInterface that is directly invokable.
- (ADD) `#[AttributeConfigFor]` attribute to indicate scanner which attribute is configured by the annotated class.
- (ADD) `#[CompileNamespace]` attribute to indicate scanner which namespace should have compiled blueprints.
- (ADD) `bin/auradi` executable with scan command.
- (CHG) Fix `lazyGet` not resolving the same service.
- (CHG) Fix collection of attributes for class constants.
- (CHG) `#[Service]`, `#[Instance]` and `#[Value]` must have `Attribute::TARGET_PROPERTY` to allow constructor promotion.

## 5.0.0-alpha.1

- (ADD) Inject via attributes
- (ADD) Inject via `#[Service]`, `#[Instance]` and `#[Value]` attributes.
- (ADD) Configure the container via attributes using the AttributeConfigInterface
- (ADD) Directories scanner for classes and annotations
- (ADD) Dependency requirement of composer/class-map-generator when using the scanner
Expand Down
110 changes: 110 additions & 0 deletions bin/auradi
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env php
<?php

use Aura\Di\ClassScanner\CachedFileGenerator;
use Aura\Di\ClassScanner\ComposerMapGenerator;

if (PHP_SAPI !== 'cli') {
echo 'Warning: auradi should be invoked via the CLI version of PHP, not the ' . PHP_SAPI . ' SAPI' . PHP_EOL;
}

$currentCwd = getcwd();

if (!file_exists($currentCwd . '/vendor/autoload.php')) {
throw new RuntimeException('Could not find autoload.php in path ' . $currentCwd . '/vendor/autoload.php');
}

require $currentCwd . '/vendor/autoload.php';

if (\count($argv) < 2) {
echo 'Use one of the following commands:' . PHP_EOL;
echo PHP_EOL;
echo 'scan Scan for classes and annotations, uses cache file' . PHP_EOL;
echo ' --force Forced full rescan, no cache used' . PHP_EOL;
echo ' -u {filename} Scan only passed updated file' . PHP_EOL;
}

$args = $argv;
$scriptDir = array_shift($args);
$command = array_shift($args);

if ($command === 'scan') {
if (!file_exists($currentCwd . '/vendor/composer/installed.json')) {
throw new RuntimeException('Could not include ./vendor/composer/installed.json in path ' . $currentCwd . '/vendor/composer/installed.json');
}

echo 'Using ./vendor/composer/installed.json to detect which paths to scan:' . PHP_EOL;

$installedJsonFile = $currentCwd . '/vendor/composer/installed.json';
$installedJsonContents = file_get_contents($installedJsonFile);
if (!$installedJsonContents) {
throw new RuntimeException('Could not read json from ./vendor/composer/installed.json');
}

$installedJson = json_decode($installedJsonContents, true);
if (!$installedJson) {
throw new RuntimeException('Could not parse json from ./vendor/composer/installed.json');
}

$classMapPaths = [];
foreach ($installedJson['packages'] as $package) {
foreach (($package['extra']['aura/di']['classmap-paths'] ?? []) as $classMapPath) {
$fullPath = realpath(dirname($installedJsonFile) . '/' . $package['install-path'] . '/' . $classMapPath);
$classMapPaths[] = $fullPath;
echo '- ' . $fullPath . PHP_EOL;
}
}

echo 'Found ' . count($classMapPaths) . ' classmap paths' . PHP_EOL;
echo PHP_EOL;

if (in_array('--force', $args, true)) {
unlink($currentCwd . '/vendor/aura.di.scan.json');
}

$generator = new CachedFileGenerator(
new ComposerMapGenerator($classMapPaths, $currentCwd),
$currentCwd . '/vendor/aura.di.scan.json',
);

echo 'Scanning paths for classes and annotations.' . PHP_EOL;

$classMap = $generator->generate();
echo '- ' . count($classMap->getFiles()) . ' files' . PHP_EOL;
echo '- ' . count($classMap->getClasses()) . ' classes' . PHP_EOL;
echo '- ' . count($classMap->getAttributeSpecifications()) . ' attributes' . PHP_EOL;
echo PHP_EOL;

$updates = [];
$position = 0;
while (array_key_exists($position, $args)) {
if ($args[$position] === '-u') {
$position++;

if (!array_key_exists($position, $args)) {
throw new \UnexpectedValueException('-u must be followed by a file name');
}

$updates[] = $args[$position];
}

$position++;
}

if ($updates) {
echo 'Updating scan for' . PHP_EOL;
foreach ($updates as $update) {
echo '- ' . $update . PHP_EOL;
}

echo PHP_EOL;

$generator->update($classMap, $updates);

echo 'After update we have ' . PHP_EOL;
echo '- ' . count($classMap->getFiles()) . ' files' . PHP_EOL;
echo '- ' . count($classMap->getClasses()) . ' classes' . PHP_EOL;
echo '- ' . count($classMap->getAttributeSpecifications()) . ' attributes' . PHP_EOL;
echo PHP_EOL;
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"homepage": "https://github.com/auraphp/Aura.Di/contributors"
}
],
"bin": ["bin/auradi"],
"require": {
"php": "^8.0",
"psr/container": "^2.0.2"
Expand Down
71 changes: 55 additions & 16 deletions docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,38 +186,42 @@ $di->params['Example']['foo'] = $di->lazyGetCall('config', 'get', 'alpha');

Modifying the container with attributes requires building the container with the
[`ClassScanner`](config.md#scan-for-classes-and-annotations). When done so, the builder will scan the
passed directories for classes and annotations. Every attribute that implements the `AttributeConfigInterface` can modify the
container. See the [`ClassScanner` documentation](config.md#scan-for-classes-and-annotations) how to
modify the container for external attributes.
passed directories for classes and annotations. Every class that is annotated with `#[AttributeConfigFor]`
and implements `AttributeConfigInterface` can modify the container.

In the following example we create our own a `#[Route]` attribute that also implements the `AttributeConfigInterface`.
The attribute `#[AttributeConfigFor]` is referencing the Route class. It is basically a self-reference because the attribute is
attached to the Route class. Now, methods annotated with the new `#[Route]`
will cause a `RealRoute` to be appended in the routes array.

In the following example we create our own a `#[Route]` attribute that implements the `AttributeConfigInterface` and
annotate methods with it inside a `Controller` class.

```php
use Aura\Di\AttributeConfigInterface;
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\Container;
use Aura\Di\Injection\Lazy;
use Aura\Di\Injection\LazyLazy;
use Aura\Di\Injection\LazyNew;

#[\Attribute]
#[AttributeConfigFor(Route::class)]
class Route implements AttributeConfigInterface {
public function __construct(private string $method, private string $uri) {
}

public function define(Container $di, \ReflectionAttribute $attribute, \Reflector $annotatedTo): void
public static function define(Container $di, AttributeSpecification $specification): void
{
if ($reflector instanceof \ReflectionMethod) {
if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) {
/** @var self $attribute */
$attribute = $specification->getAttributeInstance();
// considering the routes key is a lazy array, defined like this
// $resolver->values['routes'] = $container->lazyArray([]);
$di->values['routes']->append(
new RealRoute(
$this->method,
$this->uri,
$attribute->method,
$attribute->uri,
$container->lazyLazy(
$di->lazyCallable([
$di->lazyNew($reflector->getDeclaringClass()),
$reflector->getName()
$di->lazyNew($specification->getClassName()),
$specification->getTargetConfig()['method']
])
)
)
Expand All @@ -239,8 +243,43 @@ class RouterFactory {
#[Value('routes')]
private array $routes
) {
// $routes contains an array of RealRoute objects
}
}
```

The `$routes` parameter in the RouterFactory would result in an array of `RealRoute` objects being injected.
If your attribute cannot implement the `AttributeConfigInterface`, e.g. the attribute is defined in an external package,
you can create an implementation of `AttributeConfigInterface` yourself, and annotate it with `#[AttributeConfigFor(ExternalAttribute::class)]`.

```php
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\Container;
use Symfony\Component\Routing\Attribute\Route;

#[AttributeConfigFor(Route::class)]
class SymfonyRouteAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
{
if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) {
/** @var Route $attribute */
$attribute = $specification->getAttributeInstance();

$invokableRoute = $di->lazyCallable([
$container->lazyNew($annotatedClassName),
$specification->getTargetConfig()['method']
]);

// these are not real parameters, but just examples
$di->values['routes'][] = new Symfony\Component\Routing\Route(
$attribute->getPath(),
$attribute->getMethods(),
$attribute->getName(),
$invokableRoute
);
}
}
}
```
Loading

0 comments on commit 44fdd6f

Please sign in to comment.