Passed
Pull Request — 4.11 (#10312)
by Steve
07:34
created

Injector::convertServiceProperty()   C

Complexity

Conditions 12
Paths 17

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 26
c 0
b 0
f 0
nc 17
nop 1
dl 0
loc 41
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * Specify a 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 = [];
217
        $this->serviceCache = [
218
            'Injector' => $this,
219
        ];
220
        $this->specs = [
221
            'Injector' => ['class' => static::class]
222
        ];
223
        $this->autoProperties = [];
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] : [];
353
354
        $mapping[$property] = ['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 = [])
386
    {
387
        foreach ($config as $specId => $spec) {
388
            if (is_string($spec)) {
389
                $spec = ['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(['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 = [];
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
        $hasBacticks = false;
528
        $allMissing = true;
529
        $rx = '/`(?<name>[^`]+)`/';
530
        while (preg_match($rx, $value ?? '', $matches)) {
531
            $hasBacticks = true;
532
            $name = $matches['name'];
533
            $envValue = Environment::getEnv($name);
534
            $val = '';
535
            if ($envValue !== false) {
536
                $val = $envValue;
537
            } elseif (defined($name)) {
538
                $val = constant($name);
539
            }
540
            $value = str_replace("`$name`", $val, $value);
541
            if ($val) {
542
                $allMissing = false;
543
            }
544
        }
545
        // silverstripe sometimes explictly expects a null value rather than just an empty string
546
        if ($hasBacticks && $allMissing && $value === '') {
547
            return null;
548
        }
549
550
        return $value;
551
    }
552
553
    /**
554
     * Instantiate a managed object
555
     *
556
     * Given a specification of the form
557
     *
558
     * array(
559
     *        'class' => 'ClassName',
560
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
561
     *        'id' => 'ServiceId',
562
     *        'type' => 'singleton|prototype'
563
     * )
564
     *
565
     * will create a new object, store it in the service registry, and
566
     * set any relevant properties
567
     *
568
     * Optionally, you can pass a class name directly for creation
569
     *
570
     * To access this from the outside, you should call ->get('Name') to ensure
571
     * the appropriate checks are made on the specific type.
572
     *
573
     *
574
     * @param array $spec
575
     *                The specification of the class to instantiate
576
     * @param string $id
577
     *                The name of the object being created. If not supplied, then the id will be inferred from the
578
     *                object being created
579
     * @param string $type
580
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
581
     *                wants the object to be returned
582
     * @return object
583
     */
584
    protected function instantiate($spec, $id = null, $type = null)
585
    {
586
        if (is_string($spec)) {
0 ignored issues
show
introduced by
The condition is_string($spec) is always false.
Loading history...
587
            $spec = ['class' => $spec];
588
        }
589
        $class = $spec['class'];
590
591
        // create the object, using any constructor bindings
592
        $constructorParams = [];
593
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
594
            $constructorParams = $spec['constructor'];
595
        }
596
597
        // If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton
598
        // flag as second argument
599
        if ((!$type || $type !== self::PROTOTYPE)
600
            && empty($constructorParams)
601
            && is_subclass_of($class, DataObject::class)) {
602
            $constructorParams = [null, DataObject::CREATE_SINGLETON];
603
        }
604
605
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
606
        $object = $factory->create($class, $constructorParams);
607
608
        // Handle empty factory responses
609
        if (!$object) {
0 ignored issues
show
introduced by
$object is of type object, thus it always evaluated to true.
Loading history...
610
            return null;
611
        }
612
613
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
614
        // that we don't manage directly; we don't want to store these in the service cache below
615
        if (!$id) {
616
            $id = isset($spec['id']) ? $spec['id'] : null;
617
        }
618
619
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
620
        // created anew each time
621
        if (!$type) {
622
            $type = isset($spec['type']) ? $spec['type'] : null;
623
        }
624
625
        if ($id && (!$type || $type !== self::PROTOTYPE)) {
626
            // this ABSOLUTELY must be set before the object is injected.
627
            // This prevents circular reference errors down the line
628
            $this->serviceCache[$id] = $object;
629
        }
630
631
        // now inject safely
632
        $this->inject($object, $id);
633
634
        return $object;
635
    }
636
637
    /**
638
     * Inject $object with available objects from the service cache
639
     *
640
     * @todo Track all the existing objects that have had a service bound
641
     * into them, so we can update that binding at a later point if needbe (ie
642
     * if the managed service changes)
643
     *
644
     * @param object $object
645
     *              The object to inject
646
     * @param string $asType
647
     *              The ID this item was loaded as. This is so that the property configuration
648
     *              for a type is referenced correctly in case $object is no longer the same
649
     *              type as the loaded config specification had it as.
650
     */
651
    public function inject($object, $asType = null)
652
    {
653
        $objtype = $asType ? $asType : get_class($object);
654
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
655
656
        $spec = empty($this->specs[$objtype]) ? [] : $this->specs[$objtype];
657
658
        // first off, set any properties defined in the service specification for this
659
        // object type
660
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
661
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
662
                $val = $this->convertServiceProperty($value);
663
                $this->setObjectProperty($object, $key, $val);
664
            }
665
        }
666
667
        // Populate named methods
668
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
669
            foreach ($spec['calls'] as $method) {
670
                // Ignore any blank entries from the array; these may be left in due to config system limitations
671
                if (!$method) {
672
                    continue;
673
                }
674
675
                // Format validation
676
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
677
                    throw new InvalidArgumentException(
678
                        "'calls' entries in service definition should be 1 or 2 element arrays."
679
                    );
680
                }
681
                if (!is_string($method[0])) {
682
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
683
                }
684
                if (isset($method[1]) && !is_array($method[1])) {
685
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
686
                }
687
688
                // Check that the method exists and is callable
689
                $objectMethod = [$object, $method[0]];
690
                if (!is_callable($objectMethod)) {
691
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
692
                }
693
694
                // Call it
695
                call_user_func_array(
696
                    $objectMethod,
697
                    $this->convertServiceProperty(
698
                        isset($method[1]) ? $method[1] : []
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

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

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