Skip to content

Commit

Permalink
add documentation, some more simplifications
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikbosch committed May 21, 2024
1 parent d36eb92 commit 0a3d0e6
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 60 deletions.
79 changes: 71 additions & 8 deletions docs/attributes.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Injection via attributes
# Injection and container modification via attributes

Rather than defining injection via `$di->params`, `$di->setters`, `$di->values` or `$di->types`, you can also annotate
parameters using the [native attributes available from PHP 8.0](https://www.php.net/manual/en/language.attributes.overview.php).
For now, only constructor parameters can be annotated.

The advantage would be that you have the injection decisions included in the class itself, keeping it all
together. Moreover, when your application grows, the length of your code constructing the container might grow to such size
that maintaining it becomes a problem. This might even be the case when you have separated it with `ContainerConfigInterface` instances.
With attributes such a problem does not exist.

For now, only constructor parameters can be annotated.
Moreover, you can define attributes that modify the container. These will be discussed in the end of this chapter.

## Service Attribute
## Inject with the Service Attribute

If a parameter is annotated with the `#[Service]` attribute, you define that a service should be injected.

Expand All @@ -36,7 +37,7 @@ The _Container_ will inject `foo.service` for `$foo`. It is basically the same a
$di->params['Example']['foo'] = $di->lazyGet('foo.service');
```

## Instance Attribute
## Inject with the Instance Attribute

If the parameter is annotated with the `#[Instance]` attribute, you define a new instance of a class that should be injected.

Expand All @@ -62,7 +63,7 @@ The _Container_ will inject a new instance `Foo::class` for `$foo`. It is basica
$di->params['Example']['foo'] = $di->lazyNew(Foo::class);
```

## Value Attribute
## Inject with the Value Attribute

If a parameter is annotated with the `#[Value]` attribute, you define that a value should be injected.

Expand All @@ -88,7 +89,7 @@ The _Container_ will inject the value `foo.value` for `$foo`. It is basically th
$di->params['Example']['foo'] = $di->lazyValue('foo.value');
```

## Custom Attributes
## Inject with Custom Attributes

It is also possible to create your own custom attribute. All you have to do is create a class using [the native PHP 8.0 attribute
syntax](https://www.php.net/manual/en/language.attributes.syntax.php). On top, it has to implement the `Aura\Di\Attribute\AnnotatedInjectInterface` class.
Expand Down Expand Up @@ -169,7 +170,7 @@ It is basically the same as if you would write the following:
$di->params['Example']['foo'] = $di->lazyGetCall('config', 'get', 'foo');
```

## Overwriting attributes
## Overwriting injection attributes

When you define both an annotation and an injection via code, the injection via code has precedence over the annotation.

Expand All @@ -179,4 +180,66 @@ the `$foo` constructor parameter, and hence inject the value `"bravo"` and not `
```php
$di->set('config', new ConfigBag(['foo' => 'bar', 'alpha' => 'bravo']));
$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
[`AnnotatedResolverFactory`](config.md#containerbuilder-and-annotations). When done so, the builder will scan the
passed directories for classes and annotations. Every attribute that implements the `DefineInterface` can modify the
container.

Suppose you create a `#[Route]` attribute and define it inside a `Controller` class.

```php
#[\Attribute]
use Aura\Di\Attribute\DefineInterface;
use Aura\Di\Resolver\Resolver;
use Aura\Di\Injection\Lazy;
use Aura\Di\Injection\LazyLazy;
use Aura\Di\Injection\LazyNew;

class Route implements DefineInterface {
public function __construct(private string $method, private string $uri) {
}

public function define(Resolver $resolver, \Reflector $reflector): void
{
if ($reflector instanceof \ReflectionMethod) {
// considering the routes key is a lazy array, defined like this
// $resolver->values['routes'] = $container->lazyArray([]);
$resolver->values['routes']->append(
new RealRoute(
$this->method,
$this->uri,
handler: new LazyLazy(
$resolver,
new Lazy([
LazyNew::fromClassName($reflector->getDeclaringClass()),
$reflector->getName()
])
)
)
);
}
}
}

class Controller {
#[Route('GET', '/method1')]
public function method1() {}

#[Route('GET', '/method2')]
public function method2() {}
}

class RouterFactory {
public function __construct(
#[Value('router')]
private array $routes
) {
}
}
```

The `$routes` parameter in the RouterFactory would result in an array of `RealRoute` objects being injected.
127 changes: 125 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Container Builder and Config Classes

The _ContainerBuilder_ also 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 ContainerBuilder

The _ContainerBuilder_ also 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.

Expand All @@ -20,7 +23,11 @@ $di = $container_builder->newConfiguredInstance([
]);
```

**Note:** As with the `newInstance` method of the `ContainerBuilder`, you will have to pass `$container_builder::AUTO_RESOLVE` to `newConfiguredInstance` (as the second parameter) if you want to enable auto-resolution.
**Note:** As with the `newInstance` method of the `ContainerBuilder`, you will have to
pass `$container_builder::AUTO_RESOLVE` to `newConfiguredInstance` (as the second parameter) if you want to enable
auto-resolution.

## Container Config classes

A configuration class looks like the following:

Expand Down Expand Up @@ -102,3 +109,119 @@ the `define` and `modify` methods of each of the other ContainerConfigs.
$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.

```php
use Aura\Di\ContainerBuilder;
use Aura\Di\Resolver\AnnotatedResolverFactory;

$config_classes = [
new Aura\Router\_Config\Common(),
];

$container_builder = new ContainerBuilder();

$di = $container_builder->newCompiledInstance(
$config_classes,
ContainerBuilder::AUTO_RESOLVE
);
```

We can then serialize and unserialize the container:

```php
$serialized = serialize($di);
$di = unserialize($serialized);
```

This serialized container might be saved to a file, as cache layer before continuing. Finally, we must configure the
compiled instance.

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

$fakeService = $di->get('fake');
```

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

```php
$di->params[VendorClass::class] = [
'param' => $di->lazy(
function () {
return new VendorParamClass();
}
),
];
```

## 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
router. All routes might be compiled into a performant route dispatcher, if your routing package supports this.

When creating a compiled instance you can pass config classes that implement the `ContainerCompileInterface`. This
interface is an extension of the `ContainerConfigInterface` and has a single method `compile(Container $di): void`.
That method is executed after the `define(Container $di): void` method from the `ContainerConfigInterface` and just
before the container is compiled.

An implementation might look as follows:

```php
use Aura\Di\ContainerCompileInterface;

class RouterContainerConfig implements ContainerCompileInterface
{
public function define(Container $di): void
{
$container->set('router.factory', $container->lazyNew(MyRouterFactory::class));
}

public function compile(Container $di): void
{
$container->set('router', $container->get('router.factory')->compile());
}

public function modify(Container $di): void
{
}
}

class MyRouterFactory {
public function __construct(
#[\Aura\Di\Attribute\Value('routes')]
private array $routes
) {
}

public function compile(): Router
{
$router = new Router();
foreach ($this->routes as $route) {
$router->addRoute($route);
}
$router->compile();
return $router;
}
}
```

## ContainerBuilder and annotations

If we want to [modify the container using attributes](attributes.md#modify-the-container-using-attributes), we have to
pass the `AnnotatedResolverFactory` to the _ContainerBuilder_. The `AnnotatedResolverFactory` scans the passed directories
for classes and annotations. The classes inside the passed namespaces will be compiled.

```php
new ContainerBuilder(
new AnnotatedResolverFactory(
new ResolverFactory(), // default resolver factory
['Aura\Di\Fake'], // classes inside this namespace should be compiled
[__DIR__ . '/Fake'], // these directories should be scanned for classes and annotations
)
);
```
39 changes: 39 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,42 @@ $object = $di->newInstance('Vendor\Package\ClassName');
> N.b.: The _Container_ locks itself once a new instance is produced; this ensures that the _Container_ configuration cannot be modified once objects have been created.
However, this is a relatively naive way to create objects with the _Container_. It is better to specify the various constructor parameters, setter methods, and so on, and let the _Container_ inject those values for us only when the object is used as a dependency for something else.

## Full-featured instantiation

A full-featured container can use [attributes](attributes.md) for injection and container modification. Moreover, for
maximum performance, we would have to compile the container, serialize it and save it to a cache layer like the filesystem.
Subsequent processes unserialize the


```php
use Aura\Di\ContainerBuilder;
use Aura\Di\Resolver\AnnotatedResolverFactory;
use Aura\Di\Resolver\ResolverFactory;

$serializedContainerFile = '/var/compiled.ser';
$config_classes = [
new \MyApp\Config1,
new \MyApp\Config2,
];

if (file_exists($serializedContainerFile)) {
$di = \unserialize(file_get_contents($serializedContainerFile));
} else {
$builder = new ContainerBuilder(
new AnnotatedResolverFactory(
new ResolverFactory(), // default resolver factory
['MyApp\\'], // classes inside this namespace should be compiled
[$rootDir . '/app/src'], // these directories should be scanned for classes and annotations
)
);
$di = $builder->newCompiledInstance($config_classes);

$serialized = \serialize($di);
file_put_contents($serializedContainerFile, $serialized); // atomic for concurrency
}

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

From this point on you can call `newInstance` or `get` the container.
36 changes: 0 additions & 36 deletions docs/serializing.md

This file was deleted.

3 changes: 1 addition & 2 deletions src/Attribute/Instance.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Attribute;
use Aura\Di\Injection\LazyInterface;
use Aura\Di\Injection\LazyNew;
use Aura\Di\Resolver\Blueprint;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Instance implements AnnotatedInjectInterface
Expand All @@ -27,6 +26,6 @@ public function __construct(string $name)

public function inject(): LazyInterface
{
return new LazyNew(new Blueprint($this->name));
return LazyNew::fromClassName($this->name);
}
}
6 changes: 1 addition & 5 deletions src/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,12 @@ public function newConfiguredInstance(
* @param array $configClasses A list of ContainerConfig classes to
* instantiate and invoke for configuring the Container.
*
* @param array $extraCompileClasses A list of classes that should also be compiled, e.g. a list of classes that
* might contain container annotations.
*
* @param bool $autoResolve Use the auto-resolver?
*
* @return Container
*/
public function newCompiledInstance(
array $configClasses = [],
array $extraCompileClasses = [],
bool $autoResolve = false,
): Container {
$resolver = $this->newResolver($autoResolve);
Expand All @@ -134,7 +130,7 @@ public function newCompiledInstance(
$collection->compile($di);

$di->lock();
$resolver->compile($extraCompileClasses);
$resolver->compile();

return $di;
}
Expand Down
Loading

0 comments on commit 0a3d0e6

Please sign in to comment.