Completed
Push — master ( 0a8464...db7528 )
by Anton
04:11
created

Container::make()   C

Complexity

Conditions 11
Paths 12

Size

Total Lines 55
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 55
rs 6.6154
cc 11
eloc 27
nc 12
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Core;
9
10
use Interop\Container\ContainerInterface;
11
use ReflectionFunctionAbstract as ContextFunction;
12
use Spiral\Core\Container\Context;
13
use Spiral\Core\Container\InjectableInterface;
14
use Spiral\Core\Container\InjectorInterface;
15
use Spiral\Core\Container\SingletonInterface;
16
use Spiral\Core\Exceptions\Container\ArgumentException;
17
use Spiral\Core\Exceptions\Container\AutowireException;
18
use Spiral\Core\Exceptions\Container\ContainerException;
19
use Spiral\Core\Exceptions\Container\InjectionException;
20
21
/**
22
 * Super simple auto-wiring container with auto SINGLETON and INJECTOR constants integration.
23
 * Compatible with Container Interop.
24
 *
25
 * Container does not support setter injections, private properties and etc. Normally it will work
26
 * with classes only.
27
 *
28
 * @see  InjectableInterface
29
 * @see  SingletonInterface
30
 *
31
 * @todo polish parent usage in make method
32
 */
33
class Container extends Component implements ContainerInterface, FactoryInterface, ResolverInterface
34
{
35
    /**
36
     * IoC bindings.
37
     *
38
     * @invisible
39
     * @var array
40
     */
41
    protected $bindings = [];
42
43
    /**
44
     * Registered injectors.
45
     *
46
     * @invisible
47
     * @var array
48
     */
49
    protected $injectors = [];
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function has($alias)
55
    {
56
        return isset($this->bindings[$alias]);
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     *
62
     * Context parameter will be passed to class injectors, which makes possible to use this method
63
     * as:
64
     * $this->container->get(DatabaseInterface::class, 'default');
65
     *
66
     * @param string|null $context Call context.
67
     */
68
    public function get($alias, $context = null)
69
    {
70
        //Direct bypass to construct, i might think about this option... or not.
71
        return $this->make($alias, [], $context);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     *
77
     * @param string|null $context Related to parameter caused injection if any.
78
     */
79
    public function make($class, $parameters = [], $context = null)
80
    {
81
        if (!isset($this->bindings[$class])) {
82
            return $this->autowire($class, $parameters, $context);
83
        }
84
85
        if ($class == ContainerInterface::class && empty($parameters)) {
86
            //self wrapping
87
            return $this;
88
        }
89
90
        if (is_object($binding = $this->bindings[$class])) {
91
            //Singleton
92
            return $binding;
93
        }
94
95
        if (is_string($binding)) {
96
            //Binding is pointing to something else
97
            return $this->make($binding, $parameters, $context);
98
        }
99
100
        if (is_array($binding)) {
101
            if (is_string($binding[0])) {
102
                //Class name
103
                $instance = $this->make($binding[0], $parameters, $context);
104
            } elseif ($binding[0] instanceof \Closure) {
105
                $reflection = new \ReflectionFunction($binding[0]);
106
107
                //Invoking Closure
108
                $instance = $reflection->invokeArgs(
109
                    $this->resolveArguments($reflection, $parameters)
110
                );
111
            } elseif (is_array($binding[0])) {
112
                //In a form of resolver and method
113
                list($resolver, $method) = $binding[0];
114
115
                $method = new \ReflectionMethod($resolver = $this->get($resolver), $method);
116
117
                $instance = $method->invokeArgs(
118
                    $resolver, $this->resolveArguments($method, $parameters)
119
                );
120
            } else {
121
                throw new ContainerException("Invalid binding.");
122
            }
123
124
            if ($binding[1]) {
125
                //Singleton
126
                $this->bindings[$class] = $instance;
127
            }
128
129
            return $instance;
130
        }
131
132
        return null;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function resolveArguments(ContextFunction $reflection, array $parameters = [])
139
    {
140
        $arguments = [];
141
        foreach ($reflection->getParameters() as $parameter) {
142
            $name = $parameter->getName();
143
144
            try {
145
                $class = $parameter->getClass();
146
            } catch (\ReflectionException $exception) {
147
                throw new ContainerException(
148
                    $exception->getMessage(),
149
                    $exception->getCode(),
150
                    $exception
151
                );
152
            }
153
154
            if ($class === Context::class) {
155
                $arguments[] = new Context($reflection, $parameter);
156
                continue;
157
            }
158
159
            if (empty($class)) {
160
                if (array_key_exists($name, $parameters)) {
161
                    //Scalar value supplied by user
162
                    $arguments[] = $parameters[$name];
163
                    continue;
164
                }
165
166
                if ($parameter->isDefaultValueAvailable()) {
167
                    //Or default value?
168
                    $arguments[] = $parameter->getDefaultValue();
169
                    continue;
170
                }
171
172
                //Unable to resolve scalar argument value
173
                throw new ArgumentException($parameter, $reflection);
174
            }
175
176
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
177
                //Supplied by user
178
                $arguments[] = $parameters[$name];
179
                continue;
180
            }
181
182
            try {
183
                //Trying to resolve dependency (contextually)
184
                $arguments[] = $this->get($class->getName(), $parameter->getName());
185
186
                continue;
187
            } catch (AutowireException $exception) {
188
                if ($parameter->isDefaultValueAvailable()) {
189
                    //Let's try to use default value instead
190
                    $arguments[] = $parameter->getDefaultValue();
191
                    continue;
192
                }
193
194
                throw $exception;
195
            }
196
        }
197
198
        return $arguments;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     *
204
     * @return $this
205
     */
206 View Code Duplication
    public function bind($alias, $resolver)
207
    {
208
        if (is_array($resolver) || $resolver instanceof \Closure) {
209
            $this->bindings[$alias] = [$resolver, false];
210
211
            return $this;
212
        }
213
214
        $this->bindings[$alias] = $resolver;
215
216
        return $this;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     *
222
     * @return $this
223
     */
224 View Code Duplication
    public function bindSingleton($alias, $resolver)
225
    {
226
        if (is_object($resolver) && !$resolver instanceof \Closure) {
227
            $this->bindings[$alias] = $resolver;
228
229
            return $this;
230
        }
231
232
        $this->bindings[$alias] = [$resolver, true];
233
234
        return $this;
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     *
240
     * @return $this
241
     */
242
    public function bindInjector($class, $injector)
243
    {
244
        $this->injectors[$class] = $injector;
245
246
        return $this;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function replace($alias, $resolver)
253
    {
254
        $payload = [$alias, null];
255
        if (isset($this->bindings[$alias])) {
256
            $payload[1] = $this->bindings[$alias];
257
        }
258
259
        $this->bind($alias, $resolver);
260
261
        return $payload;
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function restore($replacePayload)
268
    {
269
        list($alias, $resolver) = $replacePayload;
270
271
        unset($this->bindings[$alias]);
272
273
        if (!empty($resolver)) {
274
            //Restoring original value
275
            $this->bindings[$alias] = $replacePayload;
276
        }
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282
    public function hasInstance($alias)
283
    {
284
        if (!$this->has($alias)) {
285
            return false;
286
        }
287
288
        //Cross bindings
289
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
290
            $alias = $this->bindings[$alias];
291
        }
292
293
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299
    public function removeBinding($alias)
300
    {
301
        unset($this->bindings[$alias]);
302
    }
303
304
    /**
305
     * Every declared Container binding. Must not be used in production code due container format is
306
     * vary.
307
     *
308
     * @return array
309
     */
310
    public function getBindings()
311
    {
312
        return $this->bindings;
313
    }
314
315
    /**
316
     * Every binded injector.
317
     *
318
     * @return array
319
     */
320
    public function getInjectors()
321
    {
322
        return $this->injectors;
323
    }
324
325
    /**
326
     * Automatically create class.
327
     *
328
     * @param string $class
329
     * @param array  $parameters
330
     * @param string $context
331
     * @return object
332
     * @throws AutowireException
333
     */
334
    protected function autowire($class, array $parameters, $context)
335
    {
336
        if (!class_exists($class)) {
337
            throw new AutowireException("Undefined class or binding '{$class}'.");
338
        }
339
340
        //OK, we can create class by ourselves
341
        $instance = $this->createInstance($class, $parameters, $context, $reflector);
342
343
        /**
344
         * Only for classes which are constructed using autowiring, SINGLETON logic can be rewritten
345
         * or disabled using custom binding or factory.
346
         *
347
         * @var \ReflectionClass $reflector
348
         */
349
        if (
350
            $instance instanceof SingletonInterface
351
            && !empty($singleton = $reflector->getConstant('SINGLETON'))
352
        ) {
353
            //Component declared SINGLETON constant, binding as constant value and class name.
354
            $this->bindings[$singleton] = $instance;
355
        }
356
357
        return $instance;
358
359
    }
360
361
    /**
362
     * Check if given class has associated injector.
363
     *
364
     * @param \ReflectionClass $reflection
365
     * @return bool
366
     */
367
    protected function hasInjector(\ReflectionClass $reflection)
368
    {
369
        if (isset($this->injectors[$reflection->getName()])) {
370
            return true;
371
        }
372
373
        return $reflection->isSubclassOf(InjectableInterface::class);
374
    }
375
376
    /**
377
     * Get injector associated with given class.
378
     *
379
     * @param \ReflectionClass $reflection
380
     * @return InjectorInterface
381
     */
382
    protected function getInjector(\ReflectionClass $reflection)
383
    {
384
        if (isset($this->injectors[$reflection->getName()])) {
385
            return $this->get($this->injectors[$reflection->getName()]);
386
        }
387
388
        return $this->get($reflection->getConstant('INJECTOR'));
389
    }
390
391
    /**
392
     * Create instance of desired class.
393
     *
394
     * @param string           $class
395
     * @param array            $parameters     Constructor parameters.
396
     * @param string|null      $context
397
     * @param \ReflectionClass $reflection     Instance of reflection associated with class,
398
     *                                         reference.
399
     * @return object
400
     * @throws ContainerException
401
     */
402
    private function createInstance(
403
        $class,
404
        array $parameters,
405
        $context = null,
406
        \ReflectionClass &$reflection = null
407
    ) {
408
        try {
409
            $reflection = new \ReflectionClass($class);
410
        } catch (\ReflectionException $exception) {
411
            throw new ContainerException(
412
                $exception->getMessage(), $exception->getCode(), $exception
413
            );
414
        }
415
416
        //We have to construct class using external injector
417
        if (empty($parameters) && $this->hasInjector($reflection)) {
418
            //Creating class using injector/factory
419
            $instance = $this->getInjector($reflection)->createInjection(
420
                $reflection,
421
                $context
422
            );
423
424
            if (!$reflection->isInstance($instance)) {
425
                throw new InjectionException("Invalid injector response.");
426
            }
427
428
            //todo: potentially to be replaced with direct call logic (when method is specified
429
            //todo: instead of class/binding name)
430
            return $instance;
431
        }
432
433
        if (!$reflection->isInstantiable()) {
434
            throw new ContainerException("Class '{$class}' can not be constructed.");
435
        }
436
437
        if (!empty($constructor = $reflection->getConstructor())) {
438
            //Using constructor with resolved arguments
439
            $instance = $reflection->newInstanceArgs(
440
                $this->resolveArguments($constructor, $parameters)
441
            );
442
        } else {
443
            //No constructor specified
444
            $instance = $reflection->newInstance();
445
        }
446
447
        return $instance;
448
    }
449
}
450