Passed
Push — master ( 68f720...5e727b )
by Paul
10:53
created

Container::isBuildable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
1
<?php
2
3
namespace GeminiLabs\SiteReviews;
4
5
use Closure;
6
use Exception;
7
use GeminiLabs\SiteReviews\Exceptions\BindingResolutionException;
8
use GeminiLabs\SiteReviews\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Helpers\Str;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionParameter;
13
14
abstract class Container
15
{
16
    /**
17
     * @var array
18
     */
19
    protected $bindings = [];
20
21
    /**
22
     * @var array
23
     */
24
    protected $buildStack = [];
25
26
    /**
27
     * @var array
28
     */
29
    protected $instances = [];
30
31
    /**
32
     * @var array[]
33
     */
34
    protected $with = [];
35
36
    /**
37
     * @param string $abstract
38
     * @param mixed $concrete
39
     * @param bool $shared
40
     * @return void
41
     */
42
    public function bind($abstract, $concrete = null, $shared = false)
43
    {
44
        $this->dropStaleInstances($abstract);
45
        if (is_null($concrete)) {
46
            $concrete = $abstract;
47
        }
48
        if (!$concrete instanceof Closure) {
49
            $concrete = $this->getClosure($abstract, $concrete);
50
        }
51
        $this->bindings[$abstract] = compact('concrete', 'shared');
52
    }
53
54
    /**
55
     * @param mixed $abstract
56
     * @return mixed
57
     */
58 25
    public function make($abstract, array $parameters = [])
59
    {
60 25
        if (is_string($abstract) && !class_exists($abstract)) {
61 9
            $alias = __NAMESPACE__.'\\'.Str::removePrefix($abstract, __NAMESPACE__);
62 9
            $abstract = Helper::ifTrue(class_exists($alias), $alias, $abstract);
63
        }
64 25
        return $this->resolve($abstract, $parameters);
65
    }
66
67
    /**
68
     * @param string $abstract
69
     * @param mixed $concrete
70
     * @return void
71
     */
72
    public function singleton($abstract, $concrete = null)
73
    {
74
        $this->bind($abstract, $concrete, true);
75
    }
76
77
    /**
78
     * @param Closure|string $concrete
79
     * @return mixed
80
     * @throws BindingResolutionException
81
     */
82 25
    protected function construct($concrete)
83
    {
84 25
        if ($concrete instanceof Closure) {
85
            return $concrete($this, $this->getLastParameterOverride()); // probably a bound closure
86
        }
87
        try {
88 25
            $reflector = new ReflectionClass($concrete); // class or classname provided
89 7
        } catch (ReflectionException $e) {
90 7
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
91
        }
92 25
        if (!$reflector->isInstantiable()) {
93
            $this->throwNotInstantiable($concrete); // not an instantiable class
94
        }
95 25
        $this->buildStack[] = $concrete;
96 25
        if (is_null($constructor = $reflector->getConstructor())) {
97 23
            array_pop($this->buildStack);
98 23
            return new $concrete(); // class has no __construct
99
        }
100
        try {
101 25
            $instances = $this->resolveDependencies($constructor->getParameters()); // resolve class dependencies
102
        } catch (BindingResolutionException $e) {
103
            array_pop($this->buildStack);
104
            throw $e;
105
        }
106 25
        array_pop($this->buildStack);
107 25
        return $reflector->newInstanceArgs($instances); // return a new class
108
    }
109
110
    /**
111
     * @param string $abstract
112
     * @return void
113
     */
114
    protected function dropStaleInstances($abstract)
115
    {
116
        unset($this->instances[$abstract]);
117
    }
118
119
    /**
120
     * @param string $abstract
121
     * @param string $concrete
122
     * @return Closure
123
     */
124
    protected function getClosure($abstract, $concrete)
125
    {
126
        return function ($container, $parameters = []) use ($abstract, $concrete) {
127
            return $abstract == $concrete
128
                ? $container->construct($concrete)
129
                : $container->resolve($concrete, $parameters);
130
        };
131
    }
132
133
    /**
134
     * @param string $abstract
135
     * @return mixed
136
     */
137 25
    protected function getConcrete($abstract)
138
    {
139 25
        if (isset($this->bindings[$abstract])) {
140
            return $this->bindings[$abstract]['concrete'];
141
        }
142 25
        return $abstract;
143
    }
144
145
    /**
146
     * @return array
147
     */
148 7
    protected function getLastParameterOverride()
149
    {
150 7
        return Arr::consolidate(end($this->with));
151
    }
152
153
    /**
154
     * @param ReflectionParameter $dependency
155
     * @return mixed
156
     */
157 7
    protected function getParameterOverride($dependency)
158
    {
159 7
        return $this->getLastParameterOverride()[$dependency->name];
160
    }
161
162
    /**
163
     * @param ReflectionParameter $dependency
164
     * @return bool
165
     */
166 7
    protected function hasParameterOverride($dependency)
167
    {
168 7
        return array_key_exists($dependency->name, $this->getLastParameterOverride());
169
    }
170
171
    /**
172
     * @param mixed $concrete
173
     * @param string $abstract
174
     * @return bool
175
     */
176 25
    protected function isBuildable($concrete, $abstract)
177
    {
178 25
        return $concrete === $abstract || $concrete instanceof Closure;
179
    }
180
181
    /**
182
     * @param string $abstract
183
     * @return bool
184
     */
185 25
    protected function isShared($abstract)
186
    {
187 25
        return isset($this->instances[$abstract]) || !empty($this->bindings[$abstract]['shared']);
188
    }
189
190
    /**
191
     * @param mixed $abstract
192
     * @param array $parameters
193
     * @return mixed
194
     * @throws BindingResolutionException
195
     */
196 25
    protected function resolve($abstract, $parameters = [])
197
    {
198 25
        if (isset($this->instances[$abstract]) && empty($parameters)) {
199 15
            return $this->instances[$abstract]; // return an existing singleton
200
        }
201 25
        $this->with[] = $parameters;
202 25
        $concrete = $this->getConcrete($abstract);
203 25
        $object = Helper::ifTrue($this->isBuildable($concrete, $abstract),
204
            function () use ($concrete) { return $this->construct($concrete); },
205
            function () use ($concrete) { return $this->make($concrete); }
206
        );
207 25
        if ($this->isShared($abstract) && empty($parameters)) {
208
            $this->instances[$abstract] = $object; // store as a singleton
209
        }
210 25
        array_pop($this->with);
211 25
        return $object;
212
    }
213
214
    /**
215
     * Resolve a class based dependency from the container.
216
     * @return mixed
217
     * @throws Exception
218
     */
219
    protected function resolveClass(ReflectionParameter $parameter)
220
    {
221
        try {
222
            return $this->make($parameter->getClass()->name);
223
        } catch (Exception $error) {
224
            if ($parameter->isOptional()) {
225
                return $parameter->getDefaultValue();
226
            }
227
            throw $error;
228
        }
229
    }
230
231
    /**
232
     * @return array
233
     */
234 25
    protected function resolveDependencies(array $dependencies)
235
    {
236 25
        $results = [];
237 25
        foreach ($dependencies as $dependency) {
238 7
            if ($this->hasParameterOverride($dependency)) {
239 7
                $results[] = $this->getParameterOverride($dependency);
240 7
                continue;
241
            }
242
            $results[] = Helper::ifTrue(is_null($dependency->getClass()),
243
                function () use ($dependency) { return $this->resolvePrimitive($dependency); },
244
                function () use ($dependency) { return $this->resolveClass($dependency); }
245
            );
246
        }
247 25
        return $results;
248
    }
249
250
    /**
251
     * @param ReflectionParameter $parameter
252
     * @return mixed
253
     * @throws BindingResolutionException
254
     */
255
    protected function resolvePrimitive(ReflectionParameter $parameter)
256
    {
257
        if ($parameter->isDefaultValueAvailable()) {
258
            return $parameter->getDefaultValue();
259
        }
260
        $this->throwUnresolvablePrimitive($parameter);
261
    }
262
263
    /**
264
     * @param string $concrete
265
     * @return void
266
     * @throws BindingResolutionException
267
     */
268
    protected function throwNotInstantiable($concrete)
269
    {
270
        if (empty($this->buildStack)) {
271
            $message = "Target [$concrete] is not instantiable.";
272
        } else {
273
            $previous = implode(', ', $this->buildStack);
274
            $message = "Target [$concrete] is not instantiable while building [$previous].";
275
        }
276
        throw new BindingResolutionException($message);
277
    }
278
279
    /**
280
     * @param ReflectionParameter $parameter
281
     * @return void
282
     * @throws BindingResolutionException
283
     */
284
    protected function throwUnresolvablePrimitive(ReflectionParameter $parameter)
285
    {
286
        throw new BindingResolutionException("Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}");
287
    }
288
}
289