Completed
Branch develop (85a9c8)
by Anton
05:44
created

Container::autowire()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 5
nc 2
nop 3
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
        //todo: unify using InvokerInterface
99
        if (is_array($binding)) {
100
            if (is_string($binding[0])) {
101
                //Class name
102
                $instance = $this->make($binding[0], $parameters, $context);
103
            } elseif ($binding[0] instanceof \Closure) {
104
                $reflection = new \ReflectionFunction($binding[0]);
105
106
                //Invoking Closure
107
                $instance = $reflection->invokeArgs(
108
                    $this->resolveArguments($reflection, $parameters)
109
                );
110
            } elseif (is_array($binding[0])) {
111
                //In a form of resolver and method
112
                list($resolver, $method) = $binding[0];
113
114
                $resolver = $this->get($resolver);
115
                $method = new \ReflectionMethod($resolver, $method);
116
                $method->setAccessible(true);
117
118
                $instance = $method->invokeArgs(
119
                    $resolver, $this->resolveArguments($method, $parameters)
120
                );
121
            } else {
122
                throw new ContainerException("Invalid binding.");
123
            }
124
125
            if ($binding[1]) {
126
                //Singleton
127
                $this->bindings[$class] = $instance;
128
            }
129
130
            if (!is_object($instance)) {
131
                //Non object bindings are allowed
132
                return $instance;
133
            }
134
135
            return $this->registerInstance(
136
                $instance,
137
                new \ReflectionObject($instance),
138
                $parameters
139
            );
140
        }
141
142
        return null;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function resolveArguments(ContextFunction $reflection, array $parameters = [])
149
    {
150
        $arguments = [];
151
        foreach ($reflection->getParameters() as $parameter) {
152
            $name = $parameter->getName();
153
154
            try {
155
                $class = $parameter->getClass();
156
            } catch (\ReflectionException $exception) {
157
                throw new ContainerException(
158
                    $exception->getMessage(),
159
                    $exception->getCode(),
160
                    $exception
161
                );
162
            }
163
164
            if ($class === Context::class) {
165
                $arguments[] = new Context($reflection, $parameter);
166
                continue;
167
            }
168
169
            if (empty($class)) {
170
                if (array_key_exists($name, $parameters)) {
171
                    //Scalar value supplied by user
172
                    $arguments[] = $parameters[$name];
173
                    continue;
174
                }
175
176
                if ($parameter->isDefaultValueAvailable()) {
177
                    //Or default value?
178
                    $arguments[] = $parameter->getDefaultValue();
179
                    continue;
180
                }
181
182
                //Unable to resolve scalar argument value
183
                throw new ArgumentException($parameter, $reflection);
184
            }
185
186
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
187
                //Supplied by user
188
                $arguments[] = $parameters[$name];
189
                continue;
190
            }
191
192
            try {
193
                //Trying to resolve dependency (contextually)
194
                $arguments[] = $this->get($class->getName(), $parameter->getName());
195
196
                continue;
197
            } catch (AutowireException $exception) {
198
                if ($parameter->isDefaultValueAvailable()) {
199
                    //Let's try to use default value instead
200
                    $arguments[] = $parameter->getDefaultValue();
201
                    continue;
202
                }
203
204
                throw $exception;
205
            }
206
        }
207
208
        return $arguments;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     *
214
     * @return $this
215
     */
216 View Code Duplication
    public function bind($alias, $resolver)
217
    {
218
        if (is_array($resolver) || $resolver instanceof \Closure) {
219
            $this->bindings[$alias] = [$resolver, false];
220
221
            return $this;
222
        }
223
224
        $this->bindings[$alias] = $resolver;
225
226
        return $this;
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     *
232
     * @return $this
233
     */
234 View Code Duplication
    public function bindSingleton($alias, $resolver)
235
    {
236
        if (is_object($resolver) && !$resolver instanceof \Closure) {
237
            $this->bindings[$alias] = $resolver;
238
239
            return $this;
240
        }
241
242
        $this->bindings[$alias] = [$resolver, true];
243
244
        return $this;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     *
250
     * @return $this
251
     */
252
    public function bindInjector($class, $injector)
253
    {
254
        $this->injectors[$class] = $injector;
255
256
        return $this;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function replace($alias, $resolver)
263
    {
264
        $payload = [$alias, null];
265
        if (isset($this->bindings[$alias])) {
266
            $payload[1] = $this->bindings[$alias];
267
        }
268
269
        $this->bind($alias, $resolver);
270
271
        return $payload;
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277
    public function restore($replacePayload)
278
    {
279
        list($alias, $resolver) = $replacePayload;
280
281
        unset($this->bindings[$alias]);
282
283
        if (!empty($resolver)) {
284
            //Restoring original value
285
            $this->bindings[$alias] = $replacePayload;
286
        }
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function hasInstance($alias)
293
    {
294
        if (!$this->has($alias)) {
295
            return false;
296
        }
297
298
        //Cross bindings
299
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
300
            $alias = $this->bindings[$alias];
301
        }
302
303
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function removeBinding($alias)
310
    {
311
        unset($this->bindings[$alias]);
312
    }
313
314
    /**
315
     * Every declared Container binding. Must not be used in production code due container format is
316
     * vary.
317
     *
318
     * @return array
319
     */
320
    public function getBindings()
321
    {
322
        return $this->bindings;
323
    }
324
325
    /**
326
     * Every binded injector.
327
     *
328
     * @return array
329
     */
330
    public function getInjectors()
331
    {
332
        return $this->injectors;
333
    }
334
335
    /**
336
     * Automatically create class.
337
     *
338
     * @param string $class
339
     * @param array  $parameters
340
     * @param string $context
341
     * @return object
342
     * @throws AutowireException
343
     */
344
    protected function autowire($class, array $parameters, $context)
345
    {
346
        if (!class_exists($class)) {
347
            throw new AutowireException("Undefined class or binding '{$class}'.");
348
        }
349
350
        //OK, we can create class by ourselves
351
        $instance = $this->createInstance($class, $parameters, $context, $reflector);
352
353
        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...
354
    }
355
356
    /**
357
     * Check if given class has associated injector.
358
     *
359
     * @todo replace with Context on demand
360
     * @param \ReflectionClass $reflection
361
     * @return bool
362
     */
363
    protected function hasInjector(\ReflectionClass $reflection)
364
    {
365
        if (isset($this->injectors[$reflection->getName()])) {
366
            return true;
367
        }
368
369
        return $reflection->isSubclassOf(InjectableInterface::class);
370
    }
371
372
    /**
373
     * Get injector associated with given class.
374
     *
375
     * @todo replace with Context on demand
376
     * @param \ReflectionClass $reflection
377
     * @return InjectorInterface
378
     */
379
    protected function getInjector(\ReflectionClass $reflection)
380
    {
381
        if (isset($this->injectors[$reflection->getName()])) {
382
            return $this->get($this->injectors[$reflection->getName()]);
383
        }
384
385
        return $this->get($reflection->getConstant('INJECTOR'));
386
    }
387
388
    /**
389
     * Create instance of desired class.
390
     *
391
     * @param string           $class
392
     * @param array            $parameters     Constructor parameters.
393
     * @param string|null      $context
394
     * @param \ReflectionClass $reflection     Instance of reflection associated with class,
395
     *                                         reference.
396
     * @return object
397
     * @throws ContainerException
398
     */
399
    private function createInstance(
400
        $class,
401
        array $parameters,
402
        $context = null,
403
        \ReflectionClass &$reflection = null
404
    ) {
405
        try {
406
            $reflection = new \ReflectionClass($class);
407
        } catch (\ReflectionException $exception) {
408
            throw new ContainerException(
409
                $exception->getMessage(), $exception->getCode(), $exception
410
            );
411
        }
412
413
        //We have to construct class using external injector
414
        if (empty($parameters) && $this->hasInjector($reflection)) {
415
            //Creating class using injector/factory
416
            $instance = $this->getInjector($reflection)->createInjection(
417
                $reflection,
418
                $context
419
            );
420
421
            if (!$reflection->isInstance($instance)) {
422
                throw new InjectionException("Invalid injector response.");
423
            }
424
425
            //todo: potentially to be replaced with direct call logic (when method is specified
426
            //todo: instead of class/binding name) (see Context class)
427
            return $instance;
428
        }
429
430
        if (!$reflection->isInstantiable()) {
431
            throw new ContainerException("Class '{$class}' can not be constructed.");
432
        }
433
434
        if (!empty($constructor = $reflection->getConstructor())) {
435
            //Using constructor with resolved arguments
436
            $instance = $reflection->newInstanceArgs(
437
                $this->resolveArguments($constructor, $parameters)
438
            );
439
        } else {
440
            //No constructor specified
441
            $instance = $reflection->newInstance();
442
        }
443
444
        return $instance;
445
    }
446
447
448
    /**
449
     * Make sure instance conditions are met
450
     *
451
     * @param object           $instance
452
     * @param \ReflectionClass $reflector
453
     * @param array            $parameters
454
     * @return object
455
     */
456
    private function registerInstance($instance, \ReflectionClass $reflector, array $parameters)
457
    {
458
        if (
459
            empty($parameters)
460
            && $instance instanceof SingletonInterface
461
            && !empty($singleton = $reflector->getConstant('SINGLETON'))
462
        ) {
463
            if (!isset($this->bindings[$singleton])) {
464
                $this->bindings[$singleton] = $instance;
465
            }
466
        }
467
468
        //todo: additional registration operations?
469
470
        return $instance;
471
    }
472
}