Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

Injector::setInjectMapping()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 4
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Injector;
4
5
use Psr\Container\NotFoundExceptionInterface;
6
use SilverStripe\Core\Config\Config;
7
use ReflectionProperty;
8
use ArrayObject;
9
use ReflectionObject;
10
use ReflectionMethod;
11
use Psr\Container\ContainerInterface;
12
use SilverStripe\Dev\Deprecation;
13
14
/**
15
 * A simple injection manager that manages creating objects and injecting
16
 * dependencies between them. It borrows quite a lot from ideas taken from
17
 * Spring's configuration, but is adapted to the stateless PHP way of doing
18
 * things.
19
 *
20
 * In its simplest form, the dependency injector can be used as a mechanism to
21
 * instantiate objects. Simply call
22
 *
23
 * Injector::inst()->get('ClassName')
24
 *
25
 * and a new instance of ClassName will be created and returned to you.
26
 *
27
 * Classes can have specific configuration defined for them to
28
 * indicate dependencies that should be injected. This takes the form of
29
 * a static variable $dependencies defined in the class (or configuration),
30
 * which indicates the name of a property that should be set.
31
 *
32
 * eg
33
 *
34
 * <code>
35
 * class MyController extends Controller {
36
 *
37
 *      public $permissions;
38
 *      public $defaultText;
39
 *
40
 *      static $dependencies = array(
41
 *          'defaultText'       => 'Override in configuration',
42
 *          'permissions'       => '%$PermissionService',
43
 *      );
44
 * }
45
 * </code>
46
 *
47
 * will result in an object of type MyController having the defaultText property
48
 * set to 'Override in configuration', and an object identified
49
 * as PermissionService set into the property called 'permissions'. The %$
50
 * syntax tells the injector to look the provided name up as an item to be created
51
 * by the Injector itself.
52
 *
53
 * A key concept of the injector is whether to manage the object as
54
 *
55
 * * A pseudo-singleton, in that only one item will be created for a particular
56
 *   identifier (but the same class could be used for multiple identifiers)
57
 * * A prototype, where the same configuration is used, but a new object is
58
 *   created each time
59
 * * unmanaged, in which case a new object is created and injected, but no
60
 *   information about its state is managed.
61
 *
62
 * Additional configuration of items managed by the injector can be done by
63
 * providing configuration for the types, either by manually loading in an
64
 * array describing the configuration, or by specifying the configuration
65
 * for a type via SilverStripe's configuration mechanism.
66
 *
67
 * Specify a configuration array of the format
68
 *
69
 * <code>
70
 * array(
71
 *      array(
72
 *          'id'            => 'BeanId',                    // the name to be used if diff from the filename
73
 *          'priority'      => 1,                           // priority. If another bean is defined with the same ID,
74
 *                                                          // but has a lower priority, it is NOT overridden
75
 *          'class'         => 'ClassName',                 // the name of the PHP class
76
 *          'src'           => '/path/to/file'              // the location of the class
77
 *          'type'          => 'singleton|prototype'        // if you want prototype object generation, set it as the
78
 *                                                          // type
79
 *                                                          // By default, singleton is assumed
80
 *
81
 *          'factory' => 'FactoryService'                   // A factory service to use to create instances.
82
 *          'construct'     => array(                       // properties to set at construction
83
 *              'scalar',
84
 *              '%$BeanId',
85
 *          )
86
 *          'properties'    => array(
87
 *              'name' => 'value'                           // scalar value
88
 *              'name' => '%$BeanId',                       // a reference to another bean
89
 *              'name' => array(
90
 *                  'scalar',
91
 *                  '%$BeanId'
92
 *              )
93
 *          )
94
 *      )
95
 *      // alternatively
96
 *      'MyBean'        => array(
97
 *          'class'         => 'ClassName',
98
 *      )
99
 *      // or simply
100
 *      'OtherBean'     => 'SomeClass',
101
 * )
102
 * </code>
103
 *
104
 * In addition to specifying the bindings directly in the configuration,
105
 * you can simply create a publicly accessible property on the target
106
 * class which will automatically be injected if the autoScanProperties
107
 * option is set to true. This means a class defined as
108
 *
109
 * <code>
110
 * class MyController extends Controller {
111
 *
112
 *      private $permissionService;
113
 *
114
 *      public setPermissionService($p) {
115
 *          $this->permissionService = $p;
116
 *      }
117
 * }
118
 * </code>
119
 *
120
 * will have setPermissionService called if
121
 *
122
 * * Injector::inst()->setAutoScanProperties(true) is called and
123
 * * A service named 'PermissionService' has been configured
124
 *
125
 * @author [email protected]
126
 * @license BSD License http://silverstripe.org/bsd-license/
127
 */
128
class Injector implements ContainerInterface
129
{
130
131
    /**
132
     * Local store of all services
133
     *
134
     * @var array
135
     */
136
    private $serviceCache;
137
138
    /**
139
     * Cache of items that need to be mapped for each service that gets injected
140
     *
141
     * @var array
142
     */
143
    private $injectMap;
144
145
    /**
146
     * A store of all the service configurations that have been defined.
147
     *
148
     * @var array
149
     */
150
    private $specs;
151
152
    /**
153
     * A map of all the properties that should be automagically set on all
154
     * objects instantiated by the injector
155
     */
156
    private $autoProperties;
157
158
    /**
159
     * A singleton if you want to use it that way
160
     *
161
     * @var Injector
162
     */
163
    private static $instance;
164
165
    /**
166
     * Indicates whether or not to automatically scan properties in injected objects to auto inject
167
     * stuff, similar to the way grails does things.
168
     *
169
     * @var boolean
170
     */
171
    private $autoScanProperties = false;
172
173
    /**
174
     * The default factory used to create new instances.
175
     *
176
     * The {@link InjectionCreator} is used by default, which simply directly
177
     * creates objects. This can be changed to use a different default creation
178
     * method if desired.
179
     *
180
     * Each individual component can also specify a custom factory to use by
181
     * using the `factory` parameter.
182
     *
183
     * @var Factory
184
     */
185
    protected $objectCreator;
186
187
    /**
188
     * Locator for determining Config properties for services
189
     *
190
     * @var ServiceConfigurationLocator
191
     */
192
    protected $configLocator;
193
194
    /**
195
     * Create a new injector.
196
     *
197
     * @param array $config
198
     *              Service configuration
199
     */
200
    public function __construct($config = null)
201
    {
202
        $this->injectMap = array();
203
        $this->serviceCache = array(
204
            'Injector'      => $this,
205
        );
206
        $this->specs = array(
207
            'Injector'      => array('class' => 'SilverStripe\\Core\\Injector\\Injector')
208
        );
209
210
        $this->autoProperties = array();
211
212
213
        $creatorClass = isset($config['creator'])
214
            ? $config['creator']
215
            : 'SilverStripe\\Core\\Injector\\InjectionCreator';
216
        $locatorClass = isset($config['locator'])
217
            ? $config['locator']
218
            : 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator';
219
220
        $this->objectCreator = new $creatorClass;
221
        $this->configLocator = new $locatorClass;
222
223
        if ($config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $config 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...
224
            $this->load($config);
225
        }
226
    }
227
228
    /**
229
     * The injector instance this one was copied from when Injector::nest() was called.
230
     *
231
     * @var Injector
232
     */
233
    protected $nestedFrom = null;
234
235
    /**
236
     * If a user wants to use the injector as a static reference
237
     *
238
     * @param array $config
239
     * @return ContainerInterface
240
     */
241
    public static function inst($config = null)
242
    {
243
        if (!self::$instance) {
244
            self::$instance = new Injector($config);
245
        }
246
        return self::$instance;
247
    }
248
249
    /**
250
     * Sets the default global injector instance.
251
     *
252
     * @param ContainerInterface $instance
253
     * @return Injector Reference to new active Injector instance
254
     */
255
    public static function set_inst(ContainerInterface $instance)
256
    {
257
        return self::$instance = $instance;
0 ignored issues
show
Documentation Bug introduced by
$instance is of type object<Psr\Container\ContainerInterface>, but the property $instance was declared to be of type object<SilverStripe\Core\Injector\Injector>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
258
    }
259
260
    /**
261
     * Make the newly active {@link Injector} be a copy of the current active
262
     * {@link Injector} instance.
263
     *
264
     * You can then make changes to the injector with methods such as
265
     * {@link Injector::inst()->registerService()} which will be discarded
266
     * upon a subsequent call to {@link Injector::unnest()}
267
     *
268
     * @return Injector Reference to new active Injector instance
269
     */
270
    public static function nest()
271
    {
272
        $current = self::$instance;
273
274
        $new = clone $current;
275
        $new->nestedFrom = $current;
276
        return self::set_inst($new);
277
    }
278
279
    /**
280
     * Change the active Injector back to the Injector instance the current active
281
     * Injector object was copied from.
282
     *
283
     * @return Injector Reference to restored active Injector instance
284
     */
285
    public static function unnest()
286
    {
287
        if (self::inst()->nestedFrom) {
0 ignored issues
show
Bug introduced by
Accessing nestedFrom on the interface Psr\Container\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
288
            self::set_inst(self::inst()->nestedFrom);
0 ignored issues
show
Bug introduced by
Accessing nestedFrom on the interface Psr\Container\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
289
        } else {
290
            user_error(
291
                "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
292
                E_USER_WARNING
293
            );
294
        }
295
        return self::inst();
296
    }
297
298
    /**
299
     * Indicate whether we auto scan injected objects for properties to set.
300
     *
301
     * @param boolean $val
302
     */
303
    public function setAutoScanProperties($val)
304
    {
305
        $this->autoScanProperties = $val;
306
    }
307
308
    /**
309
     * Sets the default factory to use for creating new objects.
310
     *
311
     * @param \SilverStripe\Core\Injector\Factory $obj
312
     */
313
    public function setObjectCreator(Factory $obj)
314
    {
315
        $this->objectCreator = $obj;
316
    }
317
318
    /**
319
     * @return Factory
320
     */
321
    public function getObjectCreator()
322
    {
323
        return $this->objectCreator;
324
    }
325
326
    /**
327
     * Set the configuration locator
328
     * @param ServiceConfigurationLocator $configLocator
329
     */
330
    public function setConfigLocator($configLocator)
331
    {
332
        $this->configLocator = $configLocator;
333
    }
334
335
    /**
336
     * Retrieve the configuration locator
337
     * @return ServiceConfigurationLocator
338
     */
339
    public function getConfigLocator()
340
    {
341
        return $this->configLocator;
342
    }
343
344
    /**
345
     * Add in a specific mapping that should be catered for on a type.
346
     * This allows configuration of what should occur when an object
347
     * of a particular type is injected, and what items should be injected
348
     * for those properties / methods.
349
     *
350
     * @param string $class The class to set a mapping for
351
     * @param string $property The property to set the mapping for
352
     * @param string $toInject The registered type that will be injected
353
     * @param string $injectVia Whether to inject by setting a property or calling a setter
354
     */
355
    public function setInjectMapping($class, $property, $toInject, $injectVia = 'property')
356
    {
357
        $mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
358
359
        $mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
360
361
        $this->injectMap[$class] = $mapping;
362
    }
363
364
    /**
365
     * Add an object that should be automatically set on managed objects
366
     *
367
     * This allows you to specify, for example, that EVERY managed object
368
     * will be automatically inject with a log object by the following
369
     *
370
     * $injector->addAutoProperty('log', new Logger());
371
     *
372
     * @param string $property
373
     *                the name of the property
374
     * @param object $object
375
     *                the object to be set
376
     * @return $this
377
     */
378
    public function addAutoProperty($property, $object)
379
    {
380
        $this->autoProperties[$property] = $object;
381
        return $this;
382
    }
383
384
    /**
385
     * Load services using the passed in configuration for those services
386
     *
387
     * @param array $config
388
     * @return $this
389
     */
390
    public function load($config = array())
391
    {
392
        foreach ($config as $specId => $spec) {
393
            if (is_string($spec)) {
394
                $spec = array('class' => $spec);
395
            }
396
397
            $file = isset($spec['src']) ? $spec['src'] : null;
398
399
            // class is whatever's explicitly set,
400
            $class = isset($spec['class']) ? $spec['class'] : null;
401
402
            // or the specid if nothing else available.
403
            if (!$class && is_string($specId)) {
404
                $class = $specId;
405
            }
406
407
            // make sure the class is set...
408
            if (empty($class)) {
409
                throw new \InvalidArgumentException('Missing spec class');
410
            }
411
            $spec['class'] = $class;
412
413
            $id = is_string($specId)
414
                ? $specId
415
                : (isset($spec['id']) ? $spec['id'] : $class);
416
417
            $priority = isset($spec['priority']) ? $spec['priority'] : 1;
418
419
            // see if we already have this defined. If so, check priority weighting
420
            if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
421
                if ($this->specs[$id]['priority'] > $priority) {
422
                    return $this;
423
                }
424
            }
425
426
            // okay, actually include it now we know we're going to use it
427
            if (file_exists($file)) {
428
                require_once $file;
429
            }
430
431
            // make sure to set the id for later when instantiating
432
            // to ensure we get cached
433
            $spec['id'] = $id;
434
435
//			We've removed this check because new functionality means that the 'class' field doesn't need to refer
436
//			specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
437
//			functionality
438
//
439
//			if (!class_exists($class)) {
440
//				throw new Exception("Failed to load '$class' from $file");
441
//			}
442
443
            // store the specs for now - we lazy load on demand later on.
444
            $this->specs[$id] = $spec;
445
446
            // EXCEPT when there's already an existing instance at this id.
447
            // if so, we need to instantiate and replace immediately
448
            if (isset($this->serviceCache[$id])) {
449
                $this->updateSpecConstructor($spec);
450
                $this->instantiate($spec, $id);
451
            }
452
        }
453
454
        return $this;
455
    }
456
457
    /**
458
     * Update the configuration of an already defined service
459
     *
460
     * Use this if you don't want to register a complete new config, just append
461
     * to an existing configuration. Helpful to avoid overwriting someone else's changes
462
     *
463
     * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
464
     *
465
     * @param string $id
466
     *              The name of the service to update the definition for
467
     * @param string $property
468
     *              The name of the property to update.
469
     * @param mixed $value
470
     *              The value to set
471
     * @param boolean $append
472
     *              Whether to append (the default) when the property is an array
473
     */
474
    public function updateSpec($id, $property, $value, $append = true)
475
    {
476
        if (isset($this->specs[$id]['properties'][$property])) {
477
            // by ref so we're updating the actual value
478
            $current = &$this->specs[$id]['properties'][$property];
479
            if (is_array($current) && $append) {
480
                $current[] = $value;
481
            } else {
482
                $this->specs[$id]['properties'][$property] = $value;
483
            }
484
485
            // and reload the object; existing bindings don't get
486
            // updated though! (for now...)
487
            if (isset($this->serviceCache[$id])) {
488
                $this->instantiate(array('class'=>$id), $id);
489
            }
490
        }
491
    }
492
493
    /**
494
     * Update a class specification to convert constructor configuration information if needed
495
     *
496
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
497
     *
498
     * @param array $spec
499
     *          The class specification to update
500
     */
501
    protected function updateSpecConstructor(&$spec)
502
    {
503
        if (isset($spec['constructor'])) {
504
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
505
        }
506
    }
507
508
    /**
509
     * Recursively convert a value into its proper representation with service references
510
     * resolved to actual objects
511
     *
512
     * @param string $value
513
     * @return array|mixed|string
514
     */
515
    public function convertServiceProperty($value)
516
    {
517
        if (is_array($value)) {
518
            $newVal = array();
519
            foreach ($value as $k => $v) {
520
                $newVal[$k] = $this->convertServiceProperty($v);
521
            }
522
            return $newVal;
523
        }
524
525
        // Evaluate service references
526
        if (is_string($value) && strpos($value, '%$') === 0) {
527
            $id = substr($value, 2);
528
            return $this->get($id);
529
        }
530
531
        // Evaluate constants surrounded by back ticks
532
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
533
            if (getenv($matches['name']) !== false) {
534
                $value = getenv($matches['name']);
535
            } elseif (defined($matches['name'])) {
536
                $value = constant($matches['name']);
537
            } else {
538
                $value = null;
539
            }
540
        }
541
542
        return $value;
543
    }
544
545
    /**
546
     * Instantiate a managed object
547
     *
548
     * Given a specification of the form
549
     *
550
     * array(
551
     *        'class' => 'ClassName',
552
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
553
     *        'id' => 'ServiceId',
554
     *        'type' => 'singleton|prototype'
555
     * )
556
     *
557
     * will create a new object, store it in the service registry, and
558
     * set any relevant properties
559
     *
560
     * Optionally, you can pass a class name directly for creation
561
     *
562
     * To access this from the outside, you should call ->get('Name') to ensure
563
     * the appropriate checks are made on the specific type.
564
     *
565
     *
566
     * @param array $spec
567
     *                The specification of the class to instantiate
568
     * @param string $id
569
     *                The name of the object being created. If not supplied, then the id will be inferred from the
570
     *                object being created
571
     * @param string $type
572
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
573
     *                wants the object to be returned
574
     * @return object
575
     */
576
    protected function instantiate($spec, $id = null, $type = null)
577
    {
578
        if (is_string($spec)) {
579
            $spec = array('class' => $spec);
580
        }
581
        $class = $spec['class'];
582
583
        // create the object, using any constructor bindings
584
        $constructorParams = array();
585
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
586
            $constructorParams = $spec['constructor'];
587
        }
588
589
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
590
        $object = $factory->create($class, $constructorParams);
591
592
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
593
        // that we don't manage directly; we don't want to store these in the service cache below
594
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null 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...
595
            $id = isset($spec['id']) ? $spec['id'] : null;
596
        }
597
598
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
599
        // created anew each time
600
        if (!$type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null 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...
601
            $type = isset($spec['type']) ? $spec['type'] : null;
602
        }
603
604
        if ($id && (!$type || $type != 'prototype')) {
605
            // this ABSOLUTELY must be set before the object is injected.
606
            // This prevents circular reference errors down the line
607
            $this->serviceCache[$id] = $object;
608
        }
609
610
        // now inject safely
611
        $this->inject($object, $id);
612
613
        return $object;
614
    }
615
616
    /**
617
     * Inject $object with available objects from the service cache
618
     *
619
     * @todo Track all the existing objects that have had a service bound
620
     * into them, so we can update that binding at a later point if needbe (ie
621
     * if the managed service changes)
622
     *
623
     * @param object $object
624
     *              The object to inject
625
     * @param string $asType
626
     *              The ID this item was loaded as. This is so that the property configuration
627
     *              for a type is referenced correctly in case $object is no longer the same
628
     *              type as the loaded config specification had it as.
629
     */
630
    public function inject($object, $asType = null)
631
    {
632
        $objtype = $asType ? $asType : get_class($object);
633
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
634
635
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
636
637
        // first off, set any properties defined in the service specification for this
638
        // object type
639
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
640
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
641
                $val = $this->convertServiceProperty($value);
642
                $this->setObjectProperty($object, $key, $val);
643
            }
644
        }
645
646
        // Populate named methods
647
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
648
            foreach ($spec['calls'] as $method) {
649
                // Ignore any blank entries from the array; these may be left in due to config system limitations
650
                if (!$method) {
651
                    continue;
652
                }
653
654
                // Format validation
655 View Code Duplication
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
656
                    throw new \InvalidArgumentException(
657
                        "'calls' entries in service definition should be 1 or 2 element arrays."
658
                    );
659
                }
660
                if (!is_string($method[0])) {
661
                    throw new \InvalidArgumentException("1st element of a 'calls' entry should be a string");
662
                }
663 View Code Duplication
                if (isset($method[1]) && !is_array($method[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
664
                    throw new \InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
665
                }
666
667
                // Check that the method exists and is callable
668
                $objectMethod = array($object, $method[0]);
669
                if (!is_callable($objectMethod)) {
670
                    throw new \InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
671
                }
672
673
                // Call it
674
                call_user_func_array(
675
                    $objectMethod,
676
                    $this->convertServiceProperty(
677
                        isset($method[1]) ? $method[1] : array()
0 ignored issues
show
Documentation introduced by
isset($method[1]) ? $method[1] : array() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
678
                    )
679
                );
680
            }
681
        }
682
683
        // now, use any cached information about what properties this object type has
684
        // and set based on name resolution
685
        if (!$mapping) {
686
            if ($this->autoScanProperties) {
687
                // we use an object to prevent array copies if/when passed around
688
                $mapping = new ArrayObject();
689
690
                // This performs public variable based injection
691
                $robj = new ReflectionObject($object);
692
                $properties = $robj->getProperties();
693
694
                foreach ($properties as $propertyObject) {
695
                    /* @var $propertyObject ReflectionProperty */
696
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
697
                        $origName = $propertyObject->getName();
698
                        $name = ucfirst($origName);
699
                        if ($this->has($name)) {
700
                            // Pull the name out of the registry
701
                            $value = $this->get($name);
702
                            $propertyObject->setValue($object, $value);
703
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
704
                        }
705
                    }
706
                }
707
708
                // and this performs setter based injection
709
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
710
711
                foreach ($methods as $methodObj) {
712
                    /* @var $methodObj ReflectionMethod */
713
                    $methName = $methodObj->getName();
0 ignored issues
show
Bug introduced by
Consider using $methodObj->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
714
                    if (strpos($methName, 'set') === 0) {
715
                        $pname = substr($methName, 3);
716
                        if ($this->has($pname)) {
717
                            // Pull the name out of the registry
718
                            $value = $this->get($pname);
719
                            $methodObj->invoke($object, $value);
720
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
721
                        }
722
                    }
723
                }
724
725
                // we store the information about what needs to be injected for objects of this
726
                // type here
727
                $this->injectMap[get_class($object)] = $mapping;
728
            }
729
        } else {
730
            foreach ($mapping as $prop => $spec) {
731
                if ($spec['type'] == 'property') {
732
                    $value = $this->get($spec['name']);
733
                    $object->$prop = $value;
734
                } else {
735
                    $method = $prop;
736
                    $value = $this->get($spec['name']);
737
                    $object->$method($value);
738
                }
739
            }
740
        }
741
742
        $injections = Config::inst()->get(get_class($object), 'dependencies');
743
        // If the type defines some injections, set them here
744
        if ($injections && count($injections)) {
745
            foreach ($injections as $property => $value) {
746
                // we're checking empty in case it already has a property at this name
747
                // this doesn't catch privately set things, but they will only be set by a setter method,
748
                // which should be responsible for preventing further setting if it doesn't want it.
749
                if (empty($object->$property)) {
750
                    $value = $this->convertServiceProperty($value);
751
                    $this->setObjectProperty($object, $property, $value);
752
                }
753
            }
754
        }
755
756
        foreach ($this->autoProperties as $property => $value) {
757
            if (!isset($object->$property)) {
758
                $value = $this->convertServiceProperty($value);
759
                $this->setObjectProperty($object, $property, $value);
760
            }
761
        }
762
763
        // Call the 'injected' method if it exists
764
        if (method_exists($object, 'injected')) {
765
            $object->injected();
766
        }
767
    }
768
769
    /**
770
     * Helper to set a property's value
771
     *
772
     * @param object $object
773
     *                  Set an object's property to a specific value
774
     * @param string $name
775
     *                  The name of the property to set
776
     * @param mixed $value
777
     *                  The value to set
778
     */
779
    protected function setObjectProperty($object, $name, $value)
780
    {
781
        if (method_exists($object, 'set'.$name)) {
782
            $object->{'set'.$name}($value);
783
        } else {
784
            $object->$name = $value;
785
        }
786
    }
787
788
    /**
789
     * @deprecated 4.0.0:5.0.0 Use Injector::has() instead
790
     * @param $name
791
     * @return string
792
     */
793
    public function hasService($name)
794
    {
795
        Deprecation::notice('5.0', 'Use Injector::has() instead');
796
797
        return $this->has($name);
798
    }
799
800
    /**
801
     * Does the given service exist?
802
     *
803
     * We do a special check here for services that are using compound names. For example,
804
     * we might want to say that a property should be injected with Log.File or Log.Memory,
805
     * but have only registered a 'Log' service, we'll instead return that.
806
     *
807
     * Will recursively call itself for each depth of dotting.
808
     *
809
     * @param string $name
810
     * @return boolean
811
     */
812
    public function has($name)
813
    {
814
        return (bool)$this->getServiceName($name);
815
    }
816
817
    /**
818
     * Does the given service exist, and if so, what's the stored name for it?
819
     *
820
     * We do a special check here for services that are using compound names. For example,
821
     * we might want to say that a property should be injected with Log.File or Log.Memory,
822
     * but have only registered a 'Log' service, we'll instead return that.
823
     *
824
     * Will recursively call itself for each depth of dotting.
825
     *
826
     * @param string $name
827
     * @return string|null The name of the service (as it might be different from the one passed in)
828
     */
829
    public function getServiceName($name)
830
    {
831
        // common case, get it over with first
832
        if (isset($this->specs[$name])) {
833
            return $name;
834
        }
835
836
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
837
        // invalid name
838
        if (!strpos($name, '.')) {
839
            return null;
840
        }
841
842
        return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
843
    }
844
845
    /**
846
     * Register a service object with an optional name to register it as the
847
     * service for
848
     *
849
     * @param object $service The object to register
850
     * @param string $replace The name of the object to replace (if different to the
851
     * class name of the object to register)
852
     */
853
    public function registerService($service, $replace = null)
854
    {
855
        $registerAt = get_class($service);
856
        if ($replace != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $replace of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
857
            $registerAt = $replace;
858
        }
859
860
        $this->specs[$registerAt] = array('class' => get_class($service));
861
        $this->serviceCache[$registerAt] = $service;
862
        $this->inject($service);
863
    }
864
865
    /**
866
     * Removes a named object from the cached list of objects managed
867
     * by the inject
868
     *
869
     * @param string $name The name to unregister
870
     */
871
    public function unregisterNamedObject($name)
872
    {
873
        unset($this->serviceCache[$name]);
874
    }
875
876
    /**
877
     * Clear out all objects that are managed by the injetor.
878
     */
879
    public function unregisterAllObjects()
880
    {
881
        $this->serviceCache = array('Injector' => $this);
882
    }
883
884
    /**
885
     * Get a named managed object
886
     *
887
     * Will first check to see if the item has been registered as a configured service/bean
888
     * and return that if so.
889
     *
890
     * Next, will check to see if there's any registered configuration for the given type
891
     * and will then try and load that
892
     *
893
     * Failing all of that, will just return a new instance of the specified object.
894
     *
895
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
896
     *
897
     * @param string $name The name of the service to retrieve. If not a registered
898
     * service, then a class of the given name is instantiated
899
     * @param boolean $asSingleton Whether to register the created object as a singleton
900
     * if no other configuration is found
901
     * @param array $constructorArgs Optional set of arguments to pass as constructor arguments
902
     * if this object is to be created from scratch (with $asSingleton = false)
903
     * @return mixed Instance of the specified object
904
     */
905
    public function get($name, $asSingleton = true, $constructorArgs = null)
906
    {
907
        $object = $this->getNamedService($name, $asSingleton, $constructorArgs);
908
909
        if (!$object) {
910
            throw new InjectorNotFoundException("The '{$name}' service could not be found");
911
        }
912
913
        return $object;
914
    }
915
916
    /**
917
     * Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
918
     *
919
     * @param string $name
920
     * @param boolean $asSingleton
921
     * @param array $constructorArgs
922
     * @return mixed|null Instance of the specified object (if it exists)
923
     */
924
    protected function getNamedService($name, $asSingleton = true, $constructorArgs = null)
925
    {
926
        // reassign the name as it might actually be a compound name
927
        if ($serviceName = $this->getServiceName($name)) {
928
            // check to see what the type of bean is. If it's a prototype,
929
            // we don't want to return the singleton version of it.
930
            $spec = $this->specs[$serviceName];
931
            $type = isset($spec['type']) ? $spec['type'] : null;
932
            // if we're explicitly a prototype OR we're not wanting a singleton
933
            if (($type && $type == 'prototype') || !$asSingleton) {
934
                if ($spec && $constructorArgs) {
935
                    $spec['constructor'] = $constructorArgs;
936
                } else {
937
                    // convert any _configured_ constructor args.
938
                    // we don't call this for get() calls where someone passes in
939
                    // constructor args, otherwise we end up calling convertServiceParams
940
                    // way too often
941
                    $this->updateSpecConstructor($spec);
942
                }
943
                return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
944
            } else {
945
                if (!isset($this->serviceCache[$serviceName])) {
946
                    $this->updateSpecConstructor($spec);
947
                    $this->instantiate($spec, $serviceName);
948
                }
949
                return $this->serviceCache[$serviceName];
950
            }
951
        }
952
        $config = $this->configLocator->locateConfigFor($name);
953
        if ($config) {
954
            $this->load(array($name => $config));
955
            if (isset($this->specs[$name])) {
956
                $spec = $this->specs[$name];
957
                $this->updateSpecConstructor($spec);
958
                if ($constructorArgs) {
959
                    $spec['constructor'] = $constructorArgs;
960
                }
961
                return $this->instantiate($spec, $name);
962
            }
963
        }
964
        // If we've got this far, we're dealing with a case of a user wanting
965
        // to create an object based on its name. So, we need to fake its config
966
        // if the user wants it managed as a singleton service style object
967
        $spec = array('class' => $name, 'constructor' => $constructorArgs);
968
        if ($asSingleton) {
969
            // need to load the spec in; it'll be given the singleton type by default
970
            $this->load(array($name => $spec));
971
            return $this->instantiate($spec, $name);
972
        }
973
974
        return $this->instantiate($spec);
975
    }
976
977
    /**
978
     * Magic method to return an item directly
979
     *
980
     * @param string $name
981
     *              The named object to retrieve
982
     * @return mixed
983
     */
984
    public function __get($name)
985
    {
986
        return $this->get($name);
987
    }
988
989
    /**
990
     * Similar to get() but always returns a new object of the given type
991
     *
992
     * Additional parameters are passed through as
993
     *
994
     * @param string $name
995
     * @param mixed $argument,... arguments to pass to the constructor
0 ignored issues
show
Documentation introduced by
There is no parameter named $argument,.... Did you maybe mean $argument?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
996
     * @return mixed A new instance of the specified object
997
     */
998
    public function create($name, $argument = null)
0 ignored issues
show
Unused Code introduced by
The parameter $argument is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
999
    {
1000
        $constructorArgs = func_get_args();
1001
        array_shift($constructorArgs);
1002
        return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
1003
    }
1004
1005
    /**
1006
     * Creates an object with the supplied argument array
1007
     *
1008
     * @param string $name Name of the class to create an object of
1009
     * @param array $constructorArgs Arguments to pass to the constructor
1010
     * @return mixed
1011
     */
1012
    public function createWithArgs($name, $constructorArgs)
1013
    {
1014
        return $this->get($name, false, $constructorArgs);
1015
    }
1016
}
1017