Completed
Push — director-middleware ( 408832...334a87 )
by Sam
08:23
created

Injector::createFromSpec()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Injector;
4
5
use ArrayObject;
6
use InvalidArgumentException;
7
use Psr\Container\ContainerInterface;
8
use Psr\Container\NotFoundExceptionInterface;
9
use ReflectionMethod;
10
use ReflectionObject;
11
use ReflectionProperty;
12
use SilverStripe\Core\ClassInfo;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Dev\Deprecation;
15
16
/**
17
 * A simple injection manager that manages creating objects and injecting
18
 * dependencies between them. It borrows quite a lot from ideas taken from
19
 * Spring's configuration, but is adapted to the stateless PHP way of doing
20
 * things.
21
 *
22
 * In its simplest form, the dependency injector can be used as a mechanism to
23
 * instantiate objects. Simply call
24
 *
25
 * Injector::inst()->get('ClassName')
26
 *
27
 * and a new instance of ClassName will be created and returned to you.
28
 *
29
 * Classes can have specific configuration defined for them to
30
 * indicate dependencies that should be injected. This takes the form of
31
 * a static variable $dependencies defined in the class (or configuration),
32
 * which indicates the name of a property that should be set.
33
 *
34
 * eg
35
 *
36
 * <code>
37
 * class MyController extends Controller {
38
 *
39
 *      public $permissions;
40
 *      public $defaultText;
41
 *
42
 *      static $dependencies = array(
43
 *          'defaultText'       => 'Override in configuration',
44
 *          'permissions'       => '%$PermissionService',
45
 *      );
46
 * }
47
 * </code>
48
 *
49
 * will result in an object of type MyController having the defaultText property
50
 * set to 'Override in configuration', and an object identified
51
 * as PermissionService set into the property called 'permissions'. The %$
52
 * syntax tells the injector to look the provided name up as an item to be created
53
 * by the Injector itself.
54
 *
55
 * A key concept of the injector is whether to manage the object as
56
 *
57
 * * A pseudo-singleton, in that only one item will be created for a particular
58
 *   identifier (but the same class could be used for multiple identifiers)
59
 * * A prototype, where the same configuration is used, but a new object is
60
 *   created each time
61
 * * unmanaged, in which case a new object is created and injected, but no
62
 *   information about its state is managed.
63
 *
64
 * Additional configuration of items managed by the injector can be done by
65
 * providing configuration for the types, either by manually loading in an
66
 * array describing the configuration, or by specifying the configuration
67
 * for a type via SilverStripe's configuration mechanism.
68
 *
69
 * Specify a configuration array of the format
70
 *
71
 * <code>
72
 * array(
73
 *      array(
74
 *          'id'            => 'BeanId',                    // the name to be used if diff from the filename
75
 *          'priority'      => 1,                           // priority. If another bean is defined with the same ID,
76
 *                                                          // but has a lower priority, it is NOT overridden
77
 *          'class'         => 'ClassName',                 // the name of the PHP class
78
 *          'src'           => '/path/to/file'              // the location of the class
79
 *          'type'          => 'singleton|prototype'        // if you want prototype object generation, set it as the
80
 *                                                          // type
81
 *                                                          // By default, singleton is assumed
82
 *
83
 *          'factory' => 'FactoryService'                   // A factory service to use to create instances.
84
 *          'construct'     => array(                       // properties to set at construction
85
 *              'scalar',
86
 *              '%$BeanId',
87
 *          )
88
 *          'properties'    => array(
89
 *              'name' => 'value'                           // scalar value
90
 *              'name' => '%$BeanId',                       // a reference to another bean
91
 *              'name' => array(
92
 *                  'scalar',
93
 *                  '%$BeanId'
94
 *              )
95
 *          )
96
 *      )
97
 *      // alternatively
98
 *      'MyBean'        => array(
99
 *          'class'         => 'ClassName',
100
 *      )
101
 *      // or simply
102
 *      'OtherBean'     => 'SomeClass',
103
 * )
104
 * </code>
105
 *
106
 * In addition to specifying the bindings directly in the configuration,
107
 * you can simply create a publicly accessible property on the target
108
 * class which will automatically be injected if the autoScanProperties
109
 * option is set to true. This means a class defined as
110
 *
111
 * <code>
112
 * class MyController extends Controller {
113
 *
114
 *      private $permissionService;
115
 *
116
 *      public setPermissionService($p) {
117
 *          $this->permissionService = $p;
118
 *      }
119
 * }
120
 * </code>
121
 *
122
 * will have setPermissionService called if
123
 *
124
 * * Injector::inst()->setAutoScanProperties(true) is called and
125
 * * A service named 'PermissionService' has been configured
126
 *
127
 * @author [email protected]
128
 * @license BSD License http://silverstripe.org/bsd-license/
129
 */
130
class Injector implements ContainerInterface
131
{
132
133
    /**
134
     * Local store of all services
135
     *
136
     * @var array
137
     */
138
    private $serviceCache;
139
140
    /**
141
     * Cache of items that need to be mapped for each service that gets injected
142
     *
143
     * @var array
144
     */
145
    private $injectMap;
146
147
    /**
148
     * A store of all the service configurations that have been defined.
149
     *
150
     * @var array
151
     */
152
    private $specs;
153
154
    /**
155
     * A map of all the properties that should be automagically set on all
156
     * objects instantiated by the injector
157
     */
158
    private $autoProperties;
159
160
    /**
161
     * A singleton if you want to use it that way
162
     *
163
     * @var Injector
164
     */
165
    private static $instance;
166
167
    /**
168
     * Indicates whether or not to automatically scan properties in injected objects to auto inject
169
     * stuff, similar to the way grails does things.
170
     *
171
     * @var boolean
172
     */
173
    private $autoScanProperties = false;
174
175
    /**
176
     * The default factory used to create new instances.
177
     *
178
     * The {@link InjectionCreator} is used by default, which simply directly
179
     * creates objects. This can be changed to use a different default creation
180
     * method if desired.
181
     *
182
     * Each individual component can also specify a custom factory to use by
183
     * using the `factory` parameter.
184
     *
185
     * @var Factory
186
     */
187
    protected $objectCreator;
188
189
    /**
190
     * Locator for determining Config properties for services
191
     *
192
     * @var ServiceConfigurationLocator
193
     */
194
    protected $configLocator;
195
196
    /**
197
     * Create a new injector.
198
     *
199
     * @param array $config
200
     *              Service configuration
201
     */
202
    public function __construct($config = null)
203
    {
204
        $this->injectMap = array();
205
        $this->serviceCache = array(
206
            'Injector' => $this,
207
        );
208
        $this->specs = [
209
            'Injector' => ['class' => static::class]
210
        ];
211
        $this->autoProperties = array();
212
        $creatorClass = isset($config['creator'])
213
            ? $config['creator']
214
            : InjectionCreator::class;
215
        $locatorClass = isset($config['locator'])
216
            ? $config['locator']
217
            : SilverStripeServiceConfigurationLocator::class;
218
219
        $this->objectCreator = new $creatorClass;
220
        $this->configLocator = new $locatorClass;
221
222
        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...
223
            $this->load($config);
224
        }
225
    }
226
227
    /**
228
     * The injector instance this one was copied from when Injector::nest() was called.
229
     *
230
     * @var Injector
231
     */
232
    protected $nestedFrom = null;
233
234
    /**
235
     * @return Injector
236
     */
237
    public static function inst()
238
    {
239
        return InjectorLoader::inst()->getManifest();
240
    }
241
242
    /**
243
     * Make the newly active {@link Injector} be a copy of the current active
244
     * {@link Injector} instance.
245
     *
246
     * You can then make changes to the injector with methods such as
247
     * {@link Injector::inst()->registerService()} which will be discarded
248
     * upon a subsequent call to {@link Injector::unnest()}
249
     *
250
     * @return Injector Reference to new active Injector instance
251
     */
252
    public static function nest()
253
    {
254
        // Clone current injector and nest
255
        $new = clone self::inst();
256
        InjectorLoader::inst()->pushManifest($new);
257
        return $new;
258
    }
259
260
    /**
261
     * Change the active Injector back to the Injector instance the current active
262
     * Injector object was copied from.
263
     *
264
     * @return Injector Reference to restored active Injector instance
265
     */
266 View Code Duplication
    public static function unnest()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
267
    {
268
        // Unnest unless we would be left at 0 manifests
269
        $loader = InjectorLoader::inst();
270
        if ($loader->countManifests() <= 1) {
271
            user_error(
272
                "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
273
                E_USER_WARNING
274
            );
275
        } else {
276
            $loader->popManifest();
277
        }
278
        return static::inst();
279
    }
280
281
    /**
282
     * Indicate whether we auto scan injected objects for properties to set.
283
     *
284
     * @param boolean $val
285
     */
286
    public function setAutoScanProperties($val)
287
    {
288
        $this->autoScanProperties = $val;
289
    }
290
291
    /**
292
     * Sets the default factory to use for creating new objects.
293
     *
294
     * @param \SilverStripe\Core\Injector\Factory $obj
295
     */
296
    public function setObjectCreator(Factory $obj)
297
    {
298
        $this->objectCreator = $obj;
299
    }
300
301
    /**
302
     * @return Factory
303
     */
304
    public function getObjectCreator()
305
    {
306
        return $this->objectCreator;
307
    }
308
309
    /**
310
     * Set the configuration locator
311
     * @param ServiceConfigurationLocator $configLocator
312
     */
313
    public function setConfigLocator($configLocator)
314
    {
315
        $this->configLocator = $configLocator;
316
    }
317
318
    /**
319
     * Retrieve the configuration locator
320
     * @return ServiceConfigurationLocator
321
     */
322
    public function getConfigLocator()
323
    {
324
        return $this->configLocator;
325
    }
326
327
    /**
328
     * Add in a specific mapping that should be catered for on a type.
329
     * This allows configuration of what should occur when an object
330
     * of a particular type is injected, and what items should be injected
331
     * for those properties / methods.
332
     *
333
     * @param string $class The class to set a mapping for
334
     * @param string $property The property to set the mapping for
335
     * @param string $toInject The registered type that will be injected
336
     * @param string $injectVia Whether to inject by setting a property or calling a setter
337
     */
338
    public function setInjectMapping($class, $property, $toInject, $injectVia = 'property')
339
    {
340
        $mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
341
342
        $mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
343
344
        $this->injectMap[$class] = $mapping;
345
    }
346
347
    /**
348
     * Add an object that should be automatically set on managed objects
349
     *
350
     * This allows you to specify, for example, that EVERY managed object
351
     * will be automatically inject with a log object by the following
352
     *
353
     * $injector->addAutoProperty('log', new Logger());
354
     *
355
     * @param string $property
356
     *                the name of the property
357
     * @param object $object
358
     *                the object to be set
359
     * @return $this
360
     */
361
    public function addAutoProperty($property, $object)
362
    {
363
        $this->autoProperties[$property] = $object;
364
        return $this;
365
    }
366
367
    /**
368
     * Load services using the passed in configuration for those services
369
     *
370
     * @param array $config
371
     * @return $this
372
     */
373
    public function load($config = array())
374
    {
375
        foreach ($config as $specId => $spec) {
376
            if (is_string($spec)) {
377
                $spec = array('class' => $spec);
378
            }
379
380
            $file = isset($spec['src']) ? $spec['src'] : null;
381
382
            // class is whatever's explicitly set,
383
            $class = isset($spec['class']) ? $spec['class'] : null;
384
385
            // or the specid if nothing else available.
386
            if (!$class && is_string($specId)) {
387
                $class = $specId;
388
            }
389
390
            // make sure the class is set...
391
            if (empty($class)) {
392
                throw new InvalidArgumentException('Missing spec class');
393
            }
394
            $spec['class'] = $class;
395
396
            $id = is_string($specId)
397
                ? $specId
398
                : (isset($spec['id']) ? $spec['id'] : $class);
399
400
            $priority = isset($spec['priority']) ? $spec['priority'] : 1;
401
402
            // see if we already have this defined. If so, check priority weighting
403
            if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
404
                if ($this->specs[$id]['priority'] > $priority) {
405
                    return $this;
406
                }
407
            }
408
409
            // okay, actually include it now we know we're going to use it
410
            if (file_exists($file)) {
411
                require_once $file;
412
            }
413
414
            // make sure to set the id for later when instantiating
415
            // to ensure we get cached
416
            $spec['id'] = $id;
417
418
//			We've removed this check because new functionality means that the 'class' field doesn't need to refer
419
//			specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
420
//			functionality
421
//
422
//			if (!class_exists($class)) {
423
//				throw new Exception("Failed to load '$class' from $file");
424
//			}
425
426
            // store the specs for now - we lazy load on demand later on.
427
            $this->specs[$id] = $spec;
428
429
            // EXCEPT when there's already an existing instance at this id.
430
            // if so, we need to instantiate and replace immediately
431
            if (isset($this->serviceCache[$id])) {
432
                $this->updateSpecConstructor($spec);
433
                $this->instantiate($spec, $id);
434
            }
435
        }
436
437
        return $this;
438
    }
439
440
    /**
441
     * Update the configuration of an already defined service
442
     *
443
     * Use this if you don't want to register a complete new config, just append
444
     * to an existing configuration. Helpful to avoid overwriting someone else's changes
445
     *
446
     * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
447
     *
448
     * @param string $id
449
     *              The name of the service to update the definition for
450
     * @param string $property
451
     *              The name of the property to update.
452
     * @param mixed $value
453
     *              The value to set
454
     * @param boolean $append
455
     *              Whether to append (the default) when the property is an array
456
     */
457
    public function updateSpec($id, $property, $value, $append = true)
458
    {
459
        if (isset($this->specs[$id]['properties'][$property])) {
460
            // by ref so we're updating the actual value
461
            $current = &$this->specs[$id]['properties'][$property];
462
            if (is_array($current) && $append) {
463
                $current[] = $value;
464
            } else {
465
                $this->specs[$id]['properties'][$property] = $value;
466
            }
467
468
            // and reload the object; existing bindings don't get
469
            // updated though! (for now...)
470
            if (isset($this->serviceCache[$id])) {
471
                $this->instantiate(array('class'=>$id), $id);
472
            }
473
        }
474
    }
475
476
    /**
477
     * Update a class specification to convert constructor configuration information if needed
478
     *
479
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
480
     *
481
     * @param array $spec
482
     *          The class specification to update
483
     */
484
    protected function updateSpecConstructor(&$spec)
485
    {
486
        if (isset($spec['constructor'])) {
487
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
488
        }
489
    }
490
491
    /**
492
     * Recursively convert a value into its proper representation with service references
493
     * resolved to actual objects
494
     *
495
     * @param string $value
496
     * @return array|mixed|string
497
     */
498
    public function convertServiceProperty($value)
499
    {
500
        if (is_array($value)) {
501
            $newVal = array();
502
            foreach ($value as $k => $v) {
503
                $newVal[$k] = $this->convertServiceProperty($v);
504
            }
505
            return $newVal;
506
        }
507
508
        // Evaluate service references
509
        if (is_string($value) && strpos($value, '%$') === 0) {
510
            $id = substr($value, 2);
511
            return $this->get($id);
512
        }
513
514
        // Evaluate constants surrounded by back ticks
515
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
516
            if (getenv($matches['name']) !== false) {
517
                $value = getenv($matches['name']);
518
            } elseif (defined($matches['name'])) {
519
                $value = constant($matches['name']);
520
            } else {
521
                $value = null;
522
            }
523
        }
524
525
        return $value;
526
    }
527
528
    /**
529
     * Instantiate an object from the given spec, without saving to the service cache
530
     */
531
    public function createFromSpec(array $spec)
532
    {
533
        $this->updateSpecConstructor($spec);
534
        return $this->instantiate($spec, null, 'prototype');
535
    }
536
537
    /**
538
     * Instantiate a managed object
539
     *
540
     * Given a specification of the form
541
     *
542
     * array(
543
     *        'class' => 'ClassName',
544
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
545
     *        'id' => 'ServiceId',
546
     *        'type' => 'singleton|prototype'
547
     * )
548
     *
549
     * will create a new object, store it in the service registry, and
550
     * set any relevant properties
551
     *
552
     * Optionally, you can pass a class name directly for creation
553
     *
554
     * To access this from the outside, you should call ->get('Name') to ensure
555
     * the appropriate checks are made on the specific type.
556
     *
557
     *
558
     * @param array $spec
559
     *                The specification of the class to instantiate
560
     * @param string $id
561
     *                The name of the object being created. If not supplied, then the id will be inferred from the
562
     *                object being created
563
     * @param string $type
564
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
565
     *                wants the object to be returned
566
     * @return object
567
     */
568
    protected function instantiate($spec, $id = null, $type = null)
569
    {
570
        if (is_string($spec)) {
571
            $spec = array('class' => $spec);
572
        }
573
        $class = $spec['class'];
574
575
        // create the object, using any constructor bindings
576
        $constructorParams = array();
577
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
578
            $constructorParams = $spec['constructor'];
579
        }
580
581
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
582
        $object = $factory->create($class, $constructorParams);
583
584
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
585
        // that we don't manage directly; we don't want to store these in the service cache below
586
        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...
587
            $id = isset($spec['id']) ? $spec['id'] : null;
588
        }
589
590
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
591
        // created anew each time
592
        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...
593
            $type = isset($spec['type']) ? $spec['type'] : null;
594
        }
595
596
        if ($id && (!$type || $type != 'prototype')) {
597
            // this ABSOLUTELY must be set before the object is injected.
598
            // This prevents circular reference errors down the line
599
            $this->serviceCache[$id] = $object;
600
        }
601
602
        // now inject safely
603
        $this->inject($object, $spec, $id);
604
605
        return $object;
606
    }
607
608
    /**
609
     * Inject $object with available objects from the service cache
610
     *
611
     * @todo Track all the existing objects that have had a service bound
612
     * into them, so we can update that binding at a later point if needbe (ie
613
     * if the managed service changes)
614
     *
615
     * @param object $object
616
     *              The object to inject
617
     * @param string $asType
618
     *              The ID this item was loaded as. This is so that the property configuration
619
     *              for a type is referenced correctly in case $object is no longer the same
620
     *              type as the loaded config specification had it as.
621
     */
622
    protected function inject($object, $spec, $asType = null)
623
    {
624
        $objtype = $asType ? $asType : get_class($object);
625
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
626
627
        // first off, set any properties defined in the service specification for this
628
        // object type
629
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
630
            foreach ($spec['properties'] as $key => $value) {
631
                $val = $this->convertServiceProperty($value);
632
                $this->setObjectProperty($object, $key, $val);
633
            }
634
        }
635
636
        // Populate named methods
637
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
638
            foreach ($spec['calls'] as $method) {
639
                // Ignore any blank entries from the array; these may be left in due to config system limitations
640
                if (!$method) {
641
                    continue;
642
                }
643
644
                // Format validation
645 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...
646
                    throw new InvalidArgumentException(
647
                        "'calls' entries in service definition should be 1 or 2 element arrays."
648
                    );
649
                }
650
                if (!is_string($method[0])) {
651
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
652
                }
653 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...
654
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
655
                }
656
657
                // Check that the method exists and is callable
658
                $objectMethod = array($object, $method[0]);
659
                if (!is_callable($objectMethod)) {
660
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
661
                }
662
663
                // Call it
664
                call_user_func_array(
665
                    $objectMethod,
666
                    $this->convertServiceProperty(
667
                        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...
668
                    )
669
                );
670
            }
671
        }
672
673
        // now, use any cached information about what properties this object type has
674
        // and set based on name resolution
675
        if (!$mapping) {
676
            if ($this->autoScanProperties) {
677
                // we use an object to prevent array copies if/when passed around
678
                $mapping = new ArrayObject();
679
680
                // This performs public variable based injection
681
                $robj = new ReflectionObject($object);
682
                $properties = $robj->getProperties();
683
684
                foreach ($properties as $propertyObject) {
685
                    /* @var $propertyObject ReflectionProperty */
686
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
687
                        $origName = $propertyObject->getName();
688
                        $name = ucfirst($origName);
689
                        if ($this->has($name)) {
690
                            // Pull the name out of the registry
691
                            $value = $this->get($name);
692
                            $propertyObject->setValue($object, $value);
693
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
694
                        }
695
                    }
696
                }
697
698
                // and this performs setter based injection
699
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
700
701
                foreach ($methods as $methodObj) {
702
                    /* @var $methodObj ReflectionMethod */
703
                    $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...
704
                    if (strpos($methName, 'set') === 0) {
705
                        $pname = substr($methName, 3);
706
                        if ($this->has($pname)) {
707
                            // Pull the name out of the registry
708
                            $value = $this->get($pname);
709
                            $methodObj->invoke($object, $value);
710
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
711
                        }
712
                    }
713
                }
714
715
                // we store the information about what needs to be injected for objects of this
716
                // type here
717
                $this->injectMap[get_class($object)] = $mapping;
718
            }
719
        } else {
720
            foreach ($mapping as $prop => $spec) {
721
                if ($spec['type'] == 'property') {
722
                    $value = $this->get($spec['name']);
723
                    $object->$prop = $value;
724
                } else {
725
                    $method = $prop;
726
                    $value = $this->get($spec['name']);
727
                    $object->$method($value);
728
                }
729
            }
730
        }
731
732
        $injections = Config::inst()->get(get_class($object), 'dependencies');
733
        // If the type defines some injections, set them here
734
        if ($injections && count($injections)) {
735
            foreach ($injections as $property => $value) {
736
                // we're checking empty in case it already has a property at this name
737
                // this doesn't catch privately set things, but they will only be set by a setter method,
738
                // which should be responsible for preventing further setting if it doesn't want it.
739
                if (empty($object->$property)) {
740
                    $value = $this->convertServiceProperty($value);
741
                    $this->setObjectProperty($object, $property, $value);
742
                }
743
            }
744
        }
745
746
        foreach ($this->autoProperties as $property => $value) {
747
            if (!isset($object->$property)) {
748
                $value = $this->convertServiceProperty($value);
749
                $this->setObjectProperty($object, $property, $value);
750
            }
751
        }
752
753
        // Call the 'injected' method if it exists
754
        if (method_exists($object, 'injected')) {
755
            $object->injected();
756
        }
757
    }
758
759
    /**
760
     * Helper to set a property's value
761
     *
762
     * @param object $object
763
     *                  Set an object's property to a specific value
764
     * @param string $name
765
     *                  The name of the property to set
766
     * @param mixed $value
767
     *                  The value to set
768
     */
769
    protected function setObjectProperty($object, $name, $value)
770
    {
771
        if (method_exists($object, 'set'.$name)) {
772
            $object->{'set'.$name}($value);
773
        } else {
774
            $object->$name = $value;
775
        }
776
    }
777
778
    /**
779
     * @deprecated 4.0.0:5.0.0 Use Injector::has() instead
780
     * @param $name
781
     * @return string
782
     */
783
    public function hasService($name)
784
    {
785
        Deprecation::notice('5.0', 'Use Injector::has() instead');
786
787
        return $this->has($name);
788
    }
789
790
    /**
791
     * Does the given service exist?
792
     *
793
     * We do a special check here for services that are using compound names. For example,
794
     * we might want to say that a property should be injected with Log.File or Log.Memory,
795
     * but have only registered a 'Log' service, we'll instead return that.
796
     *
797
     * Will recursively call itself for each depth of dotting.
798
     *
799
     * @param string $name
800
     * @return boolean
801
     */
802
    public function has($name)
803
    {
804
        return (bool)$this->getServiceName($name);
805
    }
806
807
    /**
808
     * Does the given service exist, and if so, what's the stored name for it?
809
     *
810
     * We do a special check here for services that are using compound names. For example,
811
     * we might want to say that a property should be injected with Log.File or Log.Memory,
812
     * but have only registered a 'Log' service, we'll instead return that.
813
     *
814
     * Will recursively call itself for each depth of dotting.
815
     *
816
     * @param string $name
817
     * @return string|null The name of the service (as it might be different from the one passed in)
818
     */
819
    public function getServiceName($name)
820
    {
821
        // common case, get it overwith first
822
        if (isset($this->specs[$name])) {
823
            return $name;
824
        }
825
826
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
827
        // invalid name
828
        if (!strpos($name, '.')) {
829
            return null;
830
        }
831
832
        return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
833
    }
834
835
    /**
836
     * Register a service object with an optional name to register it as the
837
     * service for
838
     *
839
     * @param object $service The object to register
840
     * @param string $replace The name of the object to replace (if different to the
841
     * class name of the object to register)
842
     * @return $this
843
     */
844
    public function registerService($service, $replace = null)
845
    {
846
        $registerAt = get_class($service);
847
        if ($replace !== null) {
848
            $registerAt = $replace;
849
        }
850
851
        $this->specs[$registerAt] = array('class' => get_class($service));
852
        $this->serviceCache[$registerAt] = $service;
853
        return $this;
854
    }
855
856
    /**
857
     * Removes a named object from the cached list of objects managed
858
     * by the inject
859
     *
860
     * @param string $name The name to unregister
861
     * @return $this
862
     */
863
    public function unregisterNamedObject($name)
864
    {
865
        unset($this->serviceCache[$name]);
866
        return $this;
867
    }
868
869
    /**
870
     * Clear out objects of one or more types that are managed by the injetor.
871
     *
872
     * @param array|string $types Base class of object (not service name) to remove
873
     * @return $this
874
     */
875
    public function unregisterObjects($types)
876
    {
877
        if (!is_array($types)) {
878
            $types = [ $types ];
879
        }
880
881
        // Filter all objects
882
        foreach ($this->serviceCache as $key => $object) {
883
            foreach ($types as $filterClass) {
884
                // Prevent destructive flushing
885
                if (strcasecmp($filterClass, 'object') === 0) {
886
                    throw new InvalidArgumentException("Global unregistration is not allowed");
887
                }
888
                if ($object instanceof $filterClass) {
889
                    unset($this->serviceCache[$key]);
890
                    break;
891
                }
892
            }
893
        }
894
        return $this;
895
    }
896
897
    /**
898
     * Get a named managed object
899
     *
900
     * Will first check to see if the item has been registered as a configured service/bean
901
     * and return that if so.
902
     *
903
     * Next, will check to see if there's any registered configuration for the given type
904
     * and will then try and load that
905
     *
906
     * Failing all of that, will just return a new instance of the specified object.
907
     *
908
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
909
     *
910
     * @param string $name The name of the service to retrieve. If not a registered
911
     * service, then a class of the given name is instantiated
912
     * @param boolean $asSingleton Whether to register the created object as a singleton
913
     * if no other configuration is found
914
     * @param array $constructorArgs Optional set of arguments to pass as constructor arguments
915
     * if this object is to be created from scratch (with $asSingleton = false)
916
     * @return mixed Instance of the specified object
917
     */
918
    public function get($name, $asSingleton = true, $constructorArgs = [])
919
    {
920
        $object = $this->getNamedService($name, $asSingleton, $constructorArgs);
921
922
        if (!$object) {
923
            throw new InjectorNotFoundException("The '{$name}' service could not be found");
924
        }
925
926
        return $object;
927
    }
928
929
    /**
930
     * Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
931
     *
932
     * @param string $name
933
     * @param boolean $asSingleton
934
     * @param array $constructorArgs
935
     * @return mixed|null Instance of the specified object (if it exists)
936
     */
937
    protected function getNamedService($name, $asSingleton = true, $constructorArgs = [])
938
    {
939
        // Allow service names of the form "%$ServiceName"
940
        if (substr($name, 0, 2) == '%$') {
941
            $name = substr($name, 2);
942
        }
943
944
        // Normalise service / args
945
        list($name, $constructorArgs) = $this->normaliseArguments($name, $constructorArgs);
946
947
        // reassign the name as it might actually be a compound name
948
        if ($serviceName = $this->getServiceName($name)) {
949
            // check to see what the type of bean is. If it's a prototype,
950
            // we don't want to return the singleton version of it.
951
            $spec = $this->specs[$serviceName];
952
            $type = isset($spec['type']) ? $spec['type'] : null;
953
            // if we're explicitly a prototype OR we're not wanting a singleton
954
            if (($type && $type == 'prototype') || !$asSingleton) {
955
                if ($spec && $constructorArgs) {
956
                    $spec['constructor'] = $constructorArgs;
957
                } else {
958
                    // convert any _configured_ constructor args.
959
                    // we don't call this for get() calls where someone passes in
960
                    // constructor args, otherwise we end up calling convertServiceParams
961
                    // way too often
962
                    $this->updateSpecConstructor($spec);
963
                }
964
                return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
965
            } else {
966
                if (!isset($this->serviceCache[$serviceName])) {
967
                    $this->updateSpecConstructor($spec);
968
                    $this->instantiate($spec, $serviceName);
969
                }
970
                return $this->serviceCache[$serviceName];
971
            }
972
        }
973
        $config = $this->configLocator->locateConfigFor($name);
974
        if ($config) {
975
            $this->load(array($name => $config));
976
            if (isset($this->specs[$name])) {
977
                $spec = $this->specs[$name];
978
                $this->updateSpecConstructor($spec);
979
                if ($constructorArgs) {
980
                    $spec['constructor'] = $constructorArgs;
981
                }
982
                return $this->instantiate($spec, $name);
983
            }
984
        }
985
        // If we've got this far, we're dealing with a case of a user wanting
986
        // to create an object based on its name. So, we need to fake its config
987
        // if the user wants it managed as a singleton service style object
988
        $spec = array('class' => $name, 'constructor' => $constructorArgs);
989
        if ($asSingleton) {
990
            // need to load the spec in; it'll be given the singleton type by default
991
            $this->load(array($name => $spec));
992
            return $this->instantiate($spec, $name);
993
        }
994
995
        return $this->instantiate($spec);
996
    }
997
998
    /**
999
     * Detect service references with constructor arguments included.
1000
     * These will be split out of the service name reference and appended
1001
     * to the $args
1002
     *
1003
     * @param string $name
1004
     * @param array $args
1005
     * @return array Two items with name and new args
1006
     */
1007
    protected function normaliseArguments($name, $args = [])
1008
    {
1009
        if (strstr($name, '(')) {
1010
            list($name, $extraArgs) = ClassInfo::parse_class_spec($name);
1011
            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...
1012
                $args = array_merge($args, $extraArgs);
1013
            } else {
1014
                $args = $extraArgs;
1015
            }
1016
        }
1017
        return [ $name, $args ];
1018
    }
1019
1020
    /**
1021
     * Magic method to return an item directly
1022
     *
1023
     * @param string $name
1024
     *              The named object to retrieve
1025
     * @return mixed
1026
     */
1027
    public function __get($name)
1028
    {
1029
        return $this->get($name);
1030
    }
1031
1032
    /**
1033
     * Similar to get() but always returns a new object of the given type
1034
     *
1035
     * Additional parameters are passed through as
1036
     *
1037
     * @param string $name
1038
     * @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...
1039
     * @return mixed A new instance of the specified object
1040
     */
1041
    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...
1042
    {
1043
        $constructorArgs = func_get_args();
1044
        array_shift($constructorArgs);
1045
        return $this->createWithArgs($name, $constructorArgs);
1046
    }
1047
1048
    /**
1049
     * Creates an object with the supplied argument array
1050
     *
1051
     * @param string $name Name of the class to create an object of
1052
     * @param array $constructorArgs Arguments to pass to the constructor
1053
     * @return mixed
1054
     */
1055
    public function createWithArgs($name, $constructorArgs)
1056
    {
1057
        return $this->get($name, false, $constructorArgs);
1058
    }
1059
}
1060