Skip to content

Commit

Permalink
fix compilation to blueprints for classes with attributes only, docs
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikbosch committed Aug 14, 2024
1 parent 2610e8f commit 5fbb45b
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 72 deletions.
54 changes: 52 additions & 2 deletions docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ $di->params['Example']['foo'] = $di->lazyGetCall('config', 'get', 'alpha');
## Modify the Container using attributes

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
[`ClassScannerConfig`](config.md#scan-for-classes-and-annotations). When done so, the builder will scan the
passed directories for classes and annotations. Every class that is annotated with `#[AttributeConfigFor]`
and implements `AttributeConfigInterface` can modify the container.

Expand Down Expand Up @@ -282,4 +282,54 @@ class SymfonyRouteAttributeConfig implements AttributeConfigInterface
}
}
}
```
```

## Compiled Blueprints

[Reflection](https://www.php.net/reflection) is used by the container to get information of the class, e.g. what
parameters are used by the constructor. This information is used to create a class that in this package is called
a `Blueprint`.

When you annotate a constructor parameter with `#[Service]`, `#[Instance]`, `#[Value]` or with an attribute implementing
`Aura\Di\Attribute\AnnotatedInjectInterface` then the class automatically gets the marker that it needs to compiled
into a `Blueprint` when you call `newCompiledInstance` method on the `ContainerBuilder`. This is also true that use
the code method like `$container->params` and `$container->setters`.

There might be classes however, that are not configured using attributes or code but need to be instantiated by the
container somewhere in your code anyhow. Take for instance the class below. This class does not have an injection
attribute like `#[Service]`, `#[Config]` or `#[Value]`, and there might not also be a `$di->params[]` call to configure
this class. So the class is unknown to the container.

If you want to create a `Blueprint` for this class during container compilation, annotate it with `#[Blueprint]`.

```php
use Aura\Di\Attribute\Blueprint;

#[Blueprint]
class OrderController
{
public function __construct(private Connection $databaseConnection)
{
}
}
```

To prevent, many classes have to be annotated with the `#[Blueprint]` attribute, you can also use the
`#[BlueprintNamespace]` attribute, typically annotated to be an `Application`, `Kernel` or `Plugin` class.

```php
namespace MyPlugin;

use Aura\Di\Attribute\BlueprintNamespace;

#[BlueprintNamespace(__NAMESPACE__ . '\\Controllers')]
#[BlueprintNamespace(__NAMESPACE__ . '\\Command')]
class Plugin {

}
```

Typically, you should not compile all namespace in your application or plugin. That would be overkill, because there
are classes like entities, models and DTOs that are never being instantiated by the container.

Working with compiled blueprints require using the [`ClassScannerConfig`](config.md#scan-for-classes-and-annotations).
103 changes: 53 additions & 50 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

## The ContainerBuilder

The _ContainerBuilder_ also builds fully-configured _Container_ objects using _ContainerConfig_ classes. It works
The _ContainerBuilder_ builds fully-configured _Container_ objects using _ContainerConfig_ classes. It works
using a [two-stage configuration system](http://auraphp.com/blog/2014/04/07/two-stage-config).

The two stages are "define" and "modify". In the "define" stage, the _ContainerConfig_ object defines constructor parameter values, setter method values, services, and so on. The _ContainerBuilder_ then locks the _Container_ so that these definitions cannot be changed, and begins the "modify" stage. In the "modify" stage, we may `get()` services from the _Container_ and modify them programmatically if needed.
The two stages are "define" and "modify". In the "define" stage, the _ContainerConfig_ object defines constructor
parameter values, setter method values, services, and so on. The _ContainerBuilder_ then locks the _Container_ so that
these definitions cannot be changed, and begins the "modify" stage. In the "modify" stage, we may `get()` services from
the _Container_ and modify them programmatically if needed.

To build a fully-configured _Container_ using the _ContainerBuilder_, we do something like the following:

Expand All @@ -17,9 +20,9 @@ $container_builder = new ContainerBuilder();
// use the builder to create and configure a container
// using an array of ContainerConfig classes
$di = $container_builder->newConfiguredInstance([
'Aura\Cli\_Config\Common',
'Aura\Router\_Config\Common',
'Aura\Web\_Config\Common',
Aura\Cli\_Config\Common::class,
Aura\Router\_Config\Common::class,
Aura\Web\_Config\Common::class,
]);
```

Expand Down Expand Up @@ -68,9 +71,9 @@ $routerConfig = new Aura\Router\_Config\Common();
// use the builder to create and configure a container
// using an array of ContainerConfig classes
$di = $container_builder->newConfiguredInstance([
'Aura\Cli\_Config\Common',
Aura\Cli\_Config\Common::class,
$routerConfig,
'Aura\Web\_Config\Common',
Aura\Web\_Config\Common::class,
]);
```

Expand Down Expand Up @@ -112,8 +115,11 @@ $di = $container_builder->newConfiguredInstance([\My\App\Config::class])
## Compiling and serializing the container

With the _ContainerBuilder_, you can also create a compiled container that is ready for serialization. A compiled
container does all the class metadata collection and creates a `Blueprint` class for every class that can be instantiated.
The methodology for creating a compiled container is similar to creating a configured instance.
container does all the class metadata collection and creates a `Blueprint` class for every class that has been
configured in the container. The methodology for creating a compiled container is similar to creating
a configured instance.

Instead of `newConfiguredInstance`, you now call the `newCompiledInstance` method.

```php
use Aura\Di\ContainerBuilder;
Expand All @@ -130,23 +136,28 @@ $di = $container_builder->newCompiledInstance(
);
```

We can then serialize and unserialize the container:
The resulting container is ready for serialization. You can find a more real-world example below, which checks for
a serialized container on the filesystem. When it does not exist, it uses the `ContainerBuilder` to create a container
and save it to the filesystem.

```php
$serialized = serialize($di);
$di = unserialize($serialized);
```
use Aura\Di\ContainerBuilder;

This serialized container might be saved to a file, as cache layer before continuing. Finally, we must configure the
compiled instance.
$serializedContainerFile = '/var/compiled.ser';

```php
$di = $builder->configureCompiledInstance($di, $config_classes);

$fakeService = $di->get('fake');
if (file_exists($serializedContainerFile)) {
$di = \unserialize(file_get_contents($serializedContainerFile));
} else {
$builder = new ContainerBuilder();
$di = $builder->newCompiledInstance($config_classes);

$serialized = \serialize($di);
file_put_contents($serializedContainerFile, $serialized);
}
```

Please note, serializing won't work with closures. Serializing a container with following throws an exception.
Please note, serialization won't work with closures. Serializing a container with following configuration throws an
exception.

```php
$di->params[VendorClass::class] = [
Expand All @@ -158,37 +169,11 @@ $di->params[VendorClass::class] = [
];
```

## Compiled Blueprints

When classes are instantiated by the container, it uses reflection to get information of the class, e.g. what
parameters are used by the constructor. This information is used to create a class that in this package is called
a Blueprint.

In order to prevent that blueprints have to created for every PHP-run, you can improve performance by compiling
blueprints. You do this by annotating a class, typically an `Application`, `Kernel` or `Plugin` class. The following
example demonstrates how to use compiled blueprints for your `Controller` and `Command` namespace.

```php
namespace MyPlugin;

use Aura\Di\Attribute\CompileNamespace;

#[CompileNamespace(__NAMESPACE__ . '\\Controllers')]
#[CompileNamespace(__NAMESPACE__ . '\\Command')]
class Plugin {

}
```

Typically, you should not compile namespace your complete plugin. That would be overkill, because there are classes
like entities, models and DTOs that are never being instantiated by the container.

Working with the `#[CompileNamespace]` attribute requires using the `ClassScannerConfig` which is documented below.

## Scan for classes and annotations

The `ClassScannerConfig` class uses a generated-file to extract all classes and annotations from your project. You will
need that if you want to [modify the container using attributes](attributes.md#modify-the-container-using-attributes).
need that if you want to [modify the container using attributes](attributes.md#modify-the-container-using-attributes) or
you want to compile blueprints.

First of all, this does require to add a package to your dependencies.

Expand Down Expand Up @@ -220,10 +205,14 @@ be picked up by the scanner too.
}
```

Then execute the scan.
Then execute the scan, and see the file `vendor/aura.di.scan.json` as result.

```shell
# scan inside the classmap paths, but if cache exits, it returns the cache
vendor/bin/auradi scan

# force a complete new scan of all classes and annotations inside the classmap paths
vendor/bin/auradi scan --force
```

Then add the `ClassScannerConfig` to your Container Config classes. This example will generate a container in which
Expand All @@ -237,12 +226,26 @@ $builder = new ContainerBuilder();
$config_classes = [
new \MyApp\Config1,
new \MyApp\Config2,
ClassScannerConfig::newScanner(__DIR__ . '/../../aura.di.scan.json') // reference the correct path here
ClassScannerConfig::newScanner('vendor/aura.di.scan.json') // reference the correct path here
];

$di = $builder->newCompiledInstance($config_classes);
```

During development, you will have to rescan if you have annotated classes with attributes that modify the
container. Also, if you have dependencies with those attributes, you probably want to attach
[a 'post-install-cmd' and/or a 'post-update-cmd'](https://getcomposer.org/doc/articles/scripts.md#command-events)
script to your composer.json.

```json
{
"scripts": {
"post-install-cmd": "vendor/bin/auradi scan --force",
"post-update-cmd": "vendor/bin/auradi scan --force"
}
}
```

## Compiled objects inside the container

There might be other objects that you want to compile before serializing the container. A good example might be a
Expand Down
18 changes: 18 additions & 0 deletions src/Attribute/Blueprint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
*
* This file is part of Aura for PHP.
*
* @license http://opensource.org/licenses/MIT MIT
*
*/

namespace Aura\Di\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Blueprint
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class CompileNamespace
class BlueprintNamespace
{
private string $namespace;

Expand Down
26 changes: 26 additions & 0 deletions src/ClassScanner/AnnotatedInjectAttributeConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
*
* This file is part of Aura for PHP.
*
* @license http://opensource.org/licenses/MIT MIT
*
*/

namespace Aura\Di\ClassScanner;

use Aura\Di\Attribute\AnnotatedInjectInterface;
use Aura\Di\Container;

final class AnnotatedInjectAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
{
/** @var AnnotatedInjectInterface $attribute */
$attribute = $specification->getAttributeInstance();
if ($specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) {
$di->params[$specification->getClassName()][$specification->getTargetConfig()['parameter']] = $attribute->inject();
}
}
}
21 changes: 21 additions & 0 deletions src/ClassScanner/BlueprintAttributeConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
*
* This file is part of Aura for PHP.
*
* @license http://opensource.org/licenses/MIT MIT
*
*/

namespace Aura\Di\ClassScanner;

use Aura\Di\Container;

final class BlueprintAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
{
$di->params[$specification->getClassName()] = $di->params[$specification->getClassName()] ?? [];
}
}
35 changes: 26 additions & 9 deletions src/ClassScanner/ClassScannerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Aura\Di\ClassScanner;

use Aura\Di\Attribute\AnnotatedInjectInterface;
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\Attribute\CompileNamespace;
use Aura\Di\Attribute\Blueprint;
use Aura\Di\Attribute\BlueprintNamespace;
use Aura\Di\Container;
use Aura\Di\ContainerConfigInterface;

Expand Down Expand Up @@ -36,37 +38,52 @@ public function define(Container $di): void
{
$classMap = $this->mapGenerator->generate();

$configuration = [];
$compileNamespaces = [];
$attributeConfigs = [
AnnotatedInjectInterface::class => AnnotatedInjectAttributeConfig::class,
Blueprint::class => BlueprintAttributeConfig::class,
];

$blueprintNamespaces = [];
foreach ($classMap->getAttributeSpecifications() as $specification) {
$attribute = $specification->getAttributeInstance();
$attributeConfigClass = $specification->getClassName();
if ($attribute instanceof AttributeConfigFor && \is_a($attributeConfigClass, AttributeConfigInterface::class, true)) {
$configFor = $attribute->getClassName();
$configuration[$configFor] = $attributeConfigClass;
$attributeConfigs[$configFor] = $attributeConfigClass;
}

if ($attribute instanceof CompileNamespace) {
$compileNamespaces[] = $attribute->getNamespace();
if ($attribute instanceof BlueprintNamespace) {
$blueprintNamespaces[] = $attribute->getNamespace();
}
}

foreach ($classMap->getClasses() as $className) {
foreach ($compileNamespaces as $namespace) {
foreach ($blueprintNamespaces as $namespace) {
if (\str_starts_with($className, $namespace)) {
$di->params[$className] = $di->params[$className] ?? [];
}
}

foreach ($classMap->getAttributeSpecificationsFor($className) as $specification) {
$attribute = $specification->getAttributeInstance();
if (\array_key_exists($attribute::class, $configuration)) {
$configuredBy = $configuration[$attribute::class];
if (\array_key_exists($attribute::class, $attributeConfigs)) {
$configuredBy = $attributeConfigs[$attribute::class];
$configuredBy::define(
$di,
$specification,
);
}

$interfaces = \class_implements($attribute);
foreach ($interfaces as $interface) {
if (\array_key_exists($interface, $attributeConfigs)) {
$configuredBy = $attributeConfigs[$interface];
$configuredBy::define(
$di,
$specification,
);
}
}
}
}
}
Expand Down
Loading

0 comments on commit 5fbb45b

Please sign in to comment.