diff --git a/docs/attributes.md b/docs/attributes.md index a6e3ea1..5a03b01 100644 --- a/docs/attributes.md +++ b/docs/attributes.md @@ -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. @@ -282,4 +282,54 @@ class SymfonyRouteAttributeConfig implements AttributeConfigInterface } } } -``` \ No newline at end of file +``` + +## 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). \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index 5e1a0f7..cbe4636 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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: @@ -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, ]); ``` @@ -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, ]); ``` @@ -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; @@ -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] = [ @@ -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. @@ -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 @@ -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 diff --git a/src/Attribute/Blueprint.php b/src/Attribute/Blueprint.php new file mode 100644 index 0000000..c5d077e --- /dev/null +++ b/src/Attribute/Blueprint.php @@ -0,0 +1,18 @@ +getAttributeInstance(); + if ($specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) { + $di->params[$specification->getClassName()][$specification->getTargetConfig()['parameter']] = $attribute->inject(); + } + } +} \ No newline at end of file diff --git a/src/ClassScanner/BlueprintAttributeConfig.php b/src/ClassScanner/BlueprintAttributeConfig.php new file mode 100644 index 0000000..0626a5d --- /dev/null +++ b/src/ClassScanner/BlueprintAttributeConfig.php @@ -0,0 +1,21 @@ +params[$specification->getClassName()] = $di->params[$specification->getClassName()] ?? []; + } +} \ No newline at end of file diff --git a/src/ClassScanner/ClassScannerConfig.php b/src/ClassScanner/ClassScannerConfig.php index 38a2f38..d22577e 100644 --- a/src/ClassScanner/ClassScannerConfig.php +++ b/src/ClassScanner/ClassScannerConfig.php @@ -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; @@ -36,23 +38,27 @@ 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] ?? []; } @@ -60,13 +66,24 @@ public function define(Container $di): void 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, + ); + } + } } } } diff --git a/tests/ClassScanner/ClassScannerConfigTest.php b/tests/ClassScanner/ClassScannerConfigTest.php index 1a206e4..731fb02 100644 --- a/tests/ClassScanner/ClassScannerConfigTest.php +++ b/tests/ClassScanner/ClassScannerConfigTest.php @@ -2,6 +2,7 @@ namespace Aura\Di\ClassScanner; use Aura\Di\Container; +use Aura\Di\Fake\FakeControllerClass; use Aura\Di\Fake\FakeInjectAnnotatedWithClass; use Aura\Di\Resolver\Reflector; use Aura\Di\Resolver\Resolver; @@ -38,4 +39,13 @@ public function testAttributes() $this->assertCount(1, $injectedWith->getWorkers()); $this->assertSame($annotation, $injectedWith->getWorkers()[0]); } + + public function testBlueprintApplied() + { + $resolver = new Resolver(new Reflector()); + $container = new Container($resolver); + $this->config->define($container); + + $this->assertTrue(\array_key_exists(FakeControllerClass::class, $resolver->params)); + } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 338cc77..c9bebc9 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -190,7 +190,7 @@ public function testLazyLazyParams() $callable = $this->container->lazyLazy( $this->container->lazyCallable([ $this->container->lazyNew('Aura\Di\Fake\FakeControllerClass', [ - 'foo' => 'bar' + 'foo' => new FakeOtherClass() ]), 'process' ]) diff --git a/tests/Fake/FakeAllAttributes.php b/tests/Fake/FakeAllAttributes.php index 81c2bf1..11651df 100644 --- a/tests/Fake/FakeAllAttributes.php +++ b/tests/Fake/FakeAllAttributes.php @@ -12,7 +12,9 @@ class FakeAllAttributes public function __construct( #[FakeAttribute(4)] - $parameter + $parameter, + #[FakeAttribute(4)] + public $promotedProperty ) { } diff --git a/tests/Fake/FakeConstructAttributeClass.php b/tests/Fake/FakeConstructAttributeClass.php index 46c6413..8926b6f 100644 --- a/tests/Fake/FakeConstructAttributeClass.php +++ b/tests/Fake/FakeConstructAttributeClass.php @@ -10,7 +10,6 @@ class FakeConstructAttributeClass { private FakeInterface $fakeService; private FakeInvokableClass $fakeServiceGet; - private FakeInterface $fakeInstance; private string $string; public function __construct( @@ -19,13 +18,12 @@ public function __construct( #[Service('fake.service', 'getFoo')] FakeInvokableClass $fakeServiceGet, #[Instance(FakeInterfaceClass2::class)] - FakeInterface $fakeInstance, + private FakeInterface $fakeInstance, #[Value('fake.value')] string $string, ) { $this->fakeService = $fakeService; $this->fakeServiceGet = $fakeServiceGet; - $this->fakeInstance = $fakeInstance; $this->string = $string; } diff --git a/tests/Fake/FakeControllerClass.php b/tests/Fake/FakeControllerClass.php index 5485d6c..914033b 100644 --- a/tests/Fake/FakeControllerClass.php +++ b/tests/Fake/FakeControllerClass.php @@ -1,11 +1,14 @@ foo = $foo; } diff --git a/tests/Fake/FakeWorkerAttribute.php b/tests/Fake/FakeWorkerAttribute.php index e74e819..1379573 100644 --- a/tests/Fake/FakeWorkerAttribute.php +++ b/tests/Fake/FakeWorkerAttribute.php @@ -2,14 +2,14 @@ namespace Aura\Di\Fake; use Aura\Di\Attribute\AttributeConfigFor; -use Aura\Di\Attribute\CompileNamespace; +use Aura\Di\Attribute\BlueprintNamespace; use Aura\Di\ClassScanner\AttributeConfigInterface; use Aura\Di\ClassScanner\AttributeSpecification; use Aura\Di\Container; #[\Attribute] #[AttributeConfigFor(FakeWorkerAttribute::class)] -#[CompileNamespace(__NAMESPACE__)] +#[BlueprintNamespace(__NAMESPACE__)] class FakeWorkerAttribute implements AttributeConfigInterface { private int $someSetting; diff --git a/tests/Resolver/ReflectorTest.php b/tests/Resolver/ReflectorTest.php index 2ba09bf..ecfb084 100644 --- a/tests/Resolver/ReflectorTest.php +++ b/tests/Resolver/ReflectorTest.php @@ -14,12 +14,12 @@ public function testYieldAttributes() /** @var array $attributes */ $attributes = [...$reflector->yieldAttributes(FakeAllAttributes::class)]; - $this->assertCount(5, $attributes); + $this->assertCount(7, $attributes); foreach ($attributes as $attribute) { $instance = $attribute->getAttributeInstance(); match ($attribute->getAttributeTarget()) { \Attribute::TARGET_CLASS => $this->assertSame(1, $instance->getValue()), - \Attribute::TARGET_PROPERTY => $this->assertSame(2, $instance->getValue()), + \Attribute::TARGET_PROPERTY => $this->assertContains($instance->getValue(), [2, 4]), \Attribute::TARGET_CLASS_CONSTANT => $this->assertSame(3, $instance->getValue()), \Attribute::TARGET_PARAMETER => $this->assertSame(4, $instance->getValue()), \Attribute::TARGET_METHOD => $this->assertSame(5, $instance->getValue()),