Completed
Push — master ( d87922...40c321 )
by James Ekow Abaka
01:19
created

Container::getResolvedBinding()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 3
nop 1
crap 4
1
<?php
2
3
namespace ntentan\panie;
4
5
use Psr\Container\ContainerInterface;
6
7
/**
8
 * Container class through which dependencies are defined and resolved.
9
 *
10
 * @author ekow
11
 */
12
class Container implements ContainerInterface
13
{
14
15
    /**
16
     * Holds all bindings defined.
17
     * @var Bindings 
18
     */
19
    private $bindings;
20
    
21
    /**
22
     * Holds instances of all singletons.
23
     * @var array
24
     */
25
    private $singletons = [];
26
27 13
    public function __construct()
28
    {
29 13
        $this->bindings = new Bindings();
30 13
    }
31
32
    /**
33
     * Resolves names of items requested from the container to their correct binding definition.
34
     * 
35
     * @param string $class
36
     * @return array|null The name of the class detected or null
37
     */
38 12
    private function getResolvedBinding(string $class)
39
    {
40 12
        $bound = null;
41 12
        if ($this->bindings->has($class)) {
42 7
            $bound = $this->bindings->get($class);
43 8
        } else if (is_string($class) && class_exists($class)) {
44 6
            $bound = ['binding' => $class];
45
        }
46 12
        return $bound;
47
    }
48
49
    /**
50
     * Starts the process of defining a binding.
51
     * 
52
     * @param string $type
53
     * @return \ntentan\panie\Bindings
54
     */
55 7
    public function bind(string $type) : Bindings
56
    {
57 7
        return $this->bindings->setActiveKey($type);
58
    }
59
60
    /**
61
     * Returns true if type is found in container otherwise it returns false.
62
     * 
63
     * @param string $type
64
     * @return bool
65
     */
66 1
    public function has($type) : bool
67
    {
68 1
        return $this->bindings->has($type);
69
    }
70
71
    /**
72
     * Pass an array of bindings to the container.
73
     * 
74
     * @param type $bindings
75
     */
76 2
    public function setup($bindings) : void
77
    {
78 2
        $this->bindings->merge($bindings);
79 2
    }
80
81
    /**
82
     * Resolves a type and returns an instance of an object of the requested type.
83
     * Optional constructor arguments could be provided to be used in initializing the object. This method throws a 
84
     * ResolutionException in cases where the type could not be resolved.
85
     * 
86
     * @param string $type
87
     * @param array $constructorArguments
88
     * @return mixed
89
     * @throws exceptions\ResolutionException
90
     */
91 12
    public function resolve(string $type, array $constructorArguments = [])
92
    {
93 12
        $resolvedClass = $this->getResolvedBinding($type);
94 12
        if ($resolvedClass['binding'] === null) {
95 2
            throw new exceptions\ResolutionException("Could not resolve dependency of type [$type]");
96
        }
97 10
        if ($resolvedClass['singleton'] ?? false) {
98 2
            $instance = $this->getSingletonInstance($type, $resolvedClass['binding'], $constructorArguments);
99
        } else {
100 8
            $instance = $this->getInstance($resolvedClass['binding'], $constructorArguments);
101
        }
102
        
103 9
        foreach($resolvedClass['calls'] ?? [] as $calls) {
104
            $method = new \ReflectionMethod($instance, $calls[0]);
105
            $method->invokeArgs($instance, $this->getMethodArguments($method, $calls[1]));
106
        }
107
        
108 9
        return $instance;
109
    }
110
111
    /**
112
     * Returns an instance of the type requested if this type (which was requested) is defined in the container.
113
     * 
114
     * @param string $type
115
     * @return mixed
116
     */
117
    public function get($type)
118
    {
119
        return $this->resolve($type);
120
    }
121
122
    /**
123
     * Resolves an argument for a method or constructor.
124
     * If the argument passed is a string and the type hint of the argument points to an object, the string passed is
125
     * assumed to be a class binding and it is resolved.
126
     * 
127
     * @param mixed $argument
128
     * @param string $class
129
     * @return mixed
130
     */
131 1
    private function resolveArgument($argument, $class)
132
    {
133 1
        if($class && is_string($argument)) {
134
            return $this->resolve($argument);
135
        }
136 1
        return $argument;
137
    }
138
139
    /**
140
     * Resolves all the arguments of a method or constructor.
141
     * 
142
     * @param \ReflectionMethod $method
143
     * @param array $methodArguments
144
     * @return array
145
     */
146 3
    private function getMethodArguments(\ReflectionMethod $method, array $methodArguments) : array
147
    {
148 3
        $argumentValues = [];
149 3
        $parameters = $method->getParameters();
150 3
        foreach ($parameters as $parameter) {
151 3
            $class = $parameter->getClass();
152 3
            $className = $class ? $class->getName() : null;
153 3
            if (isset($methodArguments[$parameter->getName()])) {
1 ignored issue
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
154 1
                $argumentValues[] = $this->resolveArgument($methodArguments[$parameter->getName()], $className);
1 ignored issue
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
155
            } else {
156 3
                $argumentValues[] = $className ? $this->resolve($className) : null;
157
            }
158
        }
159 3
        return $argumentValues;
160
    }
161
162
    /**
163
     * Returns a singleton of a given bound type.
164
     * 
165
     * @param string $type
166
     * @param string $class
167
     * @param array $constructorArguments
168
     * @return mixed
169
     */
170 2
    private function getSingletonInstance(string $type, string $class, array $constructorArguments)
171
    {
172 2
        if (!isset($this->singletons[$type])) {
173 2
            $this->singletons[$type] = $this->getInstance($class, $constructorArguments);
174
        }
175 2
        return $this->singletons[$type];
176
    }
177
178
    /**
179
     * Returns an instance of a class.
180
     * 
181
     * @param string|closure $className
182
     * @param array $constructorArguments
183
     * @return mixed
184
     * @throws exceptions\ResolutionException
185
     */
186 10
    private function getInstance($className, array $constructorArguments = [])
187
    {
188 10
        if (is_callable($className)) {
189 1
            return $className($this);
190
        }
191 9
        $reflection = new \ReflectionClass($className);
192 9
        if ($reflection->isAbstract()) {
193 1
            throw new exceptions\ResolutionException(
194 1
            "Abstract class {$reflection->getName()} cannot be instantiated. "
1 ignored issue
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
195 1
            . "Please provide a binding to an implementation."
196
            );
197
        }
198 8
        $constructor = $reflection->getConstructor();
199 8
        return $reflection->newInstanceArgs($constructor ? $this->getMethodArguments($constructor, $constructorArguments) : []);
200
    }
201
}
202