Skip to content

Commit

Permalink
Merge c17949f into 91c8b9b
Browse files Browse the repository at this point in the history
  • Loading branch information
quimcalpe committed Nov 16, 2015
2 parents 91c8b9b + c17949f commit 8f9fa2e
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 9 deletions.
3 changes: 3 additions & 0 deletions change-log.md
Expand Up @@ -5,6 +5,9 @@
Improvements:

- [#272](https://github.com/PHP-DI/PHP-DI/issues/272): Support 'Class::method' syntax for callables
- [#333](https://github.com/PHP-DI/PHP-DI/pull/333): Allow injection of any container object as factory parameter via type hinting
- [#345](https://github.com/PHP-DI/PHP-DI/pull/345): Allow injection of container and current definition instance as factory parameters


Bugfixes:

Expand Down
20 changes: 13 additions & 7 deletions doc/php-definitions.md
Expand Up @@ -101,25 +101,30 @@ Here is an example using a closure:

```php
use Interop\Container\ContainerInterface;
use DI\Definition\Definition;

return [
'Foo' => function (ContainerInterface $c) {
'Foo' => function (ContainerInterface $c, Definition $d) {
// $d->getName() contains the actual requested name
return new Foo($c->get('db.host'));
},
];
```

The only parameter of a factory is the container (which can be used to retrieve other entries). You are encouraged to type-hint against the interface `Interop\Container\ContainerInterface` instead of the implementation `DI\Container`: that can be necessary in scenarios where you are using multiple containers (for example if using the PHP-DI + Symfony integration).
Instances of other classes can be injected through parameters via type-hinting (as long as they are registered within the container or autowiring is enabled).

The container, as seen above, can be injected and then used to retrieve other entries, like values, that can't be automatically injected via type -hinting. You are encouraged to type-hint against the interface `Interop\Container\ContainerInterface` instead of the implementation `DI\Container` in this case: that can be necessary in scenarios where you are using multiple containers (for example if using the PHP-DI + Symfony integration).

The current Definition instance can also be injected type-hinting the `DI\Definition\Definition` class.

You can also use a factory class - as an example, let's assume you have a simple factory class like this:

```php
class FooFactory
{
// note: $container can be omitted if not needed
public function create($container)
public function create(Bar $bar)
{
return new Foo();
return new Foo($bar);
}
}
```
Expand All @@ -135,7 +140,7 @@ return [

But the factory will be created on every request (`new FooFactory`) even if not used. Additionally with this method it's harder to pass dependencies in the factory.

The recommended solution is let the container create the factory:
The recommended solution is to let the container create the factory:

```php
return [
Expand All @@ -155,7 +160,8 @@ This configuration is equivalent to the following code:

```php
$factory = $container->get(FooFactory::class);
return $factory->create();
$bar = $container->get(Bar::class);
return $factory->create($bar);
```

Please note:
Expand Down
20 changes: 18 additions & 2 deletions src/DI/Definition/Resolver/FactoryResolver.php
Expand Up @@ -5,10 +5,14 @@
use DI\Definition\Definition;
use DI\Definition\Exception\DefinitionException;
use DI\Definition\FactoryDefinition;
use DI\Invoker\FactoryParameterResolver;
use Interop\Container\ContainerInterface;
use Invoker\Exception\NotCallableException;
use Invoker\Exception\NotEnoughParametersException;
use Invoker\Invoker;
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
use Invoker\ParameterResolver\NumericArrayResolver;
use Invoker\ParameterResolver\ResolverChain;

/**
* Resolves a factory definition to a value.
Expand Down Expand Up @@ -51,17 +55,29 @@ public function __construct(ContainerInterface $container)
public function resolve(Definition $definition, array $parameters = [])
{
if (! $this->invoker) {
$this->invoker = new Invoker(new NumericArrayResolver, $this->container);
$parameterResolver = new ResolverChain([
new FactoryParameterResolver,
new TypeHintContainerResolver($this->container),
new NumericArrayResolver,
]);

$this->invoker = new Invoker($parameterResolver, $this->container);
}

try {
return $this->invoker->call($definition->getCallable(), [$this->container]);
return $this->invoker->call($definition->getCallable(), [$this->container, $definition]);
} catch (NotCallableException $e) {
throw new DefinitionException(sprintf(
'Entry "%s" cannot be resolved: factory %s',
$definition->getName(),
$e->getMessage()
));
} catch (NotEnoughParametersException $e) {
throw new DefinitionException(sprintf(
'Entry "%s" cannot be resolved: %s',
$definition->getName(),
$e->getMessage()
));
}
}

Expand Down
39 changes: 39 additions & 0 deletions src/DI/Invoker/FactoryParameterResolver.php
@@ -0,0 +1,39 @@
<?php

namespace DI\Invoker;

use DI\Definition\Definition;
use Interop\Container\ContainerInterface;
use Invoker\ParameterResolver\ParameterResolver;
use ReflectionFunctionAbstract;

/**
* Inject container and definition entries if closure typehinted them.
*
* @author Quim Calpe <quim@kalpe.com>
*/
class FactoryParameterResolver implements ParameterResolver
{
public function getParameters(
ReflectionFunctionAbstract $reflection,
array $providedParameters,
array $resolvedParameters
) {
$parameters = $reflection->getParameters();

foreach ($parameters as $index => $parameter) {
$parameterClass = $parameter->getClass();

if ($parameterClass) {
if ('Interop\Container\ContainerInterface' == $parameterClass->name && isset($providedParameters[0]) && $providedParameters[0] instanceof ContainerInterface) {
$resolvedParameters[$index] = $providedParameters[0];
}
if ('DI\Definition\Definition' == $parameterClass->name && isset($providedParameters[1]) && $providedParameters[1] instanceof Definition) {
$resolvedParameters[$index] = $providedParameters[1];
}
}
}

return $resolvedParameters;
}
}
69 changes: 69 additions & 0 deletions tests/IntegrationTest/Definitions/FactoryDefinitionTest.php
Expand Up @@ -3,6 +3,8 @@
namespace DI\Test\IntegrationTest\Definitions;

use DI\ContainerBuilder;
use DI\Definition\Definition;
use Interop\Container\ContainerInterface;

/**
* Test factory definitions.
Expand Down Expand Up @@ -80,6 +82,73 @@ public function test_named_invokable_container_entry_as_factory()
$this->assertSame('bar', $container->get('factory'));
}

public function test_container_gets_injected_as_first_argument_without_typehint()
{
$container = $this->createContainer([
'factory' => function ($c) {
return $c;
},
]);

$factory = $container->get('factory');

$this->assertInstanceOf('Interop\Container\ContainerInterface', $factory);
}

public function test_definition_gets_injected_as_second_argument_without_typehint()
{
$container = $this->createContainer([
'factory' => function ($c, $d) {
return $d;
},
]);

$factory = $container->get('factory');

$this->assertInstanceOf('DI\Definition\Definition', $factory);
}

public function test_definition_gets_injected_with_typehint()
{
$container = $this->createContainer([
'factory' => function (Definition $d) {
return $d;
},
]);

$factory = $container->get('factory');

$this->assertInstanceOf('DI\Definition\Definition', $factory);
}

public function test_arbitrary_object_gets_injected_via_typehint()
{
$container = $this->createContainer([
'factory' => function (\stdClass $stdClass) {
return $stdClass;
},
]);

$factory = $container->get('factory');

$this->assertInstanceOf('stdClass', $factory);
}

public function test_container_and_definition_gets_injected_in_arbitrary_position_via_typehint()
{
$container = $this->createContainer([
'factory' => function (\stdClass $stdClass, Definition $d, ContainerInterface $c) {
return [$stdClass, $d, $c];
},
]);

$factory = $container->get('factory');

$this->assertInstanceOf('stdClass', $factory[0]);
$this->assertInstanceOf('DI\Definition\Definition', $factory[1]);
$this->assertInstanceOf('Interop\Container\ContainerInterface', $factory[2]);
}

/**
* @expectedException \DI\Definition\Exception\DefinitionException
* @expectedExceptionMessage Entry "foo" cannot be resolved: factory "Hello World" is neither a callable nor a valid container entry
Expand Down
@@ -0,0 +1,94 @@
<?php

namespace DI\Test\UnitTest\Definition\Resolver;

use DI\Definition\Definition;
use DI\Invoker\FactoryParameterResolver;
use EasyMock\EasyMock;
use Interop\Container\ContainerInterface;

/**
* @covers \DI\Invoker\FactoryParameterResolver
*/
class FactoryParameterResolverTest extends \PHPUnit_Framework_TestCase
{
use EasyMock;

/**
* @var FactoryParameterResolver
*/
private $resolver;

/**
* @var ContainerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
private $container;

/**
* @var Definition|\PHPUnit_Framework_MockObject_MockObject
*/
private $definition;

public function setUp()
{
$this->resolver = new FactoryParameterResolver;
$this->container = $this->easyMock('Interop\Container\ContainerInterface');
$this->definition = $this->easyMock('DI\Definition\Definition');
}

/**
* @test
*/
public function should_resolve_container_and_definition()
{
$callable = function (ContainerInterface $c, Definition $d) {};
$reflection = new \ReflectionFunction($callable);

$parameters = $this->resolver->getParameters($reflection, [$this->container, $this->definition], []);

$this->assertCount(2, $parameters);
$this->assertSame($this->container, $parameters[0]);
$this->assertSame($this->definition, $parameters[1]);
}

/**
* @test
*/
public function should_resolve_only_container()
{
$callable = function (ContainerInterface $c) {};
$reflection = new \ReflectionFunction($callable);

$parameters = $this->resolver->getParameters($reflection, [$this->container, $this->definition], []);

$this->assertCount(1, $parameters);
$this->assertSame($this->container, $parameters[0]);
}

/**
* @test
*/
public function should_resolve_only_definition()
{
$callable = function (Definition $d) {};
$reflection = new \ReflectionFunction($callable);

$parameters = $this->resolver->getParameters($reflection, [$this->container, $this->definition], []);

$this->assertCount(1, $parameters);
$this->assertSame($this->definition, $parameters[0]);
}

/**
* @test
*/
public function should_resolve_nothing()
{
$callable = function () {};
$reflection = new \ReflectionFunction($callable);

$parameters = $this->resolver->getParameters($reflection, [$this->container, $this->definition], []);

$this->assertCount(0, $parameters);
}
}

0 comments on commit 8f9fa2e

Please sign in to comment.