Completed
Push — 4.0 ( b59aea...80f83b )
by Loz
52s queued 21s
created

Injector   F

Complexity

Total Complexity 140

Size/Duplication

Total Lines 986
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 280
dl 0
loc 986
rs 2
c 0
b 0
f 0
wmc 140

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectCreator() 0 3 1
A addAutoProperty() 0 4 1
A setConfigLocator() 0 3 1
A setAutoScanProperties() 0 3 1
A updateSpecConstructor() 0 4 2
A inst() 0 3 1
A setObjectCreator() 0 3 1
A getConfigLocator() 0 3 1
A has() 0 3 1
A createWithArgs() 0 3 1
A __get() 0 3 1
F load() 0 65 16
A unregisterObjects() 0 20 6
A hasService() 0 5 1
A normaliseArguments() 0 17 4
C instantiate() 0 43 13
A getServiceNamedSpec() 0 14 2
B convertServiceProperty() 0 29 8
A getServiceSpec() 0 21 6
A unnest() 0 13 2
A __construct() 0 22 4
A registerService() 0 10 2
B getNamedService() 0 29 7
A nest() 0 6 1
A setInjectMapping() 0 7 2
A get() 0 9 2
A updateSpec() 0 15 5
A getServiceName() 0 14 3
A unregisterNamedObject() 0 5 1
A setObjectProperty() 0 6 2
A create() 0 5 1
F inject() 0 152 40

How to fix   Complexity   

Complex Class

Complex classes like Injector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Injector, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Core\Injector;
4
5
use ArrayObject;
6
use InvalidArgumentException;
7
use Psr\Container\ContainerInterface;
8
use Psr\Container\NotFoundExceptionInterface;
9
use ReflectionMethod;
10
use ReflectionObject;
11
use ReflectionProperty;
12
use SilverStripe\Core\ClassInfo;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Core\Environment;
15
use SilverStripe\Dev\Deprecation;
16
17
/**
18
 * A simple injection manager that manages creating objects and injecting
19
 * dependencies between them. It borrows quite a lot from ideas taken from
20
 * Spring's configuration, but is adapted to the stateless PHP way of doing
21
 * things.
22
 *
23
 * In its simplest form, the dependency injector can be used as a mechanism to
24
 * instantiate objects. Simply call
25
 *
26
 * Injector::inst()->get('ClassName')
27
 *
28
 * and a new instance of ClassName will be created and returned to you.
29
 *
30
 * Classes can have specific configuration defined for them to
31
 * indicate dependencies that should be injected. This takes the form of
32
 * a static variable $dependencies defined in the class (or configuration),
33
 * which indicates the name of a property that should be set.
34
 *
35
 * eg
36
 *
37
 * <code>
38
 * class MyController extends Controller {
39
 *
40
 *      public $permissions;
41
 *      public $defaultText;
42
 *
43
 *      static $dependencies = array(
44
 *          'defaultText'       => 'Override in configuration',
45
 *          'permissions'       => '%$PermissionService',
46
 *      );
47
 * }
48
 * </code>
49
 *
50
 * will result in an object of type MyController having the defaultText property
51
 * set to 'Override in configuration', and an object identified
52
 * as PermissionService set into the property called 'permissions'. The %$
53
 * syntax tells the injector to look the provided name up as an item to be created
54
 * by the Injector itself.
55
 *
56
 * A key concept of the injector is whether to manage the object as
57
 *
58
 * * A pseudo-singleton, in that only one item will be created for a particular
59
 *   identifier (but the same class could be used for multiple identifiers)
60
 * * A prototype, where the same configuration is used, but a new object is
61
 *   created each time
62
 * * unmanaged, in which case a new object is created and injected, but no
63
 *   information about its state is managed.
64
 *
65
 * Additional configuration of items managed by the injector can be done by
66
 * providing configuration for the types, either by manually loading in an
67
 * array describing the configuration, or by specifying the configuration
68
 * for a type via SilverStripe's configuration mechanism.
69
 *
70
 * Specify a configuration array of the format
71
 *
72
 * <code>
73
 * array(
74
 *      array(
75
 *          'id'            => 'BeanId',                    // the name to be used if diff from the filename
76
 *          'priority'      => 1,                           // priority. If another bean is defined with the same ID,
77
 *                                                          // but has a lower priority, it is NOT overridden
78
 *          'class'         => 'ClassName',                 // the name of the PHP class
79
 *          'src'           => '/path/to/file'              // the location of the class
80
 *          'type'          => 'singleton|prototype'        // if you want prototype object generation, set it as the
81
 *                                                          // type
82
 *                                                          // By default, singleton is assumed
83
 *
84
 *          'factory' => 'FactoryService'                   // A factory service to use to create instances.
85
 *          'construct'     => array(                       // properties to set at construction
86
 *              'scalar',
87
 *              '%$BeanId',
88
 *          )
89
 *          'properties'    => array(
90
 *              'name' => 'value'                           // scalar value
91
 *              'name' => '%$BeanId',                       // a reference to another bean
92
 *              'name' => array(
93
 *                  'scalar',
94
 *                  '%$BeanId'
95
 *              )
96
 *          )
97
 *      )
98
 *      // alternatively
99
 *      'MyBean'        => array(
100
 *          'class'         => 'ClassName',
101
 *      )
102
 *      // or simply
103
 *      'OtherBean'     => 'SomeClass',
104
 * )
105
 * </code>
106
 *
107
 * In addition to specifying the bindings directly in the configuration,
108
 * you can simply create a publicly accessible property on the target
109
 * class which will automatically be injected if the autoScanProperties
110
 * option is set to true. This means a class defined as
111
 *
112
 * <code>
113
 * class MyController extends Controller {
114
 *
115
 *      private $permissionService;
116
 *
117
 *      public setPermissionService($p) {
118
 *          $this->permissionService = $p;
119
 *      }
120
 * }
121
 * </code>
122
 *
123
 * will have setPermissionService called if
124
 *
125
 * * Injector::inst()->setAutoScanProperties(true) is called and
126
 * * A service named 'PermissionService' has been configured
127
 *
128
 * @author [email protected]
129
 * @license BSD License http://silverstripe.org/bsd-license/
130
 */
131
class Injector implements ContainerInterface
132
{
133
134
    /**
135
     * Local store of all services
136
     *
137
     * @var array
138
     */
139
    private $serviceCache;
140
141
    /**
142
     * Cache of items that need to be mapped for each service that gets injected
143
     *
144
     * @var array
145
     */
146
    private $injectMap;
147
148
    /**
149
     * A store of all the service configurations that have been defined.
150
     *
151
     * @var array
152
     */
153
    private $specs;
154
155
    /**
156
     * A map of all the properties that should be automagically set on all
157
     * objects instantiated by the injector
158
     */
159
    private $autoProperties;
160
161
    /**
162
     * A singleton if you want to use it that way
163
     *
164
     * @var Injector
165
     */
166
    private static $instance;
167
168
    /**
169
     * Indicates whether or not to automatically scan properties in injected objects to auto inject
170
     * stuff, similar to the way grails does things.
171
     *
172
     * @var boolean
173
     */
174
    private $autoScanProperties = false;
175
176
    /**
177
     * The default factory used to create new instances.
178
     *
179
     * The {@link InjectionCreator} is used by default, which simply directly
180
     * creates objects. This can be changed to use a different default creation
181
     * method if desired.
182
     *
183
     * Each individual component can also specify a custom factory to use by
184
     * using the `factory` parameter.
185
     *
186
     * @var Factory
187
     */
188
    protected $objectCreator;
189
190
    /**
191
     * Locator for determining Config properties for services
192
     *
193
     * @var ServiceConfigurationLocator
194
     */
195
    protected $configLocator;
196
197
    /**
198
     * Specify a service type singleton
199
     */
200
    const SINGLETON = 'singleton';
201
202
    /**
203
     * Specif ya service type prototype
204
     */
205
    const PROTOTYPE = 'prototype';
206
207
    /**
208
     * Create a new injector.
209
     *
210
     * @param array $config
211
     *              Service configuration
212
     */
213
    public function __construct($config = null)
214
    {
215
        $this->injectMap = array();
216
        $this->serviceCache = array(
217
            'Injector' => $this,
218
        );
219
        $this->specs = [
220
            'Injector' => ['class' => static::class]
221
        ];
222
        $this->autoProperties = array();
223
        $creatorClass = isset($config['creator'])
224
            ? $config['creator']
225
            : InjectionCreator::class;
226
        $locatorClass = isset($config['locator'])
227
            ? $config['locator']
228
            : SilverStripeServiceConfigurationLocator::class;
229
230
        $this->objectCreator = new $creatorClass;
231
        $this->configLocator = new $locatorClass;
232
233
        if ($config) {
234
            $this->load($config);
235
        }
236
    }
237
238
    /**
239
     * The injector instance this one was copied from when Injector::nest() was called.
240
     *
241
     * @var Injector
242
     */
243
    protected $nestedFrom = null;
244
245
    /**
246
     * @return Injector
247
     */
248
    public static function inst()
249
    {
250
        return InjectorLoader::inst()->getManifest();
251
    }
252
253
    /**
254
     * Make the newly active {@link Injector} be a copy of the current active
255
     * {@link Injector} instance.
256
     *
257
     * You can then make changes to the injector with methods such as
258
     * {@link Injector::inst()->registerService()} which will be discarded
259
     * upon a subsequent call to {@link Injector::unnest()}
260
     *
261
     * @return Injector Reference to new active Injector instance
262
     */
263
    public static function nest()
264
    {
265
        // Clone current injector and nest
266
        $new = clone self::inst();
267
        InjectorLoader::inst()->pushManifest($new);
268
        return $new;
269
    }
270
271
    /**
272
     * Change the active Injector back to the Injector instance the current active
273
     * Injector object was copied from.
274
     *
275
     * @return Injector Reference to restored active Injector instance
276
     */
277
    public static function unnest()
278
    {
279
        // Unnest unless we would be left at 0 manifests
280
        $loader = InjectorLoader::inst();
281
        if ($loader->countManifests() <= 1) {
282
            user_error(
283
                "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
284
                E_USER_WARNING
285
            );
286
        } else {
287
            $loader->popManifest();
288
        }
289
        return static::inst();
290
    }
291
292
    /**
293
     * Indicate whether we auto scan injected objects for properties to set.
294
     *
295
     * @param boolean $val
296
     */
297
    public function setAutoScanProperties($val)
298
    {
299
        $this->autoScanProperties = $val;
300
    }
301
302
    /**
303
     * Sets the default factory to use for creating new objects.
304
     *
305
     * @param \SilverStripe\Core\Injector\Factory $obj
306
     */
307
    public function setObjectCreator(Factory $obj)
308
    {
309
        $this->objectCreator = $obj;
310
    }
311
312
    /**
313
     * @return Factory
314
     */
315
    public function getObjectCreator()
316
    {
317
        return $this->objectCreator;
318
    }
319
320
    /**
321
     * Set the configuration locator
322
     * @param ServiceConfigurationLocator $configLocator
323
     */
324
    public function setConfigLocator($configLocator)
325
    {
326
        $this->configLocator = $configLocator;
327
    }
328
329
    /**
330
     * Retrieve the configuration locator
331
     * @return ServiceConfigurationLocator
332
     */
333
    public function getConfigLocator()
334
    {
335
        return $this->configLocator;
336
    }
337
338
    /**
339
     * Add in a specific mapping that should be catered for on a type.
340
     * This allows configuration of what should occur when an object
341
     * of a particular type is injected, and what items should be injected
342
     * for those properties / methods.
343
     *
344
     * @param string $class The class to set a mapping for
345
     * @param string $property The property to set the mapping for
346
     * @param string $toInject The registered type that will be injected
347
     * @param string $injectVia Whether to inject by setting a property or calling a setter
348
     */
349
    public function setInjectMapping($class, $property, $toInject, $injectVia = 'property')
350
    {
351
        $mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
352
353
        $mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
354
355
        $this->injectMap[$class] = $mapping;
356
    }
357
358
    /**
359
     * Add an object that should be automatically set on managed objects
360
     *
361
     * This allows you to specify, for example, that EVERY managed object
362
     * will be automatically inject with a log object by the following
363
     *
364
     * $injector->addAutoProperty('log', new Logger());
365
     *
366
     * @param string $property
367
     *                the name of the property
368
     * @param object $object
369
     *                the object to be set
370
     * @return $this
371
     */
372
    public function addAutoProperty($property, $object)
373
    {
374
        $this->autoProperties[$property] = $object;
375
        return $this;
376
    }
377
378
    /**
379
     * Load services using the passed in configuration for those services
380
     *
381
     * @param array $config
382
     * @return $this
383
     */
384
    public function load($config = array())
385
    {
386
        foreach ($config as $specId => $spec) {
387
            if (is_string($spec)) {
388
                $spec = array('class' => $spec);
389
            }
390
391
            $file = isset($spec['src']) ? $spec['src'] : null;
392
393
            // class is whatever's explicitly set,
394
            $class = isset($spec['class']) ? $spec['class'] : null;
395
396
            // or the specid if nothing else available.
397
            if (!$class && is_string($specId)) {
398
                $class = $specId;
399
            }
400
401
            // make sure the class is set...
402
            if (empty($class)) {
403
                throw new InvalidArgumentException('Missing spec class');
404
            }
405
            $spec['class'] = $class;
406
407
            $id = is_string($specId)
408
                ? $specId
409
                : (isset($spec['id']) ? $spec['id'] : $class);
410
411
            $priority = isset($spec['priority']) ? $spec['priority'] : 1;
412
413
            // see if we already have this defined. If so, check priority weighting
414
            if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
415
                if ($this->specs[$id]['priority'] > $priority) {
416
                    return $this;
417
                }
418
            }
419
420
            // okay, actually include it now we know we're going to use it
421
            if (file_exists($file)) {
422
                require_once $file;
423
            }
424
425
            // make sure to set the id for later when instantiating
426
            // to ensure we get cached
427
            $spec['id'] = $id;
428
429
//          We've removed this check because new functionality means that the 'class' field doesn't need to refer
430
//          specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
431
//          functionality
432
//
433
//          if (!class_exists($class)) {
434
//              throw new Exception("Failed to load '$class' from $file");
435
//          }
436
437
            // store the specs for now - we lazy load on demand later on.
438
            $this->specs[$id] = $spec;
439
440
            // EXCEPT when there's already an existing instance at this id.
441
            // if so, we need to instantiate and replace immediately
442
            if (isset($this->serviceCache[$id])) {
443
                $this->updateSpecConstructor($spec);
444
                $this->instantiate($spec, $id);
445
            }
446
        }
447
448
        return $this;
449
    }
450
451
    /**
452
     * Update the configuration of an already defined service
453
     *
454
     * Use this if you don't want to register a complete new config, just append
455
     * to an existing configuration. Helpful to avoid overwriting someone else's changes
456
     *
457
     * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
458
     *
459
     * @param string $id
460
     *              The name of the service to update the definition for
461
     * @param string $property
462
     *              The name of the property to update.
463
     * @param mixed $value
464
     *              The value to set
465
     * @param boolean $append
466
     *              Whether to append (the default) when the property is an array
467
     */
468
    public function updateSpec($id, $property, $value, $append = true)
469
    {
470
        if (isset($this->specs[$id]['properties'][$property])) {
471
            // by ref so we're updating the actual value
472
            $current = &$this->specs[$id]['properties'][$property];
473
            if (is_array($current) && $append) {
474
                $current[] = $value;
475
            } else {
476
                $this->specs[$id]['properties'][$property] = $value;
477
            }
478
479
            // and reload the object; existing bindings don't get
480
            // updated though! (for now...)
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
481
            if (isset($this->serviceCache[$id])) {
482
                $this->instantiate(array('class'=>$id), $id);
483
            }
484
        }
485
    }
486
487
    /**
488
     * Update a class specification to convert constructor configuration information if needed
489
     *
490
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
491
     *
492
     * @param array $spec
493
     *          The class specification to update
494
     */
495
    protected function updateSpecConstructor(&$spec)
496
    {
497
        if (isset($spec['constructor'])) {
498
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
499
        }
500
    }
501
502
    /**
503
     * Recursively convert a value into its proper representation with service references
504
     * resolved to actual objects
505
     *
506
     * @param string $value
507
     * @return array|mixed|string
508
     */
509
    public function convertServiceProperty($value)
510
    {
511
        if (is_array($value)) {
0 ignored issues
show
introduced by
The condition is_array($value) is always false.
Loading history...
512
            $newVal = array();
513
            foreach ($value as $k => $v) {
514
                $newVal[$k] = $this->convertServiceProperty($v);
515
            }
516
            return $newVal;
517
        }
518
519
        // Evaluate service references
520
        if (is_string($value) && strpos($value, '%$') === 0) {
521
            $id = substr($value, 2);
522
            return $this->get($id);
523
        }
524
525
        // Evaluate constants surrounded by back ticks
526
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
527
            $envValue = Environment::getEnv($matches['name']);
528
            if ($envValue !== false) {
529
                $value = $envValue;
530
            } elseif (defined($matches['name'])) {
531
                $value = constant($matches['name']);
532
            } else {
533
                $value = null;
534
            }
535
        }
536
537
        return $value;
538
    }
539
540
    /**
541
     * Instantiate a managed object
542
     *
543
     * Given a specification of the form
544
     *
545
     * array(
546
     *        'class' => 'ClassName',
547
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
548
     *        'id' => 'ServiceId',
549
     *        'type' => 'singleton|prototype'
550
     * )
551
     *
552
     * will create a new object, store it in the service registry, and
553
     * set any relevant properties
554
     *
555
     * Optionally, you can pass a class name directly for creation
556
     *
557
     * To access this from the outside, you should call ->get('Name') to ensure
558
     * the appropriate checks are made on the specific type.
559
     *
560
     *
561
     * @param array $spec
562
     *                The specification of the class to instantiate
563
     * @param string $id
564
     *                The name of the object being created. If not supplied, then the id will be inferred from the
565
     *                object being created
566
     * @param string $type
567
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
568
     *                wants the object to be returned
569
     * @return object
570
     */
571
    protected function instantiate($spec, $id = null, $type = null)
572
    {
573
        if (is_string($spec)) {
0 ignored issues
show
introduced by
The condition is_string($spec) is always false.
Loading history...
574
            $spec = array('class' => $spec);
575
        }
576
        $class = $spec['class'];
577
578
        // create the object, using any constructor bindings
579
        $constructorParams = array();
580
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
581
            $constructorParams = $spec['constructor'];
582
        }
583
584
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
585
        $object = $factory->create($class, $constructorParams);
586
587
        // Handle empty factory responses
588
        if (!$object) {
0 ignored issues
show
introduced by
$object is of type object, thus it always evaluated to true.
Loading history...
589
            return null;
590
        }
591
592
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
593
        // that we don't manage directly; we don't want to store these in the service cache below
594
        if (!$id) {
595
            $id = isset($spec['id']) ? $spec['id'] : null;
596
        }
597
598
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
599
        // created anew each time
600
        if (!$type) {
601
            $type = isset($spec['type']) ? $spec['type'] : null;
602
        }
603
604
        if ($id && (!$type || $type !== self::PROTOTYPE)) {
605
            // this ABSOLUTELY must be set before the object is injected.
606
            // This prevents circular reference errors down the line
607
            $this->serviceCache[$id] = $object;
608
        }
609
610
        // now inject safely
611
        $this->inject($object, $id);
612
613
        return $object;
614
    }
615
616
    /**
617
     * Inject $object with available objects from the service cache
618
     *
619
     * @todo Track all the existing objects that have had a service bound
620
     * into them, so we can update that binding at a later point if needbe (ie
621
     * if the managed service changes)
622
     *
623
     * @param object $object
624
     *              The object to inject
625
     * @param string $asType
626
     *              The ID this item was loaded as. This is so that the property configuration
627
     *              for a type is referenced correctly in case $object is no longer the same
628
     *              type as the loaded config specification had it as.
629
     */
630
    public function inject($object, $asType = null)
631
    {
632
        $objtype = $asType ? $asType : get_class($object);
633
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
634
635
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
636
637
        // first off, set any properties defined in the service specification for this
638
        // object type
639
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
640
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
641
                $val = $this->convertServiceProperty($value);
642
                $this->setObjectProperty($object, $key, $val);
643
            }
644
        }
645
646
        // Populate named methods
647
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
648
            foreach ($spec['calls'] as $method) {
649
                // Ignore any blank entries from the array; these may be left in due to config system limitations
650
                if (!$method) {
651
                    continue;
652
                }
653
654
                // Format validation
655
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
656
                    throw new InvalidArgumentException(
657
                        "'calls' entries in service definition should be 1 or 2 element arrays."
658
                    );
659
                }
660
                if (!is_string($method[0])) {
661
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
662
                }
663
                if (isset($method[1]) && !is_array($method[1])) {
664
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
665
                }
666
667
                // Check that the method exists and is callable
668
                $objectMethod = array($object, $method[0]);
669
                if (!is_callable($objectMethod)) {
670
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
671
                }
672
673
                // Call it
674
                call_user_func_array(
675
                    $objectMethod,
676
                    $this->convertServiceProperty(
677
                        isset($method[1]) ? $method[1] : array()
0 ignored issues
show
Bug introduced by
It seems like IssetNode ? $method[1] : array() can also be of type array; however, parameter $value of SilverStripe\Core\Inject...onvertServiceProperty() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

677
                        /** @scrutinizer ignore-type */ isset($method[1]) ? $method[1] : array()
Loading history...
678
                    )
679
                );
680
            }
681
        }
682
683
        // now, use any cached information about what properties this object type has
684
        // and set based on name resolution
685
        if ($mapping === null) {
686
            // we use an object to prevent array copies if/when passed around
687
            $mapping = new ArrayObject();
688
689
            if ($this->autoScanProperties) {
690
                // This performs public variable based injection
691
                $robj = new ReflectionObject($object);
692
                $properties = $robj->getProperties();
693
694
                foreach ($properties as $propertyObject) {
695
                    /* @var $propertyObject ReflectionProperty */
696
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
697
                        $origName = $propertyObject->getName();
698
                        $name = ucfirst($origName);
699
                        if ($this->has($name)) {
700
                            // Pull the name out of the registry
701
                            $value = $this->get($name);
702
                            $propertyObject->setValue($object, $value);
703
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
704
                        }
705
                    }
706
                }
707
708
                // and this performs setter based injection
709
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
710
711
                foreach ($methods as $methodObj) {
712
                    /* @var $methodObj ReflectionMethod */
713
                    $methName = $methodObj->getName();
714
                    if (strpos($methName, 'set') === 0) {
715
                        $pname = substr($methName, 3);
716
                        if ($this->has($pname)) {
717
                            // Pull the name out of the registry
718
                            $value = $this->get($pname);
719
                            $methodObj->invoke($object, $value);
720
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
721
                        }
722
                    }
723
                }
724
            }
725
726
            $injections = Config::inst()->get(get_class($object), 'dependencies');
727
            // If the type defines some injections, set them here
728
            if ($injections && count($injections)) {
729
                foreach ($injections as $property => $value) {
730
                    // we're checking empty in case it already has a property at this name
731
                    // this doesn't catch privately set things, but they will only be set by a setter method,
732
                    // which should be responsible for preventing further setting if it doesn't want it.
733
                    if (empty($object->$property)) {
734
                        $convertedValue = $this->convertServiceProperty($value);
735
                        $this->setObjectProperty($object, $property, $convertedValue);
736
                        $mapping[$property] = array('service' => $value, 'type' => 'service');
737
                    }
738
                }
739
            }
740
741
            // we store the information about what needs to be injected for objects of this
742
            // type here
743
            $this->injectMap[$objtype] = $mapping;
744
        } else {
745
            foreach ($mapping as $prop => $propSpec) {
746
                switch ($propSpec['type']) {
747
                    case 'property':
748
                        $value = $this->get($propSpec['name']);
749
                        $object->$prop = $value;
750
                        break;
751
752
753
                    case 'method':
754
                        $method = $prop;
755
                        $value = $this->get($propSpec['name']);
756
                        $object->$method($value);
757
                        break;
758
759
                    case 'service':
760
                        if (empty($object->$prop)) {
761
                            $value = $this->convertServiceProperty($propSpec['service']);
762
                            $this->setObjectProperty($object, $prop, $value);
763
                        }
764
                        break;
765
766
                    default:
767
                        throw new \LogicException("Bad mapping type: " . $propSpec['type']);
768
                }
769
            }
770
        }
771
772
        foreach ($this->autoProperties as $property => $value) {
773
            if (!isset($object->$property)) {
774
                $value = $this->convertServiceProperty($value);
775
                $this->setObjectProperty($object, $property, $value);
776
            }
777
        }
778
779
        // Call the 'injected' method if it exists
780
        if (method_exists($object, 'injected')) {
781
            $object->injected();
782
        }
783
    }
784
785
    /**
786
     * Helper to set a property's value
787
     *
788
     * @param object $object
789
     *                  Set an object's property to a specific value
790
     * @param string $name
791
     *                  The name of the property to set
792
     * @param mixed $value
793
     *                  The value to set
794
     */
795
    protected function setObjectProperty($object, $name, $value)
796
    {
797
        if (ClassInfo::hasMethod($object, 'set' . $name)) {
798
            $object->{'set' . $name}($value);
799
        } else {
800
            $object->$name = $value;
801
        }
802
    }
803
804
    /**
805
     * @deprecated 4.0.0:5.0.0 Use Injector::has() instead
806
     * @param $name
807
     * @return string
808
     */
809
    public function hasService($name)
810
    {
811
        Deprecation::notice('5.0', 'Use Injector::has() instead');
812
813
        return $this->has($name);
814
    }
815
816
    /**
817
     * Does the given service exist?
818
     *
819
     * We do a special check here for services that are using compound names. For example,
820
     * we might want to say that a property should be injected with Log.File or Log.Memory,
821
     * but have only registered a 'Log' service, we'll instead return that.
822
     *
823
     * Will recursively call itself for each depth of dotting.
824
     *
825
     * @param string $name
826
     * @return boolean
827
     */
828
    public function has($name)
829
    {
830
        return (bool)$this->getServiceName($name);
831
    }
832
833
    /**
834
     * Does the given service exist, and if so, what's the stored name for it?
835
     *
836
     * We do a special check here for services that are using compound names. For example,
837
     * we might want to say that a property should be injected with Log.File or Log.Memory,
838
     * but have only registered a 'Log' service, we'll instead return that.
839
     *
840
     * Will recursively call itself for each depth of dotting.
841
     *
842
     * @param string $name
843
     * @return string|null The name of the service (as it might be different from the one passed in)
844
     */
845
    public function getServiceName($name)
846
    {
847
        // Lazy load in spec (disable inheritance to check exact service name)
848
        if ($this->getServiceSpec($name, false)) {
849
            return $name;
850
        }
851
852
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
853
        // invalid name
854
        if (!strpos($name, '.')) {
855
            return null;
856
        }
857
858
        return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
859
    }
860
861
    /**
862
     * Register a service object with an optional name to register it as the
863
     * service for
864
     *
865
     * @param object $service The object to register
866
     * @param string $replace The name of the object to replace (if different to the
867
     * class name of the object to register)
868
     * @return $this
869
     */
870
    public function registerService($service, $replace = null)
871
    {
872
        $registerAt = get_class($service);
873
        if ($replace !== null) {
874
            $registerAt = $replace;
875
        }
876
877
        $this->specs[$registerAt] = array('class' => get_class($service));
878
        $this->serviceCache[$registerAt] = $service;
879
        return $this;
880
    }
881
882
    /**
883
     * Removes a named object from the cached list of objects managed
884
     * by the inject
885
     *
886
     * @param string $name The name to unregister
887
     * @return $this
888
     */
889
    public function unregisterNamedObject($name)
890
    {
891
        unset($this->serviceCache[$name]);
892
        unset($this->specs[$name]);
893
        return $this;
894
    }
895
896
    /**
897
     * Clear out objects of one or more types that are managed by the injetor.
898
     *
899
     * @param array|string $types Base class of object (not service name) to remove
900
     * @return $this
901
     */
902
    public function unregisterObjects($types)
903
    {
904
        if (!is_array($types)) {
905
            $types = [ $types ];
906
        }
907
908
        // Filter all objects
909
        foreach ($this->serviceCache as $key => $object) {
910
            foreach ($types as $filterClass) {
911
                // Prevent destructive flushing
912
                if (strcasecmp($filterClass, 'object') === 0) {
913
                    throw new InvalidArgumentException("Global unregistration is not allowed");
914
                }
915
                if ($object instanceof $filterClass) {
916
                    $this->unregisterNamedObject($key);
917
                    break;
918
                }
919
            }
920
        }
921
        return $this;
922
    }
923
924
    /**
925
     * Get a named managed object
926
     *
927
     * Will first check to see if the item has been registered as a configured service/bean
928
     * and return that if so.
929
     *
930
     * Next, will check to see if there's any registered configuration for the given type
931
     * and will then try and load that
932
     *
933
     * Failing all of that, will just return a new instance of the specified object.
934
     *
935
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
936
     *
937
     * @param string $name The name of the service to retrieve. If not a registered
938
     * service, then a class of the given name is instantiated
939
     * @param bool $asSingleton If set to false a new instance will be returned.
940
     * If true a singleton will be returned unless the spec is type=prototype'
941
     * @param array $constructorArgs Args to pass in to the constructor. Note: Ignored for singletons
942
     * @return mixed Instance of the specified object
943
     */
944
    public function get($name, $asSingleton = true, $constructorArgs = [])
945
    {
946
        $object = $this->getNamedService($name, $asSingleton, $constructorArgs);
947
948
        if (!$object) {
949
            throw new InjectorNotFoundException("The '{$name}' service could not be found");
950
        }
951
952
        return $object;
953
    }
954
955
    /**
956
     * Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
957
     *
958
     * @param string $name The name of the service to retrieve. If not a registered
959
     * service, then a class of the given name is instantiated
960
     * @param bool $asSingleton If set to false a new instance will be returned.
961
     * If true a singleton will be returned unless the spec is type=prototype'
962
     * @param array $constructorArgs Args to pass in to the constructor. Note: Ignored for singletons
963
     * @return mixed Instance of the specified object
964
     */
965
    protected function getNamedService($name, $asSingleton = true, $constructorArgs = [])
966
    {
967
        // Normalise service / args
968
        list($name, $constructorArgs) = $this->normaliseArguments($name, $constructorArgs);
969
970
        // Resolve name with the appropriate spec, or a suitable mock for new services
971
        list($name, $spec) = $this->getServiceNamedSpec($name, $constructorArgs);
972
973
        // Check if we are getting a prototype or singleton
974
        $type = $asSingleton
975
            ? (isset($spec['type']) ? $spec['type'] : self::SINGLETON)
976
            : self::PROTOTYPE;
977
978
        // Return existing instance for singletons
979
        if ($type === self::SINGLETON && isset($this->serviceCache[$name])) {
980
            return $this->serviceCache[$name];
981
        }
982
983
        // Update constructor args
984
        if ($type === self::PROTOTYPE && $constructorArgs) {
985
            // Passed in args are expected to already be normalised (no service references)
986
            $spec['constructor'] = $constructorArgs;
987
        } else {
988
            // Resolve references in constructor args
989
            $this->updateSpecConstructor($spec);
0 ignored issues
show
Bug introduced by
It seems like $spec can also be of type object; however, parameter $spec of SilverStripe\Core\Inject...updateSpecConstructor() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

989
            $this->updateSpecConstructor(/** @scrutinizer ignore-type */ $spec);
Loading history...
990
        }
991
992
        // Build instance
993
        return $this->instantiate($spec, $name, $type);
994
    }
995
996
    /**
997
     * Detect service references with constructor arguments included.
998
     * These will be split out of the service name reference and appended
999
     * to the $args
1000
     *
1001
     * @param string $name
1002
     * @param array $args
1003
     * @return array Two items with name and new args
1004
     */
1005
    protected function normaliseArguments($name, $args = [])
1006
    {
1007
        // Allow service names of the form "%$ServiceName"
1008
        if (substr($name, 0, 2) == '%$') {
1009
            $name = substr($name, 2);
1010
        }
1011
1012
        if (strstr($name, '(')) {
1013
            list($name, $extraArgs) = ClassInfo::parse_class_spec($name);
1014
            if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1015
                $args = array_merge($args, $extraArgs);
1016
            } else {
1017
                $args = $extraArgs;
1018
            }
1019
        }
1020
        $name = trim($name);
1021
        return [$name, $args];
1022
    }
1023
1024
    /**
1025
     * Get or build a named service and specification
1026
     *
1027
     * @param string $name Service name
1028
     * @param array $constructorArgs Optional constructor args
1029
     * @return array
1030
     */
1031
    protected function getServiceNamedSpec($name, $constructorArgs = [])
1032
    {
1033
        $spec = $this->getServiceSpec($name);
1034
        if ($spec) {
1035
            // Resolve to exact service name (in case inherited)
1036
            $name = $this->getServiceName($name);
1037
        } else {
1038
            // Late-generate config spec for non-configured spec
1039
            $spec = [
1040
                'class' => $name,
1041
                'constructor' => $constructorArgs,
1042
            ];
1043
        }
1044
        return [$name, $spec];
1045
    }
1046
1047
    /**
1048
     * Search for spec, lazy-loading in from config locator.
1049
     * Falls back to parent service name if unloaded
1050
     *
1051
     * @param string $name
1052
     * @param bool $inherit Set to true to inherit from parent service if `.` suffixed
1053
     * E.g. 'Psr/Log/LoggerInterface.custom' would fail over to 'Psr/Log/LoggerInterface'
1054
     * @return mixed|object
1055
     */
1056
    public function getServiceSpec($name, $inherit = true)
1057
    {
1058
        if (isset($this->specs[$name])) {
1059
            return $this->specs[$name];
1060
        }
1061
1062
        // Lazy load
1063
        $config = $this->configLocator->locateConfigFor($name);
1064
        if ($config) {
1065
            $this->load([$name => $config]);
1066
            if (isset($this->specs[$name])) {
1067
                return $this->specs[$name];
1068
            }
1069
        }
1070
1071
        // Fail over to parent service if allowed
1072
        if (!$inherit || !strpos($name, '.')) {
1073
            return null;
1074
        }
1075
1076
        return $this->getServiceSpec(substr($name, 0, strrpos($name, '.')));
1077
    }
1078
1079
    /**
1080
     * Magic method to return an item directly
1081
     *
1082
     * @param string $name
1083
     *              The named object to retrieve
1084
     * @return mixed
1085
     */
1086
    public function __get($name)
1087
    {
1088
        return $this->get($name);
1089
    }
1090
1091
    /**
1092
     * Similar to get() but always returns a new object of the given type
1093
     *
1094
     * Additional parameters are passed through as
1095
     *
1096
     * @param string $name
1097
     * @param mixed $argument,... arguments to pass to the constructor
1098
     * @return mixed A new instance of the specified object
1099
     */
1100
    public function create($name, $argument = null)
1101
    {
1102
        $constructorArgs = func_get_args();
1103
        array_shift($constructorArgs);
1104
        return $this->createWithArgs($name, $constructorArgs);
1105
    }
1106
1107
    /**
1108
     * Creates an object with the supplied argument array
1109
     *
1110
     * @param string $name Name of the class to create an object of
1111
     * @param array $constructorArgs Arguments to pass to the constructor
1112
     * @return mixed
1113
     */
1114
    public function createWithArgs($name, $constructorArgs)
1115
    {
1116
        return $this->get($name, false, $constructorArgs);
1117
    }
1118
}
1119