Completed
Push — master ( e1942b...994c00 )
by Anton
03:32
created

Container::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Core;
10
11
use Interop\Container\ContainerInterface;
12
use ReflectionFunctionAbstract as ContextFunction;
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
use Spiral\Core\Exceptions\Container\NotFoundException;
21
use Spiral\Core\Exceptions\LogicException;
22
23
/**
24
 * 500 lines of code size auto-wiring container with declarative singletons, contextual injections,
25
 * bindings, lazy factories and Container Interop compatible. :)
26
 *
27
 * Container does not support setter injections, private properties and etc. Normally it will work
28
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
29
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy factory.
30
 *
31
 * You can use injectors to delegate class resolution to external container.
32
 *
33
 * @see \Spiral\Core\Container::registerInstance() to add your own behaviours.
34
 *
35
 * @see InjectableInterface
36
 * @see SingletonInterface
37
 */
38
class Container extends Component implements ContainerInterface, FactoryInterface, ResolverInterface
39
{
40
    /**
41
     * IoC bindings.
42
     *
43
     * @what-if private
44
     * @invisible
45
     *
46
     * @var array
47
     */
48
    protected $bindings = [
49
        ContainerInterface::class => self::class,
50
        FactoryInterface::class   => self::class,
51
        ResolverInterface::class  => self::class
52
    ];
53
54
    /**
55
     * Registered injectors.
56
     *
57
     * @what-if private
58
     * @invisible
59
     *
60
     * @var array
61
     */
62
    protected $injectors = [];
63
64
    /**
65
     * Container constructor.
66
     */
67
    public function __construct()
68
    {
69
        $this->bindings[static::class] = self::class;
70
        $this->bindings[self::class] = $this;
71
    }
72
73
    /**
74
     * Container can not be cloned.
75
     */
76
    public function __clone()
77
    {
78
        throw new LogicException("Container is not clonable");
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function has($alias)
85
    {
86
        return array_key_exists($alias, $this->bindings);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     *
92
     * Context parameter will be passed to class injectors, which makes possible to use this method
93
     * as:
94
     * $this->container->get(DatabaseInterface::class, 'default');
95
     *
96
     * @param string|null $context Call context.
97
     *
98
     * @throws ContainerException
99
     */
100
    public function get($alias, $context = null)
101
    {
102
        //Direct bypass to construct, i might think about this option... or not.
103
        return $this->make($alias, [], $context);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     *
109
     * @param string|null $context Related to parameter caused injection if any.
110
     */
111
    final public function make(string $class, $parameters = [], string $context = null)
112
    {
113
        if (!isset($this->bindings[$class])) {
114
            //No direct instructions how to construct class, make is automatically
115
            return $this->autowire($class, $parameters, $context);
116
        }
117
118
        if (is_object($binding = $this->bindings[$class])) {
119
            //When binding is instance, assuming singleton
120
            return $binding;
121
        }
122
123
        if (is_string($binding)) {
124
            //Binding is pointing to something else
125
            return $this->make($binding, $parameters, $context);
126
        }
127
128
        if (is_string($binding[0])) {
129
            //Class name
130
            $instance = $this->make($binding[0], $parameters, $context);
131
        } elseif ($binding[0] instanceof \Closure) {
132
            $reflection = new \ReflectionFunction($binding[0]);
133
134
            //Invoking Closure with resolved arguments
135
            $instance = $reflection->invokeArgs(
136
                $this->resolveArguments($reflection, $parameters, $context)
137
            );
138
        } elseif (is_array($binding[0]) && isset($binding[0][1])) {
139
            //In a form of resolver and method
140
            list($resolver, $method) = $binding[0];
141
142
            //Resolver instance (i.e. [ClassName::class, 'method'])
143
            $resolver = $this->get($resolver);
144
            $method = new \ReflectionMethod($resolver, $method);
145
            $method->setAccessible(true);
146
147
            //Invoking factory method with resolved arguments
148
            $instance = $method->invokeArgs(
149
                $resolver,
150
                $this->resolveArguments($method, $parameters, $context)
151
            );
152
        } else {
153
            //No idea what was this binding was
154
            throw new ContainerException("Invalid binding for '{$class}'");
155
        }
156
157
        if ($binding[1]) {
158
            //Declared singleton
159
            $this->bindings[$class] = $instance;
160
        }
161
162
        if (!is_object($instance)) {
163
            //Non object bindings are allowed
164
            return $instance;
165
        }
166
167
        return $instance;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     *
173
     * @param string $context
174
     */
175
    final public function resolveArguments(
176
        ContextFunction $reflection,
177
        array $parameters = [],
178
        string $context = null
179
    ): array {
180
        $arguments = [];
181
        foreach ($reflection->getParameters() as $parameter) {
182
            try {
183
                //Information we need to know about argument in order to resolve it's value
184
                $name = $parameter->getName();
185
                $class = $parameter->getClass();
186
            } catch (\Throwable $e) {
187
                //Possibly invalid class definition or syntax error
188
                throw new ContainerException($e->getMessage(), $e->getCode(), $e);
189
            }
190
191
            //No declared type or scalar type or array
192
            if (empty($class)) {
193
                //Provided from outside
194
                if (array_key_exists($name, $parameters)) {
195
                    //Make sure it's properly typed
196
                    $this->assertType($parameter, $reflection, $parameters[$name]);
197
                    $arguments[] = $parameters[$name];
198
199
                    continue;
200
                }
201
202
                if ($parameter->isDefaultValueAvailable()) {
203
                    //Default value
204
                    $arguments[] = $parameter->getDefaultValue();
205
                    continue;
206
                }
207
208
                //Unable to resolve scalar argument value
209
                throw new ArgumentException($parameter, $reflection);
210
            }
211
212
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
213
                //Supplied by user but only as object!
214
                $arguments[] = $parameters[$name];
215
                continue;
216
            }
217
218
            try {
219
                //Trying to resolve dependency (contextually)
220
                $arguments[] = $this->get($class->getName(), $name);
221
222
                continue;
223
            } catch (AutowireException $e) {
224
                if ($parameter->isOptional()) {
225
                    //This is optional dependency, skip
226
                    $arguments[] = null;
227
                    continue;
228
                }
229
230
                throw $e;
231
            }
232
        }
233
234
        return $arguments;
235
    }
236
237
    /**
238
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
239
     * for each method call), function array or Closure (executed every call). Only object resolvers
240
     * supported by this method.
241
     *
242
     * @param string                $alias
243
     * @param string|array|callable $resolver
244
     *
245
     * @return self
246
     */
247
    final public function bind(string $alias, $resolver): Container
248
    {
249
        if (is_array($resolver) || $resolver instanceof \Closure) {
250
            //Array means = execute me, false = not singleton
251
            $this->bindings[$alias] = [$resolver, false];
252
253
            return $this;
254
        }
255
256
        $this->bindings[$alias] = $resolver;
257
258
        return $this;
259
    }
260
261
    /**
262
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
263
     * (will be constructed only once), function array or Closure (executed only once call).
264
     *
265
     * @param string                $alias
266
     * @param string|array|callable $resolver
267
     *
268
     * @return self
269
     */
270
    final public function bindSingleton(string $alias, $resolver): Container
271
    {
272
        if (is_object($resolver) && !$resolver instanceof \Closure) {
273
            //Direct binding to an instance
274
            $this->bindings[$alias] = $resolver;
275
276
            return $this;
277
        }
278
279
        $this->bindings[$alias] = [$resolver, true];
280
281
        return $this;
282
    }
283
284
    /**
285
     * Specify binding which has to be used for class injection.
286
     *
287
     * @param string        $class
288
     * @param string|object $injector
289
     *
290
     * @return self
291
     */
292
    public function bindInjector(string $class, $injector): Container
293
    {
294
        if (!is_string($injector)) {
295
            throw new \InvalidArgumentException('Injector can only be set as string binding');
296
        }
297
298
        $this->injectors[$class] = $injector;
299
300
        return $this;
301
    }
302
303
    /**
304
     * Check if given class has associated injector.
305
     *
306
     * @param \ReflectionClass $reflection
307
     *
308
     * @return bool
309
     */
310
    public function hasInjector(\ReflectionClass $reflection): bool
311
    {
312
        if (isset($this->injectors[$reflection->getName()])) {
313
            return true;
314
        }
315
316
        //Auto injection!
317
        return $reflection->isSubclassOf(InjectableInterface::class);
318
    }
319
320
    /**
321
     * Check if alias points to constructed instance (singleton).
322
     *
323
     * @param string $alias
324
     *
325
     * @return bool
326
     */
327
    final public function hasInstance(string $alias): bool
328
    {
329
        if (!$this->has($alias)) {
330
            return false;
331
        }
332
333
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
334
            //Checking alias tree
335
            $alias = $this->bindings[$alias];
336
        }
337
338
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
339
    }
340
341
    /**
342
     * @param string $alias
343
     */
344
    final public function removeBinding(string $alias)
345
    {
346
        unset($this->bindings[$alias]);
347
    }
348
349
    /**
350
     * @param string $class
351
     */
352
    final public function removeInjector(string $class)
353
    {
354
        unset($this->injectors[$class]);
355
    }
356
357
    /**
358
     * Every declared Container binding. Must not be used in production code due container format is
359
     * vary.
360
     *
361
     * @return array
362
     */
363
    final public function getBindings(): array
364
    {
365
        return $this->bindings;
366
    }
367
368
    /**
369
     * Every binded injector.
370
     *
371
     * @return array
372
     */
373
    final public function getInjectors(): array
374
    {
375
        return $this->injectors;
376
    }
377
378
    /**
379
     * Automatically create class.
380
     *
381
     * @param string $class
382
     * @param array  $parameters
383
     * @param string $context
384
     *
385
     * @return object
386
     *
387
     * @throws AutowireException
388
     */
389
    final protected function autowire(string $class, array $parameters, string $context = null)
390
    {
391
        try {
392
            if (!class_exists($class)) {
393
                throw new NotFoundException("Undefined class or binding '{$class}'");
394
            }
395
        } catch (\Error $e) {
396
            //Issues with syntax or class definition
397
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
398
        }
399
400
        //Automatically create instance
401
        $instance = $this->createInstance($class, $parameters, $context);
402
403
        //Apply registration functions to created instance
404
        return $this->registerInstance($instance, $parameters);
405
    }
406
407
    /**
408
     * Register instance in container, might perform methods like auto-singletons, log populations
409
     * and etc. Can be extended.
410
     *
411
     * @param object $instance   Created object.
412
     * @param array  $parameters Parameters which been passed with created instance.
413
     *
414
     * @return object
415
     */
416
    protected function registerInstance($instance, array $parameters)
417
    {
418
        //Declarative singletons
419
        if (empty($parameters) && $instance instanceof SingletonInterface) {
420
            $alias = get_class($instance);
421
422
            if (!isset($this->bindings[$alias])) {
423
                $this->bindings[$alias] = $instance;
424
            }
425
        }
426
427
        //Your code can go here (for example LoggerAwareInterface, custom hydration and etc)
428
429
        return $instance;
430
    }
431
432
    /**
433
     * Create instance of desired class.
434
     *
435
     * @param string      $class
436
     * @param array       $parameters Constructor parameters.
437
     * @param string|null $context
438
     *
439
     * @return object
440
     *
441
     * @throws ContainerException
442
     */
443
    private function createInstance(string $class, array $parameters, string $context = null)
444
    {
445
        $reflection = new \ReflectionClass($class);
446
447
        //We have to construct class using external injector
448
        if (empty($parameters) && $this->hasInjector($reflection)) {
449
            //Creating class using injector/factory
450
            $instance = $this->getInjector($reflection)->createInjection($reflection, $context);
451
452
            if (!$reflection->isInstance($instance)) {
453
                throw new InjectionException("Invalid injection response for '{$reflection->getName()}'");
454
            }
455
456
            return $instance;
457
        }
458
459
        if (!$reflection->isInstantiable()) {
460
            throw new ContainerException("Class '{$class}' can not be constructed");
461
        }
462
463
        if (!empty($constructor = $reflection->getConstructor())) {
464
            //Using constructor with resolved arguments
465
            $instance = $reflection->newInstanceArgs(
466
                $this->resolveArguments($constructor, $parameters)
467
            );
468
        } else {
469
            //No constructor specified
470
            $instance = $reflection->newInstance();
471
        }
472
473
        return $instance;
474
    }
475
476
    /**
477
     * Get injector associated with given class.
478
     *
479
     * @param \ReflectionClass $reflection
480
     *
481
     * @return InjectorInterface
482
     */
483
    private function getInjector(\ReflectionClass $reflection): InjectorInterface
484
    {
485
        if (isset($this->injectors[$reflection->getName()])) {
486
            //Stated directly
487
            $injector = $this->get($this->injectors[$reflection->getName()]);
488
        } else {
489
            //Auto-injection!
490
            $injector = $this->get($reflection->getConstant('INJECTOR'));
491
        }
492
493
        if (!$injector instanceof InjectorInterface) {
494
            throw new InjectionException(
495
                "Class '" . get_class($injector) . "' must be an instance of InjectorInterface for '{$reflection->getName()}'"
496
            );
497
        }
498
499
        return $injector;
500
    }
501
502
    /**
503
     * Assert that given value are matched parameter type.
504
     *
505
     * @param \ReflectionParameter        $parameter
506
     * @param \ReflectionFunctionAbstract $context
507
     * @param mixed                       $value
508
     *
509
     * @throws ArgumentException
510
     */
511
    private function assertType(
512
        \ReflectionParameter $parameter,
513
        \ReflectionFunctionAbstract $context,
514
        $value
515
    ) {
516
        if (is_null($value)) {
517
            if (!$parameter->isOptional()) {
518
                throw new ArgumentException($parameter, $context);
519
            }
520
521
            return;
522
        }
523
524
        $type = $parameter->getType();
525
526
        if ($type == 'array' && !is_array($value)) {
527
            throw new ArgumentException($parameter, $context);
528
        }
529
530
        if (($type == 'int' || $type == 'float') && !is_numeric($value)) {
531
            throw new ArgumentException($parameter, $context);
532
        }
533
534
        if ($type == 'bool' && !is_bool($value) && !is_numeric($value)) {
535
            throw new ArgumentException($parameter, $context);
536
        }
537
    }
538
}