Skip to content

Commit

Permalink
Fixes #409 Remove scopes
Browse files Browse the repository at this point in the history
The goal is to simplify the package for the v6. Scopes are useless: I don't see a valid use case for the "prototype" scope, if needed then it should be replaced by an actual factory.

By removing scopes the API would be simpler as well as the internals of PHP-DI.
  • Loading branch information
mnapoli committed Jun 3, 2017
1 parent 00dee13 commit 4c2a75f
Show file tree
Hide file tree
Showing 61 changed files with 113 additions and 744 deletions.
1 change: 1 addition & 0 deletions change-log.md
Expand Up @@ -17,6 +17,7 @@ BC breaks:

- PHP 7 or greater is required
- `DI\object()` has been removed, use `DI\create()` or `DI\autowire()` instead
- [#409](https://github.com/PHP-DI/PHP-DI/issues/409): Scopes are removed (by [@mnapoli](https://github.com/mnapoli))
- The deprecated `DI\link()` helper was removed, used `DI\get()` instead
- [#484](https://github.com/PHP-DI/PHP-DI/pull/484) The deprecated `\DI\Debug` class has been removed. Definitions can be cast to string directly
- The cache system has been moved from Doctrine Cache to PSR-16 (the simple cache standard)
Expand Down
3 changes: 0 additions & 3 deletions couscous.yml
Expand Up @@ -82,9 +82,6 @@ menu:
performances:
text: Performances
url: doc/performances.html
scopes:
text: Scopes
url: doc/scopes.html
lazy-injection:
text: Lazy injection
url: doc/lazy-injection.html
Expand Down
1 change: 0 additions & 1 deletion doc/README.md
Expand Up @@ -34,7 +34,6 @@ title: Documentation index
### Advanced topics

* [Performances](performances.md)
* [Scopes](scopes.md)
* [Lazy injection](lazy-injection.md)
* [Inject on an existing instance](inject-on-instance.md)
* [Injections depending on the environment](environments.md)
Expand Down
2 changes: 1 addition & 1 deletion doc/annotations.md
Expand Up @@ -108,7 +108,7 @@ The `@Injectable` annotation lets you set options on injectable classes:

```php
/**
* @Injectable(scope="prototype", lazy=true)
* @Injectable(lazy=true)
*/
class Example
{
Expand Down
4 changes: 4 additions & 0 deletions doc/migration/6.0.md
Expand Up @@ -37,6 +37,10 @@ If you have multiple configuration files, for example if you have built a module

The `DI\link()` function helper was deprecated in 5.0. It is now completely removed. Use `DI\get()` instead.

### Scopes

Scopes have been removed as they are out of the scope of a container. Read more details and alternatives in the [scopes](../scopes.md) documentation.

## Caching

PHP-DI 5 was using the Doctrine Cache library for caching definitions. Since then, PSR-6 and PSR-16 have standardized caching in PHP.
Expand Down
9 changes: 1 addition & 8 deletions doc/php-definitions.md
Expand Up @@ -278,14 +278,7 @@ return [
];
```

By default each entry will be created once and the same instance will be injected everywhere it is used (singleton instance). You can use the "prototype" [scope](scopes.md) if you want a new instance to be created every time it is injected:

```php
return [
'FormBuilder' => DI\create()
->scope(Scope::PROTOTYPE),
];
```
Each entry will be resolved once and the same instance will be injected everywhere it is used.

### Autowired objects

Expand Down
125 changes: 53 additions & 72 deletions doc/scopes.md
Expand Up @@ -5,99 +5,80 @@ current_menu: scopes

# Scopes

By default, PHP-DI will inject **the same instance of an object** everywhere.
**Scopes have been removed in PHP-DI 6.** Read below for more explanations.

```
class UserManager
{
public function __construct(Database $db) {
// ...
}
}
Scopes were used to make the container work as a factory: instead of using scopes you should rather inject a proper factory and create the objects you need on demand.

Before:

```php
return [
Form::class => create()
->scope(Scope::PROTOTYPE), // a new form is created everytime it is injected
];

class ArticleManager
class Service
{
public function __construct(Database $db) {
// ...
public function __construct(Form $form)
{
$this->form = $form;
}
}
```

In the example above the same `Database` object will be injected if both `UserManager` and `ArticleManager` are instantiated (or injected).

The same rule apply when getting twice the same entry from the container:
After:

```php
$object1 = $container->get('Database');
$object2 = $container->get('Database');
return [
FormFactory::class => create(),
];

// $object1 === $object2
class Service
{
public function __construct(FormFactory $formFactory)
{
$this->form = $formFactory->createForm();
}
}
```

The reason for this behavior is:

- we sometimes need to ensure that only one instance of a class exist (because for example we want to share the same database connection between classes)
- more generally there is no reason to create a new instance every time we want to use a class

If injectable classes (aka services) are correctly designed, they should be [**stateless**](https://igor.io/2013/03/31/stateless-services.html). That means that reusing the same instance in several places of the application doesn't have any side effect.

This behavior is however configurable using **scopes**.

## Available scopes

Container entries can have the following scopes:

- **singleton** (applied by default for every container entry)

The object instance is unique (shared) during the container's lifecycle - each injection by the container or explicit call of `get()` returns the same instance.

Please note that while this scope is named "singleton", it is not related to [the Singleton design pattern](http://en.wikipedia.org/wiki/Singleton_pattern).

- **prototype**
or:

The object instance is not unique - each injection or call of the container's `get()` method returns a new instance.

## Configuring the scope

Scopes are part of the [definitions](definition.md) of injections, so you can define them using annotations or PHP configuration.

### PHP configuration
```php
class Service
{
public function __construct()
{
$this->form = new Form(/* parameters */);
}
}
```

You can specify the scope by using the `scope` method:
or you can also inject the container and use it explicitly as a factory (type-hint against `DI\FactoryInterface` to avoid being coupled to the container):

```php
return [
// A new object will be created every time it is used
'MyClass1' => DI\create()
->scope(Scope::PROTOTYPE),

// The closure will be called every time MyClass2 is used (and return a new object every time)
'MyClass2' => DI\factory(function () {
return new MyClass2();
})->scope(Scope::PROTOTYPE),
];
class Service
{
public function __construct(\DI\FactoryInterface $factory)
{
$this->form = $factory->make(Form::class, /* parameters */);
}
}
```

Remember singleton is the default scope. The only exception is when aliasing an entry to another:
Scopes also created an illusion that some values could be recalculated on demand. For example you could imagine a factory that returns the current value of an environment variable:

```php
return [
'Foo' => DI\get('Bar'),
'config' => factory(function () {
return getenv('CONFIG_VAR');
})->scope(Scope::PROTOTYPE),

Service1::class => create()
->constructor(get('config')),
Service2::class => create()
->constructor(get('config')),
];
```

The alias will be cached, instead it will be resolved every time because the target entry (`Bar`) could have the "prototype" scope.

### Annotation

You can specify the scope by using the `@Injectable` annotation on the target class. Remember singleton is the default scope.

```php
/**
* @Injectable(scope="prototype")
*/
class MyService
{
// ...
}
```
Contrary to what one could think, if the `CONFIG_VAR` changes it will not be updated in places were it has already been injected before the change. Scopes are not a solution for values that can change during execution, yet they could be misinterpreted as such a solution.
26 changes: 0 additions & 26 deletions src/DI/Annotation/Injectable.php
Expand Up @@ -2,9 +2,6 @@

namespace DI\Annotation;

use DI\Scope;
use UnexpectedValueException;

/**
* "Injectable" annotation.
*
Expand All @@ -18,12 +15,6 @@
*/
final class Injectable
{
/**
* The scope of an class: prototype, singleton.
* @var string|null
*/
private $scope;

/**
* Should the object be lazy-loaded.
* @var bool|null
Expand All @@ -32,28 +23,11 @@ final class Injectable

public function __construct(array $values)
{
if (isset($values['scope'])) {
if ($values['scope'] === 'prototype') {
$this->scope = Scope::PROTOTYPE;
} elseif ($values['scope'] === 'singleton') {
$this->scope = Scope::SINGLETON;
} else {
throw new UnexpectedValueException(sprintf("Value '%s' is not a valid scope", $values['scope']));
}
}
if (isset($values['lazy'])) {
$this->lazy = (bool) $values['lazy'];
}
}

/**
* @return string|null
*/
public function getScope()
{
return $this->scope;
}

/**
* @return bool|null
*/
Expand Down
42 changes: 19 additions & 23 deletions src/DI/Container.php
Expand Up @@ -35,10 +35,10 @@
class Container implements ContainerInterface, FactoryInterface, \DI\InvokerInterface
{
/**
* Map of entries with Singleton scope that are already resolved.
* Map of entries that are already resolved.
* @var array
*/
private $singletonEntries = [];
private $resolvedEntries = [];

/**
* @var DefinitionSource
Expand Down Expand Up @@ -90,10 +90,10 @@ public function __construct(
$this->definitionResolver = new ResolverDispatcher($this->wrapperContainer, $proxyFactory);

// Auto-register the container
$this->singletonEntries[self::class] = $this;
$this->singletonEntries[FactoryInterface::class] = $this;
$this->singletonEntries[InvokerInterface::class] = $this;
$this->singletonEntries[ContainerInterface::class] = $this;
$this->resolvedEntries[self::class] = $this;
$this->resolvedEntries[FactoryInterface::class] = $this;
$this->resolvedEntries[InvokerInterface::class] = $this;
$this->resolvedEntries[ContainerInterface::class] = $this;
}

/**
Expand All @@ -108,9 +108,9 @@ public function __construct(
*/
public function get($name)
{
// Try to find the entry in the singleton map
if (isset($this->singletonEntries[$name]) || array_key_exists($name, $this->singletonEntries)) {
return $this->singletonEntries[$name];
// If the entry is already resolved we return it
if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) {
return $this->resolvedEntries[$name];
}

$definition = $this->definitionSource->getDefinition($name);
Expand All @@ -120,20 +120,16 @@ public function get($name)

$value = $this->resolveDefinition($definition);

// If the entry is singleton, we store it to always return it without recomputing it
if ($definition->getScope() === Scope::SINGLETON) {
$this->singletonEntries[$name] = $value;
}
$this->resolvedEntries[$name] = $value;

return $value;
}

/**
* Build an entry of the container by its name.
*
* This method behave like get() except it forces the scope to "prototype",
* which means the definition of the entry will be re-evaluated each time.
* For example, if the entry is a class, then a new instance will be created each time.
* This method behave like get() except resolves the entry again every time.
* For example if the entry is a class then a new instance will be created each time.
*
* This method makes the container behave like a factory.
*
Expand All @@ -158,9 +154,9 @@ public function make($name, array $parameters = [])

$definition = $this->definitionSource->getDefinition($name);
if (! $definition) {
// Try to find the entry in the singleton map
if (array_key_exists($name, $this->singletonEntries)) {
return $this->singletonEntries[$name];
// If the entry is already resolved we return it
if (array_key_exists($name, $this->resolvedEntries)) {
return $this->resolvedEntries[$name];
}

throw new NotFoundException("No entry or class found for '$name'");
Expand All @@ -186,7 +182,7 @@ public function has($name)
));
}

if (array_key_exists($name, $this->singletonEntries)) {
if (array_key_exists($name, $this->resolvedEntries)) {
return true;
}

Expand Down Expand Up @@ -254,7 +250,7 @@ public function set(string $name, $value)
if ($value instanceof Definition) {
$this->setDefinition($name, $value);
} else {
$this->singletonEntries[$name] = $value;
$this->resolvedEntries[$name] = $value;
}
}

Expand Down Expand Up @@ -301,8 +297,8 @@ private function setDefinition(string $name, Definition $definition)
}

// Clear existing entry if it exists
if (array_key_exists($name, $this->singletonEntries)) {
unset($this->singletonEntries[$name]);
if (array_key_exists($name, $this->resolvedEntries)) {
unset($this->resolvedEntries[$name]);
}

$this->definitionSource->addDefinition($definition);
Expand Down
6 changes: 0 additions & 6 deletions src/DI/Definition/AliasDefinition.php
Expand Up @@ -2,7 +2,6 @@

namespace DI\Definition;

use DI\Scope;
use Psr\Container\ContainerInterface;

/**
Expand Down Expand Up @@ -39,11 +38,6 @@ public function getName() : string
return $this->name;
}

public function getScope() : string
{
return Scope::PROTOTYPE;
}

public function getTargetEntryName() : string
{
return $this->targetEntryName;
Expand Down

0 comments on commit 4c2a75f

Please sign in to comment.