Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

Injector   F

Complexity

Total Complexity 139

Size/Duplication

Total Lines 981
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 981
rs 1.263
c 0
b 0
f 0
wmc 139

32 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

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

Loading history...
481
            if (isset($this->serviceCache[$id])) {
482
                $this->instantiate(array('class'=>$id), $id);
483
            }
484
        }
485
    }
486
487
    /**
488
     * Update a class specification to convert constructor configuration information if needed
489
     *
490
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
491
     *
492
     * @param array $spec
493
     *          The class specification to update
494
     */
495
    protected function updateSpecConstructor(&$spec)
496
    {
497
        if (isset($spec['constructor'])) {
498
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
499
        }
500
    }
501
502
    /**
503
     * Recursively convert a value into its proper representation with service references
504
     * resolved to actual objects
505
     *
506
     * @param string $value
507
     * @return array|mixed|string
508
     */
509
    public function convertServiceProperty($value)
510
    {
511
        if (is_array($value)) {
512
            $newVal = array();
513
            foreach ($value as $k => $v) {
514
                $newVal[$k] = $this->convertServiceProperty($v);
515
            }
516
            return $newVal;
517
        }
518
519
        // Evaluate service references
520
        if (is_string($value) && strpos($value, '%$') === 0) {
521
            $id = substr($value, 2);
522
            return $this->get($id);
523
        }
524
525
        // Evaluate constants surrounded by back ticks
526
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
527
            $envValue = Environment::getEnv($matches['name']);
528
            if ($envValue !== false) {
529
                $value = $envValue;
530
            } elseif (defined($matches['name'])) {
531
                $value = constant($matches['name']);
532
            } else {
533
                $value = null;
534
            }
535
        }
536
537
        return $value;
538
    }
539
540
    /**
541
     * Instantiate a managed object
542
     *
543
     * Given a specification of the form
544
     *
545
     * array(
546
     *        'class' => 'ClassName',
547
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
548
     *        'id' => 'ServiceId',
549
     *        'type' => 'singleton|prototype'
550
     * )
551
     *
552
     * will create a new object, store it in the service registry, and
553
     * set any relevant properties
554
     *
555
     * Optionally, you can pass a class name directly for creation
556
     *
557
     * To access this from the outside, you should call ->get('Name') to ensure
558
     * the appropriate checks are made on the specific type.
559
     *
560
     *
561
     * @param array $spec
562
     *                The specification of the class to instantiate
563
     * @param string $id
564
     *                The name of the object being created. If not supplied, then the id will be inferred from the
565
     *                object being created
566
     * @param string $type
567
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
568
     *                wants the object to be returned
569
     * @return object
570
     */
571
    protected function instantiate($spec, $id = null, $type = null)
572
    {
573
        if (is_string($spec)) {
574
            $spec = array('class' => $spec);
575
        }
576
        $class = $spec['class'];
577
578
        // create the object, using any constructor bindings
579
        $constructorParams = array();
580
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
581
            $constructorParams = $spec['constructor'];
582
        }
583
584
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
585
        $object = $factory->create($class, $constructorParams);
586
587
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
588
        // that we don't manage directly; we don't want to store these in the service cache below
589
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
590
            $id = isset($spec['id']) ? $spec['id'] : null;
591
        }
592
593
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
594
        // created anew each time
595
        if (!$type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
596
            $type = isset($spec['type']) ? $spec['type'] : null;
597
        }
598
599
        if ($id && (!$type || $type !== self::PROTOTYPE)) {
600
            // this ABSOLUTELY must be set before the object is injected.
601
            // This prevents circular reference errors down the line
602
            $this->serviceCache[$id] = $object;
603
        }
604
605
        // now inject safely
606
        $this->inject($object, $id);
607
608
        return $object;
609
    }
610
611
    /**
612
     * Inject $object with available objects from the service cache
613
     *
614
     * @todo Track all the existing objects that have had a service bound
615
     * into them, so we can update that binding at a later point if needbe (ie
616
     * if the managed service changes)
617
     *
618
     * @param object $object
619
     *              The object to inject
620
     * @param string $asType
621
     *              The ID this item was loaded as. This is so that the property configuration
622
     *              for a type is referenced correctly in case $object is no longer the same
623
     *              type as the loaded config specification had it as.
624
     */
625
    public function inject($object, $asType = null)
626
    {
627
        $objtype = $asType ? $asType : get_class($object);
628
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
629
630
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
631
632
        // first off, set any properties defined in the service specification for this
633
        // object type
634
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
635
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
636
                $val = $this->convertServiceProperty($value);
637
                $this->setObjectProperty($object, $key, $val);
638
            }
639
        }
640
641
        // Populate named methods
642
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
643
            foreach ($spec['calls'] as $method) {
644
                // Ignore any blank entries from the array; these may be left in due to config system limitations
645
                if (!$method) {
646
                    continue;
647
                }
648
649
                // Format validation
650
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
651
                    throw new InvalidArgumentException(
652
                        "'calls' entries in service definition should be 1 or 2 element arrays."
653
                    );
654
                }
655
                if (!is_string($method[0])) {
656
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
657
                }
658
                if (isset($method[1]) && !is_array($method[1])) {
659
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
660
                }
661
662
                // Check that the method exists and is callable
663
                $objectMethod = array($object, $method[0]);
664
                if (!is_callable($objectMethod)) {
665
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
666
                }
667
668
                // Call it
669
                call_user_func_array(
670
                    $objectMethod,
671
                    $this->convertServiceProperty(
672
                        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

672
                        /** @scrutinizer ignore-type */ isset($method[1]) ? $method[1] : array()
Loading history...
673
                    )
674
                );
675
            }
676
        }
677
678
        // now, use any cached information about what properties this object type has
679
        // and set based on name resolution
680
        if ($mapping === null) {
681
            // we use an object to prevent array copies if/when passed around
682
            $mapping = new ArrayObject();
683
684
            if ($this->autoScanProperties) {
685
                // This performs public variable based injection
686
                $robj = new ReflectionObject($object);
687
                $properties = $robj->getProperties();
688
689
                foreach ($properties as $propertyObject) {
690
                    /* @var $propertyObject ReflectionProperty */
691
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
692
                        $origName = $propertyObject->getName();
693
                        $name = ucfirst($origName);
694
                        if ($this->has($name)) {
695
                            // Pull the name out of the registry
696
                            $value = $this->get($name);
697
                            $propertyObject->setValue($object, $value);
698
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
699
                        }
700
                    }
701
                }
702
703
                // and this performs setter based injection
704
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
705
706
                foreach ($methods as $methodObj) {
707
                    /* @var $methodObj ReflectionMethod */
708
                    $methName = $methodObj->getName();
709
                    if (strpos($methName, 'set') === 0) {
710
                        $pname = substr($methName, 3);
711
                        if ($this->has($pname)) {
712
                            // Pull the name out of the registry
713
                            $value = $this->get($pname);
714
                            $methodObj->invoke($object, $value);
715
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
716
                        }
717
                    }
718
                }
719
            }
720
721
            $injections = Config::inst()->get(get_class($object), 'dependencies');
722
            // If the type defines some injections, set them here
723
            if ($injections && count($injections)) {
724
                foreach ($injections as $property => $value) {
725
                    // we're checking empty in case it already has a property at this name
726
                    // this doesn't catch privately set things, but they will only be set by a setter method,
727
                    // which should be responsible for preventing further setting if it doesn't want it.
728
                    if (empty($object->$property)) {
729
                        $convertedValue = $this->convertServiceProperty($value);
730
                        $this->setObjectProperty($object, $property, $convertedValue);
731
                        $mapping[$property] = array('service' => $value, 'type' => 'service');
732
                    }
733
                }
734
            }
735
736
            // we store the information about what needs to be injected for objects of this
737
            // type here
738
            $this->injectMap[$objtype] = $mapping;
739
        } else {
740
            foreach ($mapping as $prop => $propSpec) {
741
                switch ($propSpec['type']) {
742
                    case 'property':
743
                        $value = $this->get($propSpec['name']);
744
                        $object->$prop = $value;
745
                        break;
746
747
748
                    case 'method':
749
                        $method = $prop;
750
                        $value = $this->get($propSpec['name']);
751
                        $object->$method($value);
752
                        break;
753
754
                    case 'service':
755
                        if (empty($object->$prop)) {
756
                            $value = $this->convertServiceProperty($propSpec['service']);
757
                            $this->setObjectProperty($object, $prop, $value);
758
                        }
759
                        break;
760
761
                    default:
762
                        throw new \LogicException("Bad mapping type: " . $propSpec['type']);
763
                }
764
            }
765
        }
766
767
        foreach ($this->autoProperties as $property => $value) {
768
            if (!isset($object->$property)) {
769
                $value = $this->convertServiceProperty($value);
770
                $this->setObjectProperty($object, $property, $value);
771
            }
772
        }
773
774
        // Call the 'injected' method if it exists
775
        if (method_exists($object, 'injected')) {
776
            $object->injected();
777
        }
778
    }
779
780
    /**
781
     * Helper to set a property's value
782
     *
783
     * @param object $object
784
     *                  Set an object's property to a specific value
785
     * @param string $name
786
     *                  The name of the property to set
787
     * @param mixed $value
788
     *                  The value to set
789
     */
790
    protected function setObjectProperty($object, $name, $value)
791
    {
792
        if (ClassInfo::hasMethod($object, 'set'.$name)) {
793
            $object->{'set'.$name}($value);
794
        } else {
795
            $object->$name = $value;
796
        }
797
    }
798
799
    /**
800
     * @deprecated 4.0.0:5.0.0 Use Injector::has() instead
801
     * @param $name
802
     * @return string
803
     */
804
    public function hasService($name)
805
    {
806
        Deprecation::notice('5.0', 'Use Injector::has() instead');
807
808
        return $this->has($name);
809
    }
810
811
    /**
812
     * Does the given service exist?
813
     *
814
     * We do a special check here for services that are using compound names. For example,
815
     * we might want to say that a property should be injected with Log.File or Log.Memory,
816
     * but have only registered a 'Log' service, we'll instead return that.
817
     *
818
     * Will recursively call itself for each depth of dotting.
819
     *
820
     * @param string $name
821
     * @return boolean
822
     */
823
    public function has($name)
824
    {
825
        return (bool)$this->getServiceName($name);
826
    }
827
828
    /**
829
     * Does the given service exist, and if so, what's the stored name for it?
830
     *
831
     * We do a special check here for services that are using compound names. For example,
832
     * we might want to say that a property should be injected with Log.File or Log.Memory,
833
     * but have only registered a 'Log' service, we'll instead return that.
834
     *
835
     * Will recursively call itself for each depth of dotting.
836
     *
837
     * @param string $name
838
     * @return string|null The name of the service (as it might be different from the one passed in)
839
     */
840
    public function getServiceName($name)
841
    {
842
        // Lazy load in spec (disable inheritance to check exact service name)
843
        if ($this->getServiceSpec($name, false)) {
844
            return $name;
845
        }
846
847
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
848
        // invalid name
849
        if (!strpos($name, '.')) {
850
            return null;
851
        }
852
853
        return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
854
    }
855
856
    /**
857
     * Register a service object with an optional name to register it as the
858
     * service for
859
     *
860
     * @param object $service The object to register
861
     * @param string $replace The name of the object to replace (if different to the
862
     * class name of the object to register)
863
     * @return $this
864
     */
865
    public function registerService($service, $replace = null)
866
    {
867
        $registerAt = get_class($service);
868
        if ($replace !== null) {
869
            $registerAt = $replace;
870
        }
871
872
        $this->specs[$registerAt] = array('class' => get_class($service));
873
        $this->serviceCache[$registerAt] = $service;
874
        return $this;
875
    }
876
877
    /**
878
     * Removes a named object from the cached list of objects managed
879
     * by the inject
880
     *
881
     * @param string $name The name to unregister
882
     * @return $this
883
     */
884
    public function unregisterNamedObject($name)
885
    {
886
        unset($this->serviceCache[$name]);
887
        unset($this->specs[$name]);
888
        return $this;
889
    }
890
891
    /**
892
     * Clear out objects of one or more types that are managed by the injetor.
893
     *
894
     * @param array|string $types Base class of object (not service name) to remove
895
     * @return $this
896
     */
897
    public function unregisterObjects($types)
898
    {
899
        if (!is_array($types)) {
900
            $types = [ $types ];
901
        }
902
903
        // Filter all objects
904
        foreach ($this->serviceCache as $key => $object) {
905
            foreach ($types as $filterClass) {
906
                // Prevent destructive flushing
907
                if (strcasecmp($filterClass, 'object') === 0) {
908
                    throw new InvalidArgumentException("Global unregistration is not allowed");
909
                }
910
                if ($object instanceof $filterClass) {
911
                    $this->unregisterNamedObject($key);
912
                    break;
913
                }
914
            }
915
        }
916
        return $this;
917
    }
918
919
    /**
920
     * Get a named managed object
921
     *
922
     * Will first check to see if the item has been registered as a configured service/bean
923
     * and return that if so.
924
     *
925
     * Next, will check to see if there's any registered configuration for the given type
926
     * and will then try and load that
927
     *
928
     * Failing all of that, will just return a new instance of the specified object.
929
     *
930
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
931
     *
932
     * @param string $name The name of the service to retrieve. If not a registered
933
     * service, then a class of the given name is instantiated
934
     * @param bool $asSingleton If set to false a new instance will be returned.
935
     * If true a singleton will be returned unless the spec is type=prototype'
936
     * @param array $constructorArgs Args to pass in to the constructor. Note: Ignored for singletons
937
     * @return mixed Instance of the specified object
938
     */
939
    public function get($name, $asSingleton = true, $constructorArgs = [])
940
    {
941
        $object = $this->getNamedService($name, $asSingleton, $constructorArgs);
942
943
        if (!$object) {
944
            throw new InjectorNotFoundException("The '{$name}' service could not be found");
945
        }
946
947
        return $object;
948
    }
949
950
    /**
951
     * Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
952
     *
953
     * @param string $name The name of the service to retrieve. If not a registered
954
     * service, then a class of the given name is instantiated
955
     * @param bool $asSingleton If set to false a new instance will be returned.
956
     * If true a singleton will be returned unless the spec is type=prototype'
957
     * @param array $constructorArgs Args to pass in to the constructor. Note: Ignored for singletons
958
     * @return mixed Instance of the specified object
959
     */
960
    protected function getNamedService($name, $asSingleton = true, $constructorArgs = [])
961
    {
962
        // Normalise service / args
963
        list($name, $constructorArgs) = $this->normaliseArguments($name, $constructorArgs);
964
965
        // Resolve name with the appropriate spec, or a suitable mock for new services
966
        list($name, $spec) = $this->getServiceNamedSpec($name, $constructorArgs);
967
968
        // Check if we are getting a prototype or singleton
969
        $type = $asSingleton
970
            ? (isset($spec['type']) ? $spec['type'] : self::SINGLETON)
971
            : self::PROTOTYPE;
972
973
        // Return existing instance for singletons
974
        if ($type === self::SINGLETON && isset($this->serviceCache[$name])) {
975
            return $this->serviceCache[$name];
976
        }
977
978
        // Update constructor args
979
        if ($type === self::PROTOTYPE && $constructorArgs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constructorArgs 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...
980
            // Passed in args are expected to already be normalised (no service references)
981
            $spec['constructor'] = $constructorArgs;
982
        } else {
983
            // Resolve references in constructor args
984
            $this->updateSpecConstructor($spec);
985
        }
986
987
        // Build instance
988
        return $this->instantiate($spec, $name, $type);
989
    }
990
991
    /**
992
     * Detect service references with constructor arguments included.
993
     * These will be split out of the service name reference and appended
994
     * to the $args
995
     *
996
     * @param string $name
997
     * @param array $args
998
     * @return array Two items with name and new args
999
     */
1000
    protected function normaliseArguments($name, $args = [])
1001
    {
1002
        // Allow service names of the form "%$ServiceName"
1003
        if (substr($name, 0, 2) == '%$') {
1004
            $name = substr($name, 2);
1005
        }
1006
1007
        if (strstr($name, '(')) {
1008
            list($name, $extraArgs) = ClassInfo::parse_class_spec($name);
1009
            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...
1010
                $args = array_merge($args, $extraArgs);
1011
            } else {
1012
                $args = $extraArgs;
1013
            }
1014
        }
1015
        $name = trim($name);
1016
        return [$name, $args];
1017
    }
1018
1019
    /**
1020
     * Get or build a named service and specification
1021
     *
1022
     * @param string $name Service name
1023
     * @param array $constructorArgs Optional constructor args
1024
     * @return array
1025
     */
1026
    protected function getServiceNamedSpec($name, $constructorArgs = [])
1027
    {
1028
        $spec = $this->getServiceSpec($name);
1029
        if ($spec) {
1030
            // Resolve to exact service name (in case inherited)
1031
            $name = $this->getServiceName($name);
1032
        } else {
1033
            // Late-generate config spec for non-configured spec
1034
            $spec = [
1035
                'class' => $name,
1036
                'constructor' => $constructorArgs,
1037
            ];
1038
        }
1039
        return [$name, $spec];
1040
    }
1041
1042
    /**
1043
     * Search for spec, lazy-loading in from config locator.
1044
     * Falls back to parent service name if unloaded
1045
     *
1046
     * @param string $name
1047
     * @param bool $inherit Set to true to inherit from parent service if `.` suffixed
1048
     * E.g. 'Psr/Log/LoggerInterface.custom' would fail over to 'Psr/Log/LoggerInterface'
1049
     * @return mixed|object
1050
     */
1051
    public function getServiceSpec($name, $inherit = true)
1052
    {
1053
        if (isset($this->specs[$name])) {
1054
            return $this->specs[$name];
1055
        }
1056
1057
        // Lazy load
1058
        $config = $this->configLocator->locateConfigFor($name);
1059
        if ($config) {
1060
            $this->load([$name => $config]);
1061
            if (isset($this->specs[$name])) {
1062
                return $this->specs[$name];
1063
            }
1064
        }
1065
1066
        // Fail over to parent service if allowed
1067
        if (!$inherit || !strpos($name, '.')) {
1068
            return null;
1069
        }
1070
1071
        return $this->getServiceSpec(substr($name, 0, strrpos($name, '.')));
1072
    }
1073
1074
    /**
1075
     * Magic method to return an item directly
1076
     *
1077
     * @param string $name
1078
     *              The named object to retrieve
1079
     * @return mixed
1080
     */
1081
    public function __get($name)
1082
    {
1083
        return $this->get($name);
1084
    }
1085
1086
    /**
1087
     * Similar to get() but always returns a new object of the given type
1088
     *
1089
     * Additional parameters are passed through as
1090
     *
1091
     * @param string $name
1092
     * @param mixed $argument,... arguments to pass to the constructor
1093
     * @return mixed A new instance of the specified object
1094
     */
1095
    public function create($name, $argument = null)
1096
    {
1097
        $constructorArgs = func_get_args();
1098
        array_shift($constructorArgs);
1099
        return $this->createWithArgs($name, $constructorArgs);
1100
    }
1101
1102
    /**
1103
     * Creates an object with the supplied argument array
1104
     *
1105
     * @param string $name Name of the class to create an object of
1106
     * @param array $constructorArgs Arguments to pass to the constructor
1107
     * @return mixed
1108
     */
1109
    public function createWithArgs($name, $constructorArgs)
1110
    {
1111
        return $this->get($name, false, $constructorArgs);
1112
    }
1113
}
1114