Completed
Push — master ( f79950...4e1fd0 )
by Matthieu
12s
created

FactoryResolver::resolveExtraParams()   A

Complexity

Conditions 3
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 13
Code Lines 7

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DI\Definition\Resolver;
6
7
use DI\Definition\Definition;
8
use DI\Definition\Exception\InvalidDefinition;
9
use DI\Definition\FactoryDefinition;
10
use DI\Invoker\FactoryParameterResolver;
11
use Invoker\Exception\NotCallableException;
12
use Invoker\Exception\NotEnoughParametersException;
13
use Invoker\Invoker;
14
use Invoker\ParameterResolver\AssociativeArrayResolver;
15
use Invoker\ParameterResolver\NumericArrayResolver;
16
use Invoker\ParameterResolver\ResolverChain;
17
use Psr\Container\ContainerInterface;
18
19
/**
20
 * Resolves a factory definition to a value.
21
 *
22
 * @since 4.0
23
 * @author Matthieu Napoli <[email protected]>
24
 */
25
class FactoryResolver implements DefinitionResolver
26
{
27
    /**
28
     * @var ContainerInterface
29
     */
30
    private $container;
31
32
    /**
33
     * @var Invoker|null
34
     */
35
    private $invoker;
36
37
    /**
38
     * @var DefinitionResolver
39
     */
40
    private $resolver;
41
42
    /**
43
     * The resolver needs a container. This container will be passed to the factory as a parameter
44
     * so that the factory can access other entries of the container.
45
     */
46
    public function __construct(ContainerInterface $container, DefinitionResolver $resolver)
47
    {
48
        $this->container = $container;
49
        $this->resolver = $resolver;
50
    }
51
52
    /**
53
     * Resolve a factory definition to a value.
54
     *
55
     * This will call the callable of the definition.
56
     *
57
     * @param FactoryDefinition $definition
58
     *
59
     * {@inheritdoc}
60
     */
61
    public function resolve(Definition $definition, array $parameters = [])
62
    {
63 View Code Duplication
        if (! $this->invoker) {
64
            $parameterResolver = new ResolverChain([
65
               new AssociativeArrayResolver,
66
               new FactoryParameterResolver($this->container),
67
               new NumericArrayResolver,
68
            ]);
69
70
            $this->invoker = new Invoker($parameterResolver, $this->container);
71
        }
72
73
        $callable = $definition->getCallable();
0 ignored issues
show
Bug introduced by Matthieu Napoli
It seems like you code against a concrete implementation and not the interface DI\Definition\Definition as the method getCallable() does only exist in the following implementations of said interface: DI\Definition\DecoratorDefinition, DI\Definition\FactoryDefinition.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
74
75
        try {
76
            $providedParams = [$this->container, $definition];
77
            $extraParams = $this->resolveExtraParams($definition->getParameters());
0 ignored issues
show
Bug introduced by predakanga
It seems like you code against a concrete implementation and not the interface DI\Definition\Definition as the method getParameters() does only exist in the following implementations of said interface: DI\Definition\DecoratorDefinition, DI\Definition\FactoryDefinition, DI\Definition\ObjectDefinition\MethodInjection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
78
            $providedParams = array_merge($providedParams, $extraParams);
79
80
            return $this->invoker->call($callable, $providedParams);
81
        } catch (NotCallableException $e) {
82
            // Custom error message to help debugging
83
            if (is_string($callable) && class_exists($callable) && method_exists($callable, '__invoke')) {
84
                throw new InvalidDefinition(sprintf(
85
                    'Entry "%s" cannot be resolved: factory %s. Invokable classes cannot be automatically resolved if autowiring is disabled on the container, you need to enable autowiring or define the entry manually.',
86
                    $definition->getName(),
87
                    $e->getMessage()
88
                ));
89
            }
90
91
            throw new InvalidDefinition(sprintf(
92
                'Entry "%s" cannot be resolved: factory %s',
93
                $definition->getName(),
94
                $e->getMessage()
95
            ));
96
        } catch (NotEnoughParametersException $e) {
97
            throw new InvalidDefinition(sprintf(
98
                'Entry "%s" cannot be resolved: %s',
99
                $definition->getName(),
100
                $e->getMessage()
101
            ));
102
        }
103
    }
104
105
    public function isResolvable(Definition $definition, array $parameters = []) : bool
106
    {
107
        return true;
108
    }
109
110
    private function resolveExtraParams(array $params) : array
111
    {
112
        $resolved = [];
113
        foreach ($params as $key => $value) {
114
            // Nested definitions
115
            if ($value instanceof Definition) {
116
                $value = $this->resolver->resolve($value);
117
            }
118
            $resolved[$key] = $value;
119
        }
120
121
        return $resolved;
122
    }
123
}
124