Passed
Push — 4.3 ( 97180c...0ba275 )
by
unknown
25:37 queued 18:07
created

Injector::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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
     * Specif ya service type prototype
205
     */
206
    const PROTOTYPE = 'prototype';
207
208
    /**
209
     * Create a new injector.
210
     *
211
     * @param array $config
212
     *              Service configuration
213
     */
214
    public function __construct($config = null)
215
    {
216
        $this->injectMap = array();
217
        $this->serviceCache = array(
218
            'Injector' => $this,
219
        );
220
        $this->specs = [
221
            'Injector' => ['class' => static::class]
222
        ];
223
        $this->autoProperties = array();
224
        $creatorClass = isset($config['creator'])
225
            ? $config['creator']
226
            : InjectionCreator::class;
227
        $locatorClass = isset($config['locator'])
228
            ? $config['locator']
229
            : SilverStripeServiceConfigurationLocator::class;
230
231
        $this->objectCreator = new $creatorClass;
232
        $this->configLocator = new $locatorClass;
233
234
        if ($config) {
235
            $this->load($config);
236
        }
237
    }
238
239
    /**
240
     * The injector instance this one was copied from when Injector::nest() was called.
241
     *
242
     * @var Injector
243
     */
244
    protected $nestedFrom = null;
245
246
    /**
247
     * @return Injector
248
     */
249
    public static function inst()
250
    {
251
        return InjectorLoader::inst()->getManifest();
252
    }
253
254
    /**
255
     * Make the newly active {@link Injector} be a copy of the current active
256
     * {@link Injector} instance.
257
     *
258
     * You can then make changes to the injector with methods such as
259
     * {@link Injector::inst()->registerService()} which will be discarded
260
     * upon a subsequent call to {@link Injector::unnest()}
261
     *
262
     * @return Injector Reference to new active Injector instance
263
     */
264
    public static function nest()
265
    {
266
        // Clone current injector and nest
267
        $new = clone self::inst();
268
        InjectorLoader::inst()->pushManifest($new);
269
        return $new;
270
    }
271
272
    /**
273
     * Change the active Injector back to the Injector instance the current active
274
     * Injector object was copied from.
275
     *
276
     * @return Injector Reference to restored active Injector instance
277
     */
278
    public static function unnest()
279
    {
280
        // Unnest unless we would be left at 0 manifests
281
        $loader = InjectorLoader::inst();
282
        if ($loader->countManifests() <= 1) {
283
            user_error(
284
                "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
285
                E_USER_WARNING
286
            );
287
        } else {
288
            $loader->popManifest();
289
        }
290
        return static::inst();
291
    }
292
293
    /**
294
     * Indicate whether we auto scan injected objects for properties to set.
295
     *
296
     * @param boolean $val
297
     */
298
    public function setAutoScanProperties($val)
299
    {
300
        $this->autoScanProperties = $val;
301
    }
302
303
    /**
304
     * Sets the default factory to use for creating new objects.
305
     *
306
     * @param \SilverStripe\Core\Injector\Factory $obj
307
     */
308
    public function setObjectCreator(Factory $obj)
309
    {
310
        $this->objectCreator = $obj;
311
    }
312
313
    /**
314
     * @return Factory
315
     */
316
    public function getObjectCreator()
317
    {
318
        return $this->objectCreator;
319
    }
320
321
    /**
322
     * Set the configuration locator
323
     * @param ServiceConfigurationLocator $configLocator
324
     */
325
    public function setConfigLocator($configLocator)
326
    {
327
        $this->configLocator = $configLocator;
328
    }
329
330
    /**
331
     * Retrieve the configuration locator
332
     * @return ServiceConfigurationLocator
333
     */
334
    public function getConfigLocator()
335
    {
336
        return $this->configLocator;
337
    }
338
339
    /**
340
     * Add in a specific mapping that should be catered for on a type.
341
     * This allows configuration of what should occur when an object
342
     * of a particular type is injected, and what items should be injected
343
     * for those properties / methods.
344
     *
345
     * @param string $class The class to set a mapping for
346
     * @param string $property The property to set the mapping for
347
     * @param string $toInject The registered type that will be injected
348
     * @param string $injectVia Whether to inject by setting a property or calling a setter
349
     */
350
    public function setInjectMapping($class, $property, $toInject, $injectVia = 'property')
351
    {
352
        $mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
353
354
        $mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
355
356
        $this->injectMap[$class] = $mapping;
357
    }
358
359
    /**
360
     * Add an object that should be automatically set on managed objects
361
     *
362
     * This allows you to specify, for example, that EVERY managed object
363
     * will be automatically inject with a log object by the following
364
     *
365
     * $injector->addAutoProperty('log', new Logger());
366
     *
367
     * @param string $property
368
     *                the name of the property
369
     * @param object $object
370
     *                the object to be set
371
     * @return $this
372
     */
373
    public function addAutoProperty($property, $object)
374
    {
375
        $this->autoProperties[$property] = $object;
376
        return $this;
377
    }
378
379
    /**
380
     * Load services using the passed in configuration for those services
381
     *
382
     * @param array $config
383
     * @return $this
384
     */
385
    public function load($config = array())
386
    {
387
        foreach ($config as $specId => $spec) {
388
            if (is_string($spec)) {
389
                $spec = array('class' => $spec);
390
            }
391
392
            $file = isset($spec['src']) ? $spec['src'] : null;
393
394
            // class is whatever's explicitly set,
395
            $class = isset($spec['class']) ? $spec['class'] : null;
396
397
            // or the specid if nothing else available.
398
            if (!$class && is_string($specId)) {
399
                $class = $specId;
400
            }
401
402
            // make sure the class is set...
403
            if (empty($class)) {
404
                throw new InvalidArgumentException('Missing spec class');
405
            }
406
            $spec['class'] = $class;
407
408
            $id = is_string($specId)
409
                ? $specId
410
                : (isset($spec['id']) ? $spec['id'] : $class);
411
412
            $priority = isset($spec['priority']) ? $spec['priority'] : 1;
413
414
            // see if we already have this defined. If so, check priority weighting
415
            if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
416
                if ($this->specs[$id]['priority'] > $priority) {
417
                    return $this;
418
                }
419
            }
420
421
            // okay, actually include it now we know we're going to use it
422
            if (file_exists($file)) {
423
                require_once $file;
424
            }
425
426
            // make sure to set the id for later when instantiating
427
            // to ensure we get cached
428
            $spec['id'] = $id;
429
430
//          We've removed this check because new functionality means that the 'class' field doesn't need to refer
431
//          specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
432
//          functionality
433
//
434
//          if (!class_exists($class)) {
435
//              throw new Exception("Failed to load '$class' from $file");
436
//          }
437
438
            // store the specs for now - we lazy load on demand later on.
439
            $this->specs[$id] = $spec;
440
441
            // EXCEPT when there's already an existing instance at this id.
442
            // if so, we need to instantiate and replace immediately
443
            if (isset($this->serviceCache[$id])) {
444
                $this->updateSpecConstructor($spec);
445
                $this->instantiate($spec, $id);
446
            }
447
        }
448
449
        return $this;
450
    }
451
452
    /**
453
     * Update the configuration of an already defined service
454
     *
455
     * Use this if you don't want to register a complete new config, just append
456
     * to an existing configuration. Helpful to avoid overwriting someone else's changes
457
     *
458
     * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
459
     *
460
     * @param string $id
461
     *              The name of the service to update the definition for
462
     * @param string $property
463
     *              The name of the property to update.
464
     * @param mixed $value
465
     *              The value to set
466
     * @param boolean $append
467
     *              Whether to append (the default) when the property is an array
468
     */
469
    public function updateSpec($id, $property, $value, $append = true)
470
    {
471
        if (isset($this->specs[$id]['properties'][$property])) {
472
            // by ref so we're updating the actual value
473
            $current = &$this->specs[$id]['properties'][$property];
474
            if (is_array($current) && $append) {
475
                $current[] = $value;
476
            } else {
477
                $this->specs[$id]['properties'][$property] = $value;
478
            }
479
480
            // and reload the object; existing bindings don't get
481
            // updated though! (for now...)
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

685
                    /** @scrutinizer ignore-type */ $this->convertServiceProperty(
Loading history...
686
                        isset($method[1]) ? $method[1] : array()
0 ignored issues
show
Bug introduced by
It seems like IssetNode ? $method[1] : array() can also be of type array; however, parameter $value of SilverStripe\Core\Inject...onvertServiceProperty() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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