Completed
Pull Request — master (#79)
by Anton
15:46
created

Container::resolveArguments()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 62
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 62
rs 6.1722
cc 11
eloc 34
nc 10
nop 2

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
class Container extends Component implements ContainerInterface, FactoryInterface, ResolverInterface
32
{
33
    /**
34
     * IoC bindings.
35
     *
36
     * @invisible
37
     * @var array
38
     */
39
    protected $bindings = [];
40
41
    /**
42
     * Registered injectors.
43
     *
44
     * @invisible
45
     * @var array
46
     */
47
    protected $injectors = [];
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function has($alias)
53
    {
54
        return array_key_exists($alias, $this->bindings);
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     *
60
     * Context parameter will be passed to class injectors, which makes possible to use this method
61
     * as:
62
     * $this->container->get(DatabaseInterface::class, 'default');
63
     *
64
     * @param string|null $context Call context.
65
     */
66
    public function get($alias, $context = null)
67
    {
68
        //Direct bypass to construct, i might think about this option... or not.
69
        return $this->make($alias, [], $context);
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     *
75
     * @param string|null $context Related to parameter caused injection if any.
76
     */
77
    public function make($class, $parameters = [], $context = null)
78
    {
79
        if (!isset($this->bindings[$class])) {
80
            return $this->autowire($class, $parameters, $context);
81
        }
82
83
        if ($class == ContainerInterface::class && empty($parameters)) {
84
            //self wrapping
85
            return $this;
86
        }
87
88
        if (is_object($binding = $this->bindings[$class])) {
89
            //Singleton
90
            return $binding;
91
        }
92
93
        if (is_string($binding)) {
94
            //Binding is pointing to something else
95
            return $this->make($binding, $parameters, $context);
96
        }
97
98
        if (is_array($binding)) {
99
            if (is_string($binding[0])) {
100
                //Class name
101
                $instance = $this->make($binding[0], $parameters, $context);
102
            } elseif ($binding[0] instanceof \Closure) {
103
                $reflection = new \ReflectionFunction($binding[0]);
104
105
                //Invoking Closure
106
                $instance = $reflection->invokeArgs(
107
                    $this->resolveArguments($reflection, $parameters)
108
                );
109
            } elseif (is_array($binding[0])) {
110
                //In a form of resolver and method
111
                list($resolver, $method) = $binding[0];
112
113
                $method = new \ReflectionMethod($resolver = $this->get($resolver), $method);
114
115
                $instance = $method->invokeArgs(
116
                    $resolver, $this->resolveArguments($method, $parameters)
117
                );
118
            } else {
119
                throw new ContainerException("Invalid binding.");
120
            }
121
122
            if ($binding[1]) {
123
                //Singleton
124
                $this->bindings[$class] = $instance;
125
            }
126
127
            return $this->registerInstance(
128
                $instance,
129
                new \ReflectionObject($instance),
130
                $parameters
131
            );
132
        }
133
134
        return null;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function resolveArguments(ContextFunction $reflection, array $parameters = [])
141
    {
142
        $arguments = [];
143
        foreach ($reflection->getParameters() as $parameter) {
144
            $name = $parameter->getName();
145
146
            try {
147
                $class = $parameter->getClass();
148
            } catch (\ReflectionException $exception) {
149
                throw new ContainerException(
150
                    $exception->getMessage(),
151
                    $exception->getCode(),
152
                    $exception
153
                );
154
            }
155
156
            if ($class === Context::class) {
157
                $arguments[] = new Context($reflection, $parameter);
158
                continue;
159
            }
160
161
            if (empty($class)) {
162
                if (array_key_exists($name, $parameters)) {
163
                    //Scalar value supplied by user
164
                    $arguments[] = $parameters[$name];
165
                    continue;
166
                }
167
168
                if ($parameter->isDefaultValueAvailable()) {
169
                    //Or default value?
170
                    $arguments[] = $parameter->getDefaultValue();
171
                    continue;
172
                }
173
174
                //Unable to resolve scalar argument value
175
                throw new ArgumentException($parameter, $reflection);
176
            }
177
178
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
179
                //Supplied by user
180
                $arguments[] = $parameters[$name];
181
                continue;
182
            }
183
184
            try {
185
                //Trying to resolve dependency (contextually)
186
                $arguments[] = $this->get($class->getName(), $parameter->getName());
187
188
                continue;
189
            } catch (AutowireException $exception) {
190
                if ($parameter->isDefaultValueAvailable()) {
191
                    //Let's try to use default value instead
192
                    $arguments[] = $parameter->getDefaultValue();
193
                    continue;
194
                }
195
196
                throw $exception;
197
            }
198
        }
199
200
        return $arguments;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     *
206
     * @return $this
207
     */
208 View Code Duplication
    public function bind($alias, $resolver)
209
    {
210
        if (is_array($resolver) || $resolver instanceof \Closure) {
211
            $this->bindings[$alias] = [$resolver, false];
212
213
            return $this;
214
        }
215
216
        $this->bindings[$alias] = $resolver;
217
218
        return $this;
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     *
224
     * @return $this
225
     */
226 View Code Duplication
    public function bindSingleton($alias, $resolver)
227
    {
228
        if (is_object($resolver) && !$resolver instanceof \Closure) {
229
            $this->bindings[$alias] = $resolver;
230
231
            return $this;
232
        }
233
234
        $this->bindings[$alias] = [$resolver, true];
235
236
        return $this;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     *
242
     * @return $this
243
     */
244
    public function bindInjector($class, $injector)
245
    {
246
        $this->injectors[$class] = $injector;
247
248
        return $this;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254
    public function replace($alias, $resolver)
255
    {
256
        $payload = [$alias, null];
257
        if (isset($this->bindings[$alias])) {
258
            $payload[1] = $this->bindings[$alias];
259
        }
260
261
        $this->bind($alias, $resolver);
262
263
        return $payload;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function restore($replacePayload)
270
    {
271
        list($alias, $resolver) = $replacePayload;
272
273
        unset($this->bindings[$alias]);
274
275
        if (!empty($resolver)) {
276
            //Restoring original value
277
            $this->bindings[$alias] = $replacePayload;
278
        }
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function hasInstance($alias)
285
    {
286
        if (!$this->has($alias)) {
287
            return false;
288
        }
289
290
        //Cross bindings
291
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
292
            $alias = $this->bindings[$alias];
293
        }
294
295
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public function removeBinding($alias)
302
    {
303
        unset($this->bindings[$alias]);
304
    }
305
306
    /**
307
     * Every declared Container binding. Must not be used in production code due container format is
308
     * vary.
309
     *
310
     * @return array
311
     */
312
    public function getBindings()
313
    {
314
        return $this->bindings;
315
    }
316
317
    /**
318
     * Every binded injector.
319
     *
320
     * @return array
321
     */
322
    public function getInjectors()
323
    {
324
        return $this->injectors;
325
    }
326
327
    /**
328
     * Automatically create class.
329
     *
330
     * @param string $class
331
     * @param array  $parameters
332
     * @param string $context
333
     * @return object
334
     * @throws AutowireException
335
     */
336
    protected function autowire($class, array $parameters, $context)
337
    {
338
        if (!class_exists($class)) {
339
            throw new AutowireException("Undefined class or binding '{$class}'.");
340
        }
341
342
        //OK, we can create class by ourselves
343
        $instance = $this->createInstance($class, $parameters, $context, $reflector);
344
345
        return $this->registerInstance($instance, $reflector, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $reflector can be null; however, registerInstance() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
346
    }
347
348
    /**
349
     * Check if given class has associated injector.
350
     *
351
     * @todo replace with Context on demand
352
     * @param \ReflectionClass $reflection
353
     * @return bool
354
     */
355
    protected function hasInjector(\ReflectionClass $reflection)
356
    {
357
        if (isset($this->injectors[$reflection->getName()])) {
358
            return true;
359
        }
360
361
        return $reflection->isSubclassOf(InjectableInterface::class);
362
    }
363
364
    /**
365
     * Get injector associated with given class.
366
     *
367
     * @todo replace with Context on demand
368
     * @param \ReflectionClass $reflection
369
     * @return InjectorInterface
370
     */
371
    protected function getInjector(\ReflectionClass $reflection)
372
    {
373
        if (isset($this->injectors[$reflection->getName()])) {
374
            return $this->get($this->injectors[$reflection->getName()]);
375
        }
376
377
        return $this->get($reflection->getConstant('INJECTOR'));
378
    }
379
380
    /**
381
     * Create instance of desired class.
382
     *
383
     * @param string           $class
384
     * @param array            $parameters     Constructor parameters.
385
     * @param string|null      $context
386
     * @param \ReflectionClass $reflection     Instance of reflection associated with class,
387
     *                                         reference.
388
     * @return object
389
     * @throws ContainerException
390
     */
391
    private function createInstance(
392
        $class,
393
        array $parameters,
394
        $context = null,
395
        \ReflectionClass &$reflection = null
396
    ) {
397
        try {
398
            $reflection = new \ReflectionClass($class);
399
        } catch (\ReflectionException $exception) {
400
            throw new ContainerException(
401
                $exception->getMessage(), $exception->getCode(), $exception
402
            );
403
        }
404
405
        //We have to construct class using external injector
406
        if (empty($parameters) && $this->hasInjector($reflection)) {
407
            //Creating class using injector/factory
408
            $instance = $this->getInjector($reflection)->createInjection(
409
                $reflection,
410
                $context
411
            );
412
413
            if (!$reflection->isInstance($instance)) {
414
                throw new InjectionException("Invalid injector response.");
415
            }
416
417
            //todo: potentially to be replaced with direct call logic (when method is specified
418
            //todo: instead of class/binding name) (see Context class)
419
            return $instance;
420
        }
421
422
        if (!$reflection->isInstantiable()) {
423
            throw new ContainerException("Class '{$class}' can not be constructed.");
424
        }
425
426
        if (!empty($constructor = $reflection->getConstructor())) {
427
            //Using constructor with resolved arguments
428
            $instance = $reflection->newInstanceArgs(
429
                $this->resolveArguments($constructor, $parameters)
430
            );
431
        } else {
432
            //No constructor specified
433
            $instance = $reflection->newInstance();
434
        }
435
436
        return $instance;
437
    }
438
439
440
    /**
441
     * Make sure instance conditions are met
442
     *
443
     * @param object           $instance
444
     * @param \ReflectionClass $reflector
445
     * @param array            $parameters
446
     * @return object
447
     */
448
    private function registerInstance($instance, \ReflectionClass $reflector, array $parameters)
449
    {
450
        if (
451
            empty($parameters)
452
            && $instance instanceof SingletonInterface
453
            && !empty($singleton = $reflector->getConstant('SINGLETON'))
454
        ) {
455
            if (!$this->has($singleton)) {
456
                $this->bindSingleton($singleton, $instance);
457
            }
458
        }
459
460
        //todo: additional registration operations?
461
462
        return $instance;
463
    }
464
}