Completed
Push — 4.11 ( 62bf31...086631 )
by Guy
44s queued 26s
created

Injector::getNamedService()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 29
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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