8000 Inject via PHP 8 attributes by mnapoli · Pull Request #738 · PHP-DI/PHP-DI · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Inject via PHP 8 attributes #738

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

Merged
merged 1 commit into from
Oct 5, 2020
Merged
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
3 changes: 3 additions & 0 deletions couscous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ menu:
php-definitions:
text: PHP definitions
url: doc/php-definitions.html
attributes:
text: PHP 8 attributes
url: doc/attributes.html
annotations:
text: Annotations
url: doc/annotations.html
Expand Down
4 changes: 4 additions & 0 deletions doc/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ current_menu: annotations

# Annotations

**Since PHP 8, annotations are deprecated in favor of [PHP attributes](attributes.md).**

---

On top of [autowiring](autowiring.md) and [PHP configuration files](php-definitions.md), you can define injections using annotations.

Using annotations do not affect performances when [compiling the container](performances.md).
Expand Down
127 changes: 127 additions & 0 deletions doc/attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
layout: documentation
current_menu: attributes
---

# Attributes

On top of [autowiring](autowiring.md) and [PHP configuration files](php-definitions.md), you can define injections using PHP 8 attributes.

Using attributes do not affect performances when [compiling the container](performances.md). For a non-compiled container, the PHP reflection is used but the overhead is minimal.

## Setup

Enable attributes [via the `ContainerBuilder`](container-configuration.md):

```php
$containerBuilder->useAttributes(true);
```

## Inject

`#[Inject]` lets you define where PHP-DI should inject something, and optionally what it should inject.

It can be used on:

- the constructor (constructor injection)
- methods (setter/method injection)
- properties (property injection)

*Note: property injections occur after the constructor is executed, so any injectable property will be null inside `__construct`.*

**Note: `#[Inject]` ignores types declared in phpdoc. Only types specified in PHP code are considered.**

Here is an example of all possible uses of the `#[Inject]` attribute:

```php
use DI\Attribute\Inject;

class Example
{
/**
* Attribute combined with a type on the property:
*/
#[Inject]
private Foo $property1;

/**
* Explicit definition of the entry to inject:
*/
#[Inject('db.host')]
private $property2;

/**
* Alternative to the above:
*/
#[Inject(name: 'db.host')]
private $property3;

/**
* Attribute specifying exactly what to inject on the constructor:
*/
#[Inject(['db.host', 'db.name'])]
public function __construct($param1, $param2)
{
}

/**
* Attribute combined with PHP types:
*/
#[Inject]
public function method1(Foo $param)
{
}

/**
* Explicit definition of the entries to inject:
*/
#[Inject(['db.host', 'db.name'])]
public function method2($param1, $param2)
{
}

/**
* Explicit definition of parameters by their name
* (types are used for the other parameters):
*/
#[Inject(['param2' => 'db.host'])]
public function method3(Foo $param1, $param2)
{
}
}
```

*Note: remember to import the attribute class via `use DI\Attribute\Inject;`.*

### Troubleshooting attributes

- remember to import the attribute class via `use DI\Attribute\Inject;`
- `#[Inject]` is not meant to be used on the method to call with [`Container::call()`](container.md#call) (it will be ignored)
- `#[Inject]` ignores types declared in phpdoc. Only types specified in PHP code are considered.

Note that `#[Inject]` is implicit on all constructors (because constructors must be called to create an object).

## Injectable

The `#[Injectable]` attribute lets you set options on injectable classes:

```php
use DI\Attribute\Injectable;

#[Injectable(lazy: true)]
class Example
{
}
```

**The `#[Injectable]` attribute is optional: by default, all classes are injectable.**

## Limitations

There are things that can't be defined with attributes:

- values (instead of classes)
- mapping interfaces to implementations
- defining entries with an anonymous function

For that, you can combine attributes with [definitions in PHP](php-definitions.md).
12 changes: 6 additions & 6 deletions doc/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ This is the solution we recommend.
Example:

```php


class UserController
{
/**
* @Inject
*/
#[Inject]
private FormFactoryInterface $formFactory;

public function createForm($type, $data, $options)
Expand All @@ -67,7 +67,7 @@ Property injection is generally frowned upon, and for good reasons:

- injecting in a private property breaks encapsulation
- it is not an explicit dependency: there is no contract saying your class need the property to be set to work
- if you use PHP-DI's annotations to mark the dependency to be injected, your class is dependent on the container (see the 2nd rule above)
- if you use PHP-DI's attributes to mark the dependency to be injected, your class is dependent on the container (see the 2nd rule above)

BUT

Expand All @@ -82,13 +82,13 @@ So:
(because most dependencies like Request, Response, templating system, etc. will have changed)

This solution offers many benefits for no major drawback, so
**we recommend using annotations in controllers**.
**we recommend using attributes in controllers**.


## Writing services

Given a service is intended to be reused, tested and independent of your framework, **we do not recommend
using annotations for injecting dependencies**.
using attributes for injecting dependencies**.

Instead, we recommend using **constructor injection and autowiring**:

Expand Down
76 changes: 76 additions & 0 deletions src/Attribute/Inject.php
10000
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace DI\Attribute;

use Attribute;
use DI\Definition\Exception\InvalidAnnotation;

/**
* #[Inject] attribute.
*
* Marks a property or method as an injection point
*
* @api
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class Inject
{
/**
* Entry name.
*/
private ?string $name = null;

/**
* Parameters, indexed by the parameter number (index) or name.
*
* Used if the annotation is set on a method
*/
private array $parameters = [];

/**
* @param string|array|null $name
*
* @throws InvalidAnnotation
*/
public function __construct($name = null)
{
// #[Inject('foo')] or #[Inject(name: 'foo')]
if (is_string($name)) {
$this->name = $name;
}

// #[Inject([...])] on a method
if (is_array($name)) {
foreach ($name as $key => $value) {
if (! is_string($value)) {
throw new InvalidAnnotation(sprintf(
"#[Inject(['param' => 'value'])] expects \"value\" to be a string, %s given.",
json_encode($value, JSON_THROW_ON_ERROR)
));
}

$this->parameters[$key] = $value;
}
}
}

/**
* @return string|null Name of the entry to inject
*/
public function getName() : ?string
{
return $this->name;
}

/**
* @return array Parameters, indexed by the parameter number (index) or name
*/
public function getParameters() : array
{
return $this->parameters;
}
}
38 changes: 38 additions & 0 deletions src/Attribute/Injectable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace DI\Attribute;

use Attribute;

/**
* "Injectable" annotation.
*
* Marks a class as injectable
*
* @api
*
* @author Domenic Muskulus <domenic@muskulus.eu>
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class Injectable
{
/**
* Should the object be lazy-loaded.
*/
private ?bool $lazy = null;

public function __construct(array $values)
{
if (isset($values['lazy'])) {
$this->lazy = (bool) $values['lazy'];
}
}

public function isLazy() : ?bool
{
return $this->lazy;
}
}
24 changes: 23 additions & 1 deletion src/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use DI\Compiler\Compiler;
use DI\Definition\Source\AnnotationBasedAutowiring;
use DI\Definition\Source\AttributeBasedAutowiring;
use DI\Definition\Source\DefinitionArray;
use DI\Definition\Source\DefinitionFile;
use DI\Definition\Source\DefinitionSource;
Expand Down Expand Up @@ -48,6 +49,8 @@ class ContainerBuilder

private bool $useAnnotations = false;

private bool $useAttributes = false;

/**
* If true, write the proxies to disk to improve performances.
*/
Expand Down Expand Up @@ -104,7 +107,10 @@ public function build()
{
$sources = array_reverse($this->definitionSources);
10000
if ($this->useAnnotations) {
if ($this->useAttributes) {
$autowiring = new AttributeBasedAutowiring;
$sources[] = $autowiring;
} elseif ($this->useAnnotations) {
$autowiring = new AnnotationBasedAutowiring;
$sources[] = $autowiring;
} elseif ($this->useAutowiring) {
Expand Down Expand Up @@ -228,6 +234,22 @@ public function useAnnotations(bool $bool) : self
return $this;
}

/**
* Enable or disable the use of PHP 8 attributes to configure injections.
*
* Disabled by default.
*
* @return $this
*/
public function useAttributes(bool $bool) : self
{
$this->ensureNotLocked();

$this->useAttributes = $bool;

return $this;
}

/**
* Configure the proxy generation.
*
Expand Down
1 change: 0 additions & 1 deletion src/Definition/Helper/CreateDefinitionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace DI\Definition\Helper;

use DI\Definition\Definition;
use DI\Definition\Exception\InvalidDefinition;
use DI\Definition\ObjectDefinition;
use DI\Definition\ObjectDefinition\MethodInjection;
Expand Down
1 change: 1 addition & 0 deletions src/Definition/Resolver/ObjectCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ private function createProxy(ObjectDefinition $definition, array $parameters) :
function (& $wrappedObject, $proxy, $method, $params, & $initializer) use ($definition, $parameters) {
$wrappedObject = $this->createInstance($definition, $parameters);
$initializer = null; // turning off further lazy initialization

return true;
}
);
Expand Down
Loading
0