Completed
Branch develop (f7dc53)
by Anton
05:49
created

Container::bindInjector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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