Skip to content

[php-di v7] AnnotationBasedAutowiring with additional options for performance tweaks #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: mod7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down
7 changes: 5 additions & 2 deletions src/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ContainerBuilder

private bool $useAutowiring = true;

private int $attributesFlags = 0;

private bool $useAttributes = false;

/**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions src/Definition/AutowireDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
29 changes: 29 additions & 0 deletions src/Definition/Helper/AutowireDefinitionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
}
}
97 changes: 78 additions & 19 deletions src/Definition/Source/AttributeBasedAutowiring.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}