diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 29ef69ef..33f4467d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -15,9 +15,14 @@ '@Symfony' => true, '@Symfony:risky' => true, 'array_syntax' => ['syntax' => 'short'], - 'braces' => [ - 'allow_single_line_closure' => true, - ], + 'single_space_around_construct' => true, + 'control_structure_braces' => true, + 'control_structure_continuation_position' => true, + 'declare_parentheses' => true, + 'no_multiple_statements_per_line' => true, + 'braces_position' => true, + 'statement_indentation' => true, + 'no_extra_blank_lines' => true, 'concat_space' => [ 'spacing' => 'one', ], diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php index 7a7285c7..5693f4c6 100644 --- a/src/ContainerBuilder.php +++ b/src/ContainerBuilder.php @@ -52,6 +52,8 @@ class ContainerBuilder private bool $useAutowiring = true; + private int $attributesFlags = 0; + private bool $useAttributes = false; /** @@ -146,7 +148,7 @@ public function build() $this->compileToDirectory, $containerClass, $this->containerParentClass, - $this->useAutowiring + $this->useAutowiring || $this->useAttributes ); // Only load the file if it hasn't been already loaded // (the container can be created multiple times in the same process) @@ -216,11 +218,12 @@ public function useAutowiring(bool $bool) : self * * @return $this */ - public function useAttributes(bool $bool) : self + public function useAttributes(bool $bool, int $flags = 0) : self { $this->ensureNotLocked(); $this->useAttributes = $bool; + $this->attributesFlags = $flags; return $this; } diff --git a/src/Definition/AutowireDefinition.php b/src/Definition/AutowireDefinition.php index 25dc3ecb..a1f46c50 100644 --- a/src/Definition/AutowireDefinition.php +++ b/src/Definition/AutowireDefinition.php @@ -9,4 +9,21 @@ */ class AutowireDefinition extends ObjectDefinition { + protected ?bool $useAttributes; + + /** + * Enable/disable reading attributes for this definition, regardless of a container configuration. + */ + public function useAttributes(bool $flag = true) : void + { + $this->useAttributes = $flag; + } + + /** + * Returns boolean if the useAttributes flag was explicitly set, otherwise null. + */ + public function isUsingAttributes() : ?bool + { + return $this->useAttributes; + } } diff --git a/src/Definition/Helper/AutowireDefinitionHelper.php b/src/Definition/Helper/AutowireDefinitionHelper.php index 01b59e8f..77c70d85 100644 --- a/src/Definition/Helper/AutowireDefinitionHelper.php +++ b/src/Definition/Helper/AutowireDefinitionHelper.php @@ -5,6 +5,7 @@ namespace DI\Definition\Helper; use DI\Definition\AutowireDefinition; +use DI\Definition\ObjectDefinition; /** * Helps defining how to create an instance of a class using autowiring. @@ -15,6 +16,8 @@ class AutowireDefinitionHelper extends CreateDefinitionHelper { public const DEFINITION_CLASS = AutowireDefinition::class; + protected ?bool $useAttributes = true; + /** * Defines a value for a specific argument of the constructor. * @@ -69,4 +72,30 @@ public function methodParameter(string $method, string|int $parameter, mixed $va return $this; } + + /** + * Define if entry should use attributes reader for reading dependencies. + * This is turned off by default if autowire() helper is used, and turned on if entry is not defined explicitly in the di config. + * @return $this + */ + public function useAttributes(bool $useAttributes = true) : self + { + $this->useAttributes = $useAttributes; + + return $this; + } + + /** + * @return AutowireDefinition + */ + public function getDefinition(string $entryName) : ObjectDefinition + { + /** @var AutowireDefinition $definition */ + $definition = parent::getDefinition($entryName); + if ($this->useAttributes !== null) { + $definition->useAttributes($this->useAttributes); + } + + return $definition; + } } diff --git a/src/Definition/Source/AttributeBasedAutowiring.php b/src/Definition/Source/AttributeBasedAutowiring.php index a3010280..d3c90b40 100644 --- a/src/Definition/Source/AttributeBasedAutowiring.php +++ b/src/Definition/Source/AttributeBasedAutowiring.php @@ -6,6 +6,7 @@ use DI\Attribute\Inject; use DI\Attribute\Injectable; +use DI\Definition\AutowireDefinition; use DI\Definition\Exception\InvalidAttribute; use DI\Definition\ObjectDefinition; use DI\Definition\ObjectDefinition\MethodInjection; @@ -28,9 +29,25 @@ */ class AttributeBasedAutowiring implements DefinitionSource, Autowiring { - /** - * @throws InvalidAttribute - */ + // Annotations configuration flags: + // enable on implicit definitions + public const IMPLICIT = 1; + // enable on all autowire definitions (which are written in DI config) by default + public const EXPLICIT = 2; + // read @Injectable annotations for classes + public const INJECTABLE = 4; + // read @Inject annotations for properties + public const PROPERTIES = 8; + // read @Inject annotations for methods' parameters + public const METHODS = 16; + // all options enabled + public const ALL = 31; + + public function __construct(private int $flags = 0) + { + $this->flags = $flags > 0 ? $flags : self::ALL; // all flags turned on by default + } + public function autowire(string $name, ObjectDefinition $definition = null) : ObjectDefinition|null { $className = $definition ? $definition->getClassName() : $name; @@ -40,16 +57,36 @@ public function autowire(string $name, ObjectDefinition $definition = null) : Ob } $definition = $definition ?: new ObjectDefinition($name); + $useAttributes = $definition instanceof AutowireDefinition + ? ($definition->isUsingAttributes() ?? (bool) ($this->flags & self::EXPLICIT)) + : (bool) ($this->flags & self::IMPLICIT); + + $class = null; + if ($useAttributes && $this->flags >= self::INJECTABLE) { + $class = new ReflectionClass($className); - $class = new ReflectionClass($className); + $this->readInjectableAttribute($class, $definition); + if ($this->flags & self::INJECTABLE) { + $this->readInjectableAttribute($class, $definition); + } - $this->readInjectableAttribute($class, $definition); + // Browse the class properties looking for annotated properties + if ($this->flags & self::PROPERTIES) { + $this->readProperties($class, $definition); + } - // Browse the class properties looking for annotated properties - $this->readProperties($class, $definition); + // Browse the object's methods looking for annotated methods + if ($this->flags & self::METHODS) { + $this->readMethods($class, $definition); + } + } - // Browse the object's methods looking for annotated methods - $this->readMethods($class, $definition); + // constructor parameters should always be read, even if annotations are disabled (completely or i.a. for methods) + // so that it behaves at least as ReflectionBasedAutowiring + if (!$useAttributes || !($this->flags & self::METHODS)) { + $class ??= new ReflectionClass($className); + $this->readConstructor($class, $definition); + } return $definition; } @@ -72,7 +109,7 @@ public function getDefinitions() : array } /** - * Browse the class properties looking for annotated properties. + * Browse the class properties looking for properties with attributes. */ private function readProperties(ReflectionClass $class, ObjectDefinition $definition) : void { @@ -145,7 +182,7 @@ private function readProperty(ReflectionProperty $property, ObjectDefinition $de } /** - * Browse the object's methods looking for annotated methods. + * Browse the object's methods looking for methods with attributes. */ private function readMethods(ReflectionClass $class, ObjectDefinition $objectDefinition) : void { @@ -177,17 +214,17 @@ private function getMethodInjection(ReflectionMethod $method) : ?MethodInjection if ($attribute) { /** @var Inject $inject */ $inject = $attribute->newInstance(); - $annotationParameters = $inject->getParameters(); + $attributeParameters = $inject->getParameters(); } elseif ($method->isConstructor()) { // #[Inject] on constructor is implicit, we continue - $annotationParameters = []; + $attributeParameters = []; } else { return null; } $parameters = []; foreach ($method->getParameters() as $index => $parameter) { - $entryName = $this->getMethodParameter($index, $parameter, $annotationParameters); + $entryName = $this->getMethodParameter($index, $parameter, $attributeParameters); if ($entryName !== null) { $parameters[$index] = new Reference($entryName); @@ -204,7 +241,7 @@ private function getMethodInjection(ReflectionMethod $method) : ?MethodInjection /** * @return string|null Entry name or null if not found. */ - private function getMethodParameter(int $parameterIndex, ReflectionParameter $parameter, array $annotationParameters) : ?string + private function getMethodParameter(int $parameterIndex, ReflectionParameter $parameter, array $attributeParameters) : ?string { // Let's check if this parameter has an #[Inject] attribute $attribute = $parameter->getAttributes(Inject::class)[0] ?? null; @@ -216,11 +253,11 @@ private function getMethodParameter(int $parameterIndex, ReflectionParameter $pa } // #[Inject] has definition for this parameter (by index, or by name) - if (isset($annotationParameters[$parameterIndex])) { - return $annotationParameters[$parameterIndex]; + if (isset($attributeParameters[$parameterIndex])) { + return $attributeParameters[$parameterIndex]; } - if (isset($annotationParameters[$parameter->getName()])) { - return $annotationParameters[$parameter->getName()]; + if (isset($attributeParameters[$parameter->getName()])) { + return $attributeParameters[$parameter->getName()]; } // Skip optional parameters if not explicitly defined @@ -260,4 +297,26 @@ private function readInjectableAttribute(ReflectionClass $class, ObjectDefinitio $definition->setLazy($attribute->isLazy()); } } + + /** + * Browse the object's constructor parameters and inject dependencies. + */ + private function readConstructor(ReflectionClass $class, ObjectDefinition $definition) + { + if (!($constructor = $class->getConstructor()) || !$constructor->isPublic()) { + return; + } + + $parameters = []; + foreach ($constructor->getParameters() as $index => $parameter) { + $entryName = $this->getMethodParameter($index, $parameter, []); + + if ($entryName !== null) { + $parameters[$index] = new Reference($entryName); + } + } + + $constructorInjection = MethodInjection::constructor($parameters); + $definition->completeConstructorInjection($constructorInjection); + } }