Completed
Pull Request — master (#2)
by James Ekow Abaka
01:19
created

Container   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 198
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 2
dl 0
loc 198
ccs 59
cts 59
cp 1
rs 10
c 0
b 0
f 0

11 Methods

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