Injector   F
last analyzed

Complexity

Total Complexity 144

Size/Duplication

Total Lines 994
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 284
dl 0
loc 994
rs 2
c 0
b 0
f 0
wmc 144

32 Methods

Rating   Name   Duplication   Size   Complexity  
F load() 0 65 16
A getObjectCreator() 0 3 1
A addAutoProperty() 0 4 1
B convertServiceProperty() 0 29 8
A unnest() 0 13 2
A setConfigLocator() 0 3 1
A __construct() 0 22 4
A setAutoScanProperties() 0 3 1
A nest() 0 6 1
A updateSpecConstructor() 0 4 2
A setInjectMapping() 0 7 2
A inst() 0 3 1
A updateSpec() 0 15 5
A setObjectCreator() 0 3 1
A getConfigLocator() 0 3 1
A unregisterObjects() 0 20 6
A hasService() 0 5 1
A normaliseArguments() 0 17 4
A has() 0 3 1
F instantiate() 0 51 17
A getServiceNamedSpec() 0 14 2
A createWithArgs() 0 3 1
A getServiceSpec() 0 21 6
A registerService() 0 10 2
B getNamedService() 0 29 7
A __get() 0 3 1
A get() 0 9 2
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
use SilverStripe\ORM\DataObject;
17
18
/**
19
 * A simple injection manager that manages creating objects and injecting
20
 * dependencies between them. It borrows quite a lot from ideas taken from
21
 * Spring's configuration, but is adapted to the stateless PHP way of doing
22
 * things.
23
 *
24
 * In its simplest form, the dependency injector can be used as a mechanism to
25
 * instantiate objects. Simply call
26
 *
27
 * Injector::inst()->get('ClassName')
28
 *
29
 * and a new instance of ClassName will be created and returned to you.
30
 *
31
 * Classes can have specific configuration defined for them to
32
 * indicate dependencies that should be injected. This takes the form of
33
 * a static variable $dependencies defined in the class (or configuration),
34
 * which indicates the name of a property that should be set.
35
 *
36
 * eg
37
 *
38
 * <code>
39
 * class MyController extends Controller {
40
 *
41
 *      public $permissions;
42
 *      public $defaultText;
43
 *
44
 *      static $dependencies = array(
45
 *          'defaultText'       => 'Override in configuration',
46
 *          'permissions'       => '%$PermissionService',
47
 *      );
48
 * }
49
 * </code>
50
 *
51
 * will result in an object of type MyController having the defaultText property
52
 * set to 'Override in configuration', and an object identified
53
 * as PermissionService set into the property called 'permissions'. The %$
54
 * syntax tells the injector to look the provided name up as an item to be created
55
 * by the Injector itself.
56
 *
57
 * A key concept of the injector is whether to manage the object as
58
 *
59
 * * A pseudo-singleton, in that only one item will be created for a particular
60
 *   identifier (but the same class could be used for multiple identifiers)
61
 * * A prototype, where the same configuration is used, but a new object is
62
 *   created each time
63
 * * unmanaged, in which case a new object is created and injected, but no
64
 *   information about its state is managed.
65
 *
66
 * Additional configuration of items managed by the injector can be done by
67
 * providing configuration for the types, either by manually loading in an
68
 * array describing the configuration, or by specifying the configuration
69
 * for a type via SilverStripe's configuration mechanism.
70
 *
71
 * Specify a configuration array of the format
72
 *
73
 * <code>
74
 * array(
75
 *      array(
76
 *          'id'            => 'BeanId',                    // the name to be used if diff from the filename
77
 *          'priority'      => 1,                           // priority. If another bean is defined with the same ID,
78
 *                                                          // but has a lower priority, it is NOT overridden
79
 *          'class'         => 'ClassName',                 // the name of the PHP class
80
 *          'src'           => '/path/to/file'              // the location of the class
81
 *          'type'          => 'singleton|prototype'        // if you want prototype object generation, set it as the
82
 *                                                          // type
83
 *                                                          // By default, singleton is assumed
84
 *
85
 *          'factory' => 'FactoryService'                   // A factory service to use to create instances.
86
 *          'construct'     => array(                       // properties to set at construction
87
 *              'scalar',
88
 *              '%$BeanId',
89
 *          )
90
 *          'properties'    => array(
91
 *              'name' => 'value'                           // scalar value
92
 *              'name' => '%$BeanId',                       // a reference to another bean
93
 *              'name' => array(
94
 *                  'scalar',
95
 *                  '%$BeanId'
96
 *              )
97
 *          )
98
 *      )
99
 *      // alternatively
100
 *      'MyBean'        => array(
101
 *          'class'         => 'ClassName',
102
 *      )
103
 *      // or simply
104
 *      'OtherBean'     => 'SomeClass',
105
 * )
106
 * </code>
107
 *
108
 * In addition to specifying the bindings directly in the configuration,
109
 * you can simply create a publicly accessible property on the target
110
 * class which will automatically be injected if the autoScanProperties
111
 * option is set to true. This means a class defined as
112
 *
113
 * <code>
114
 * class MyController extends Controller {
115
 *
116
 *      private $permissionService;
117
 *
118
 *      public setPermissionService($p) {
119
 *          $this->permissionService = $p;
120
 *      }
121
 * }
122
 * </code>
123
 *
124
 * will have setPermissionService called if
125
 *
126
 * * Injector::inst()->setAutoScanProperties(true) is called and
127
 * * A service named 'PermissionService' has been configured
128
 *
129
 * @author [email protected]
130
 * @license BSD License http://silverstripe.org/bsd-license/
131
 */
132
class Injector implements ContainerInterface
133
{
134
135
    /**
136
     * Local store of all services
137
     *
138
     * @var array
139
     */
140
    private $serviceCache;
141
142
    /**
143
     * Cache of items that need to be mapped for each service that gets injected
144
     *
145
     * @var array
146
     */
147
    private $injectMap;
148
149
    /**
150
     * A store of all the service configurations that have been defined.
151
     *
152
     * @var array
153
     */
154
    private $specs;
155
156
    /**
157
     * A map of all the properties that should be automagically set on all
158
     * objects instantiated by the injector
159
     */
160
    private $autoProperties;
161
162
    /**
163
     * A singleton if you want to use it that way
164
     *
165
     * @var Injector
166
     */
167
    private static $instance;
168
169
    /**
170
     * Indicates whether or not to automatically scan properties in injected objects to auto inject
171
     * stuff, similar to the way grails does things.
172
     *
173
     * @var boolean
174
     */
175
    private $autoScanProperties = false;
176
177
    /**
178
     * The default factory used to create new instances.
179
     *
180
     * The {@link InjectionCreator} is used by default, which simply directly
181
     * creates objects. This can be changed to use a different default creation
182
     * method if desired.
183
     *
184
     * Each individual component can also specify a custom factory to use by
185
     * using the `factory` parameter.
186
     *
187
     * @var Factory
188
     */
189
    protected $objectCreator;
190
191
    /**
192
     * Locator for determining Config properties for services
193
     *
194
     * @var ServiceConfigurationLocator
195
     */
196
    protected $configLocator;
197
198
    /**
199
     * Specify a service type singleton
200
     */
201
    const SINGLETON = 'singleton';
202
203
    /**
204
     * Specif ya service type prototype
205
     */
206
    const PROTOTYPE = 'prototype';
207
208
    /**
209
     * Create a new injector.
210
     *
211
     * @param array $config
212
     *              Service configuration
213
     */
214
    public function __construct($config = null)
215
    {
216
        $this->injectMap = array();
217
        $this->serviceCache = array(
218
            'Injector' => $this,
219
        );
220
        $this->specs = [
221
            'Injector' => ['class' => static::class]
222
        ];
223
        $this->autoProperties = array();
224
        $creatorClass = isset($config['creator'])
225
            ? $config['creator']
226
            : InjectionCreator::class;
227
        $locatorClass = isset($config['locator'])
228
            ? $config['locator']
229
            : SilverStripeServiceConfigurationLocator::class;
230
231
        $this->objectCreator = new $creatorClass;
232
        $this->configLocator = new $locatorClass;
233
234
        if ($config) {
235
            $this->load($config);
236
        }
237
    }
238
239
    /**
240
     * The injector instance this one was copied from when Injector::nest() was called.
241
     *
242
     * @var Injector
243
     */
244
    protected $nestedFrom = null;
245
246
    /**
247
     * @return Injector
248
     */
249
    public static function inst()
250
    {
251
        return InjectorLoader::inst()->getManifest();
252
    }
253
254
    /**
255
     * Make the newly active {@link Injector} be a copy of the current active
256
     * {@link Injector} instance.
257
     *
258
     * You can then make changes to the injector with methods such as
259
     * {@link Injector::inst()->registerService()} which will be discarded
260
     * upon a subsequent call to {@link Injector::unnest()}
261
     *
262
     * @return Injector Reference to new active Injector instance
263
     */
264
    public static function nest()
265
    {
266
        // Clone current injector and nest
267
        $new = clone self::inst();
268
        InjectorLoader::inst()->pushManifest($new);
269
        return $new;
270
    }
271
272
    /**
273
     * Change the active Injector back to the Injector instance the current active
274
     * Injector object was copied from.
275
     *
276
     * @return Injector Reference to restored active Injector instance
277
     */
278
    public static function unnest()
279
    {
280
        // Unnest unless we would be left at 0 manifests
281
        $loader = InjectorLoader::inst();
282
        if ($loader->countManifests() <= 1) {
283
            user_error(
284
                "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
285
                E_USER_WARNING
286
            );
287
        } else {
288
            $loader->popManifest();
289
        }
290
        return static::inst();
291
    }
292
293
    /**
294
     * Indicate whether we auto scan injected objects for properties to set.
295
     *
296
     * @param boolean $val
297
     */
298
    public function setAutoScanProperties($val)
299
    {
300
        $this->autoScanProperties = $val;
301
    }
302
303
    /**
304
     * Sets the default factory to use for creating new objects.
305
     *
306
     * @param \SilverStripe\Core\Injector\Factory $obj
307
     */
308
    public function setObjectCreator(Factory $obj)
309
    {
310
        $this->objectCreator = $obj;
311
    }
312
313
    /**
314
     * @return Factory
315
     */
316
    public function getObjectCreator()
317
    {
318
        return $this->objectCreator;
319
    }
320
321
    /**
322
     * Set the configuration locator
323
     * @param ServiceConfigurationLocator $configLocator
324
     */
325
    public function setConfigLocator($configLocator)
326
    {
327
        $this->configLocator = $configLocator;
328
    }
329
330
    /**
331
     * Retrieve the configuration locator
332
     * @return ServiceConfigurationLocator
333
     */
334
    public function getConfigLocator()
335
    {
336
        return $this->configLocator;
337
    }
338
339
    /**
340
     * Add in a specific mapping that should be catered for on a type.
341
     * This allows configuration of what should occur when an object
342
     * of a particular type is injected, and what items should be injected
343
     * for those properties / methods.
344
     *
345
     * @param string $class The class to set a mapping for
346
     * @param string $property The property to set the mapping for
347
     * @param string $toInject The registered type that will be injected
348
     * @param string $injectVia Whether to inject by setting a property or calling a setter
349
     */
350
    public function setInjectMapping($class, $property, $toInject, $injectVia = 'property')
351
    {
352
        $mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
353
354
        $mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
355
356
        $this->injectMap[$class] = $mapping;
357
    }
358
359
    /**
360
     * Add an object that should be automatically set on managed objects
361
     *
362
     * This allows you to specify, for example, that EVERY managed object
363
     * will be automatically inject with a log object by the following
364
     *
365
     * $injector->addAutoProperty('log', new Logger());
366
     *
367
     * @param string $property
368
     *                the name of the property
369
     * @param object $object
370
     *                the object to be set
371
     * @return $this
372
     */
373
    public function addAutoProperty($property, $object)
374
    {
375
        $this->autoProperties[$property] = $object;
376
        return $this;
377
    }
378
379
    /**
380
     * Load services using the passed in configuration for those services
381
     *
382
     * @param array $config
383
     * @return $this
384
     */
385
    public function load($config = array())
386
    {
387
        foreach ($config as $specId => $spec) {
388
            if (is_string($spec)) {
389
                $spec = array('class' => $spec);
390
            }
391
392
            $file = isset($spec['src']) ? $spec['src'] : null;
393
394
            // class is whatever's explicitly set,
395
            $class = isset($spec['class']) ? $spec['class'] : null;
396
397
            // or the specid if nothing else available.
398
            if (!$class && is_string($specId)) {
399
                $class = $specId;
400
            }
401
402
            // make sure the class is set...
403
            if (empty($class)) {
404
                throw new InvalidArgumentException('Missing spec class');
405
            }
406
            $spec['class'] = $class;
407
408
            $id = is_string($specId)
409
                ? $specId
410
                : (isset($spec['id']) ? $spec['id'] : $class);
411
412
            $priority = isset($spec['priority']) ? $spec['priority'] : 1;
413
414
            // see if we already have this defined. If so, check priority weighting
415
            if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
416
                if ($this->specs[$id]['priority'] > $priority) {
417
                    return $this;
418
                }
419
            }
420
421
            // okay, actually include it now we know we're going to use it
422
            if (file_exists($file)) {
423
                require_once $file;
424
            }
425
426
            // make sure to set the id for later when instantiating
427
            // to ensure we get cached
428
            $spec['id'] = $id;
429
430
//          We've removed this check because new functionality means that the 'class' field doesn't need to refer
431
//          specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
432
//          functionality
433
//
434
//          if (!class_exists($class)) {
435
//              throw new Exception("Failed to load '$class' from $file");
436
//          }
437
438
            // store the specs for now - we lazy load on demand later on.
439
            $this->specs[$id] = $spec;
440
441
            // EXCEPT when there's already an existing instance at this id.
442
            // if so, we need to instantiate and replace immediately
443
            if (isset($this->serviceCache[$id])) {
444
                $this->updateSpecConstructor($spec);
445
                $this->instantiate($spec, $id);
446
            }
447
        }
448
449
        return $this;
450
    }
451
452
    /**
453
     * Update the configuration of an already defined service
454
     *
455
     * Use this if you don't want to register a complete new config, just append
456
     * to an existing configuration. Helpful to avoid overwriting someone else's changes
457
     *
458
     * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
459
     *
460
     * @param string $id
461
     *              The name of the service to update the definition for
462
     * @param string $property
463
     *              The name of the property to update.
464
     * @param mixed $value
465
     *              The value to set
466
     * @param boolean $append
467
     *              Whether to append (the default) when the property is an array
468
     */
469
    public function updateSpec($id, $property, $value, $append = true)
470
    {
471
        if (isset($this->specs[$id]['properties'][$property])) {
472
            // by ref so we're updating the actual value
473
            $current = &$this->specs[$id]['properties'][$property];
474
            if (is_array($current) && $append) {
475
                $current[] = $value;
476
            } else {
477
                $this->specs[$id]['properties'][$property] = $value;
478
            }
479
480
            // and reload the object; existing bindings don't get
481
            // updated though! (for now...)
482
            if (isset($this->serviceCache[$id])) {
483
                $this->instantiate(array('class'=>$id), $id);
484
            }
485
        }
486
    }
487
488
    /**
489
     * Update a class specification to convert constructor configuration information if needed
490
     *
491
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
492
     *
493
     * @param array $spec
494
     *          The class specification to update
495
     */
496
    protected function updateSpecConstructor(&$spec)
497
    {
498
        if (isset($spec['constructor'])) {
499
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
500
        }
501
    }
502
503
    /**
504
     * Recursively convert a value into its proper representation with service references
505
     * resolved to actual objects
506
     *
507
     * @param string $value
508
     * @return array|mixed|string
509
     */
510
    public function convertServiceProperty($value)
511
    {
512
        if (is_array($value)) {
0 ignored issues
show
introduced by
The condition is_array($value) is always false.
Loading history...
513
            $newVal = array();
514
            foreach ($value as $k => $v) {
515
                $newVal[$k] = $this->convertServiceProperty($v);
516
            }
517
            return $newVal;
518
        }
519
520
        // Evaluate service references
521
        if (is_string($value) && strpos($value, '%$') === 0) {
522
            $id = substr($value, 2);
523
            return $this->get($id);
524
        }
525
526
        // Evaluate constants surrounded by back ticks
527
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
528
            $envValue = Environment::getEnv($matches['name']);
529
            if ($envValue !== false) {
530
                $value = $envValue;
531
            } elseif (defined($matches['name'])) {
532
                $value = constant($matches['name']);
533
            } else {
534
                $value = null;
535
            }
536
        }
537
538
        return $value;
539
    }
540
541
    /**
542
     * Instantiate a managed object
543
     *
544
     * Given a specification of the form
545
     *
546
     * array(
547
     *        'class' => 'ClassName',
548
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
549
     *        'id' => 'ServiceId',
550
     *        'type' => 'singleton|prototype'
551
     * )
552
     *
553
     * will create a new object, store it in the service registry, and
554
     * set any relevant properties
555
     *
556
     * Optionally, you can pass a class name directly for creation
557
     *
558
     * To access this from the outside, you should call ->get('Name') to ensure
559
     * the appropriate checks are made on the specific type.
560
     *
561
     *
562
     * @param array $spec
563
     *                The specification of the class to instantiate
564
     * @param string $id
565
     *                The name of the object being created. If not supplied, then the id will be inferred from the
566
     *                object being created
567
     * @param string $type
568
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
569
     *                wants the object to be returned
570
     * @return object
571
     */
572
    protected function instantiate($spec, $id = null, $type = null)
573
    {
574
        if (is_string($spec)) {
0 ignored issues
show
introduced by
The condition is_string($spec) is always false.
Loading history...
575
            $spec = array('class' => $spec);
576
        }
577
        $class = $spec['class'];
578
579
        // create the object, using any constructor bindings
580
        $constructorParams = array();
581
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
582
            $constructorParams = $spec['constructor'];
583
        }
584
585
        // If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton
586
        // flag as second argument
587
        if ((!$type || $type !== self::PROTOTYPE)
588
            && empty($constructorParams)
589
            && is_subclass_of($class, DataObject::class)) {
590
            $constructorParams = array(null, true);
591
        }
592
593
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
594
        $object = $factory->create($class, $constructorParams);
595
596
        // Handle empty factory responses
597
        if (!$object) {
0 ignored issues
show
introduced by
$object is of type object, thus it always evaluated to true.
Loading history...
598
            return null;
599
        }
600
601
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
602
        // that we don't manage directly; we don't want to store these in the service cache below
603
        if (!$id) {
604
            $id = isset($spec['id']) ? $spec['id'] : null;
605
        }
606
607
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
608
        // created anew each time
609
        if (!$type) {
610
            $type = isset($spec['type']) ? $spec['type'] : null;
611
        }
612
613
        if ($id && (!$type || $type !== self::PROTOTYPE)) {
614
            // this ABSOLUTELY must be set before the object is injected.
615
            // This prevents circular reference errors down the line
616
            $this->serviceCache[$id] = $object;
617
        }
618
619
        // now inject safely
620
        $this->inject($object, $id);
621
622
        return $object;
623
    }
624
625
    /**
626
     * Inject $object with available objects from the service cache
627
     *
628
     * @todo Track all the existing objects that have had a service bound
629
     * into them, so we can update that binding at a later point if needbe (ie
630
     * if the managed service changes)
631
     *
632
     * @param object $object
633
     *              The object to inject
634
     * @param string $asType
635
     *              The ID this item was loaded as. This is so that the property configuration
636
     *              for a type is referenced correctly in case $object is no longer the same
637
     *              type as the loaded config specification had it as.
638
     */
639
    public function inject($object, $asType = null)
640
    {
641
        $objtype = $asType ? $asType : get_class($object);
642
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
643
644
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
645
646
        // first off, set any properties defined in the service specification for this
647
        // object type
648
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
649
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
650
                $val = $this->convertServiceProperty($value);
651
                $this->setObjectProperty($object, $key, $val);
652
            }
653
        }
654
655
        // Populate named methods
656
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
657
            foreach ($spec['calls'] as $method) {
658
                // Ignore any blank entries from the array; these may be left in due to config system limitations
659
                if (!$method) {
660
                    continue;
661
                }
662
663
                // Format validation
664
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
665
                    throw new InvalidArgumentException(
666
                        "'calls' entries in service definition should be 1 or 2 element arrays."
667
                    );
668
                }
669
                if (!is_string($method[0])) {
670
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
671
                }
672
                if (isset($method[1]) && !is_array($method[1])) {
673
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
674
                }
675
676
                // Check that the method exists and is callable
677
                $objectMethod = array($object, $method[0]);
678
                if (!is_callable($objectMethod)) {
679
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
680
                }
681
682
                // Call it
683
                call_user_func_array(
684
                    $objectMethod,
685
                    $this->convertServiceProperty(
686
                        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

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

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