Completed
Push — director-middleware ( 92e508...4c9b4c )
by Damian
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 a managed object
530
     *
531
     * Given a specification of the form
532
     *
533
     * array(
534
     *        'class' => 'ClassName',
535
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
536
     *        'id' => 'ServiceId',
537
     *        'type' => 'singleton|prototype'
538
     * )
539
     *
540
     * will create a new object, store it in the service registry, and
541
     * set any relevant properties
542
     *
543
     * Optionally, you can pass a class name directly for creation
544
     *
545
     * To access this from the outside, you should call ->get('Name') to ensure
546
     * the appropriate checks are made on the specific type.
547
     *
548
     *
549
     * @param array $spec
550
     *                The specification of the class to instantiate
551
     * @param string $id
552
     *                The name of the object being created. If not supplied, then the id will be inferred from the
553
     *                object being created
554
     * @param string $type
555
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
556
     *                wants the object to be returned
557
     * @return object
558
     */
559
    protected function instantiate($spec, $id = null, $type = null)
560
    {
561
        if (is_string($spec)) {
562
            $spec = array('class' => $spec);
563
        }
564
        $class = $spec['class'];
565
566
        // create the object, using any constructor bindings
567
        $constructorParams = array();
568
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
569
            $constructorParams = $spec['constructor'];
570
        }
571
572
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
573
        $object = $factory->create($class, $constructorParams);
574
575
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
576
        // that we don't manage directly; we don't want to store these in the service cache below
577
        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...
578
            $id = isset($spec['id']) ? $spec['id'] : null;
579
        }
580
581
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
582
        // created anew each time
583
        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...
584
            $type = isset($spec['type']) ? $spec['type'] : null;
585
        }
586
587
        if ($id && (!$type || $type != 'prototype')) {
588
            // this ABSOLUTELY must be set before the object is injected.
589
            // This prevents circular reference errors down the line
590
            $this->serviceCache[$id] = $object;
591
        }
592
593
        // now inject safely
594
        $this->inject($object, $id);
595
596
        return $object;
597
    }
598
599
    /**
600
     * Inject $object with available objects from the service cache
601
     *
602
     * @todo Track all the existing objects that have had a service bound
603
     * into them, so we can update that binding at a later point if needbe (ie
604
     * if the managed service changes)
605
     *
606
     * @param object $object
607
     *              The object to inject
608
     * @param string $asType
609
     *              The ID this item was loaded as. This is so that the property configuration
610
     *              for a type is referenced correctly in case $object is no longer the same
611
     *              type as the loaded config specification had it as.
612
     */
613
    public function inject($object, $asType = null)
614
    {
615
        $objtype = $asType ? $asType : get_class($object);
616
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
617
618
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
619
620
        // first off, set any properties defined in the service specification for this
621
        // object type
622
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
623
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
624
                $val = $this->convertServiceProperty($value);
625
                $this->setObjectProperty($object, $key, $val);
626
            }
627
        }
628
629
        // Populate named methods
630
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
631
            foreach ($spec['calls'] as $method) {
632
                // Ignore any blank entries from the array; these may be left in due to config system limitations
633
                if (!$method) {
634
                    continue;
635
                }
636
637
                // Format validation
638 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...
639
                    throw new InvalidArgumentException(
640
                        "'calls' entries in service definition should be 1 or 2 element arrays."
641
                    );
642
                }
643
                if (!is_string($method[0])) {
644
                    throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
645
                }
646 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...
647
                    throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
648
                }
649
650
                // Check that the method exists and is callable
651
                $objectMethod = array($object, $method[0]);
652
                if (!is_callable($objectMethod)) {
653
                    throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
654
                }
655
656
                // Call it
657
                call_user_func_array(
658
                    $objectMethod,
659
                    $this->convertServiceProperty(
660
                        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...
661
                    )
662
                );
663
            }
664
        }
665
666
        // now, use any cached information about what properties this object type has
667
        // and set based on name resolution
668
        if (!$mapping) {
669
            if ($this->autoScanProperties) {
670
                // we use an object to prevent array copies if/when passed around
671
                $mapping = new ArrayObject();
672
673
                // This performs public variable based injection
674
                $robj = new ReflectionObject($object);
675
                $properties = $robj->getProperties();
676
677
                foreach ($properties as $propertyObject) {
678
                    /* @var $propertyObject ReflectionProperty */
679
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
680
                        $origName = $propertyObject->getName();
681
                        $name = ucfirst($origName);
682
                        if ($this->has($name)) {
683
                            // Pull the name out of the registry
684
                            $value = $this->get($name);
685
                            $propertyObject->setValue($object, $value);
686
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
687
                        }
688
                    }
689
                }
690
691
                // and this performs setter based injection
692
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
693
694
                foreach ($methods as $methodObj) {
695
                    /* @var $methodObj ReflectionMethod */
696
                    $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...
697
                    if (strpos($methName, 'set') === 0) {
698
                        $pname = substr($methName, 3);
699
                        if ($this->has($pname)) {
700
                            // Pull the name out of the registry
701
                            $value = $this->get($pname);
702
                            $methodObj->invoke($object, $value);
703
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
704
                        }
705
                    }
706
                }
707
708
                // we store the information about what needs to be injected for objects of this
709
                // type here
710
                $this->injectMap[get_class($object)] = $mapping;
711
            }
712
        } else {
713
            foreach ($mapping as $prop => $spec) {
714
                if ($spec['type'] == 'property') {
715
                    $value = $this->get($spec['name']);
716
                    $object->$prop = $value;
717
                } else {
718
                    $method = $prop;
719
                    $value = $this->get($spec['name']);
720
                    $object->$method($value);
721
                }
722
            }
723
        }
724
725
        $injections = Config::inst()->get(get_class($object), 'dependencies');
726
        // If the type defines some injections, set them here
727
        if ($injections && count($injections)) {
728
            foreach ($injections as $property => $value) {
729
                // we're checking empty in case it already has a property at this name
730
                // this doesn't catch privately set things, but they will only be set by a setter method,
731
                // which should be responsible for preventing further setting if it doesn't want it.
732
                if (empty($object->$property)) {
733
                    $value = $this->convertServiceProperty($value);
734
                    $this->setObjectProperty($object, $property, $value);
735
                }
736
            }
737
        }
738
739
        foreach ($this->autoProperties as $property => $value) {
740
            if (!isset($object->$property)) {
741
                $value = $this->convertServiceProperty($value);
742
                $this->setObjectProperty($object, $property, $value);
743
            }
744
        }
745
746
        // Call the 'injected' method if it exists
747
        if (method_exists($object, 'injected')) {
748
            $object->injected();
749
        }
750
    }
751
752
    /**
753
     * Helper to set a property's value
754
     *
755
     * @param object $object
756
     *                  Set an object's property to a specific value
757
     * @param string $name
758
     *                  The name of the property to set
759
     * @param mixed $value
760
     *                  The value to set
761
     */
762
    protected function setObjectProperty($object, $name, $value)
763
    {
764
        if (method_exists($object, 'set'.$name)) {
765
            $object->{'set'.$name}($value);
766
        } else {
767
            $object->$name = $value;
768
        }
769
    }
770
771
    /**
772
     * @deprecated 4.0.0:5.0.0 Use Injector::has() instead
773
     * @param $name
774
     * @return string
775
     */
776
    public function hasService($name)
777
    {
778
        Deprecation::notice('5.0', 'Use Injector::has() instead');
779
780
        return $this->has($name);
781
    }
782
783
    /**
784
     * Does the given service exist?
785
     *
786
     * We do a special check here for services that are using compound names. For example,
787
     * we might want to say that a property should be injected with Log.File or Log.Memory,
788
     * but have only registered a 'Log' service, we'll instead return that.
789
     *
790
     * Will recursively call itself for each depth of dotting.
791
     *
792
     * @param string $name
793
     * @return boolean
794
     */
795
    public function has($name)
796
    {
797
        return (bool)$this->getServiceName($name);
798
    }
799
800
    /**
801
     * Does the given service exist, and if so, what's the stored name for it?
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 string|null The name of the service (as it might be different from the one passed in)
811
     */
812
    public function getServiceName($name)
813
    {
814
        // common case, get it overwith first
815
        if (isset($this->specs[$name])) {
816
            return $name;
817
        }
818
819
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
820
        // invalid name
821
        if (!strpos($name, '.')) {
822
            return null;
823
        }
824
825
        return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
826
    }
827
828
    /**
829
     * Register a service object with an optional name to register it as the
830
     * service for
831
     *
832
     * @param object $service The object to register
833
     * @param string $replace The name of the object to replace (if different to the
834
     * class name of the object to register)
835
     * @return $this
836
     */
837
    public function registerService($service, $replace = null)
838
    {
839
        $registerAt = get_class($service);
840
        if ($replace !== null) {
841
            $registerAt = $replace;
842
        }
843
844
        $this->specs[$registerAt] = array('class' => get_class($service));
845
        $this->serviceCache[$registerAt] = $service;
846
        return $this;
847
    }
848
849
    /**
850
     * Removes a named object from the cached list of objects managed
851
     * by the inject
852
     *
853
     * @param string $name The name to unregister
854
     * @param bool $flushSpecs Set to true to clear spec for this service
855
     * @return $this
856
     */
857
    public function unregisterNamedObject($name, $flushSpecs = false)
858
    {
859
        unset($this->serviceCache[$name]);
860
        if ($flushSpecs) {
861
            unset($this->specs[$name]);
862
        }
863
        return $this;
864
    }
865
866
    /**
867
     * Clear out objects of one or more types that are managed by the injetor.
868
     *
869
     * @param array|string $types Base class of object (not service name) to remove
870
     * @param bool $flushSpecs Set to true to clear spec for this service
871
     * @return $this
872
     */
873
    public function unregisterObjects($types, $flushSpecs = false)
874
    {
875
        if (!is_array($types)) {
876
            $types = [ $types ];
877
        }
878
879
        // Filter all objects
880
        foreach ($this->serviceCache as $key => $object) {
881
            foreach ($types as $filterClass) {
882
                // Prevent destructive flushing
883
                if (strcasecmp($filterClass, 'object') === 0) {
884
                    throw new InvalidArgumentException("Global unregistration is not allowed");
885
                }
886
                if ($object instanceof $filterClass) {
887
                    $this->unregisterNamedObject($key, $flushSpecs);
888
                    break;
889
                }
890
            }
891
        }
892
        return $this;
893
    }
894
895
    /**
896
     * Get a named managed object
897
     *
898
     * Will first check to see if the item has been registered as a configured service/bean
899
     * and return that if so.
900
     *
901
     * Next, will check to see if there's any registered configuration for the given type
902
     * and will then try and load that
903
     *
904
     * Failing all of that, will just return a new instance of the specified object.
905
     *
906
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
907
     *
908
     * @param string $name The name of the service to retrieve. If not a registered
909
     * service, then a class of the given name is instantiated
910
     * @param boolean $asSingleton Whether to register the created object as a singleton
911
     * if no other configuration is found
912
     * @param array $constructorArgs Optional set of arguments to pass as constructor arguments
913
     * if this object is to be created from scratch (with $asSingleton = false)
914
     * @return mixed Instance of the specified object
915
     */
916
    public function get($name, $asSingleton = true, $constructorArgs = [])
917
    {
918
        $object = $this->getNamedService($name, $asSingleton, $constructorArgs);
919
920
        if (!$object) {
921
            throw new InjectorNotFoundException("The '{$name}' service could not be found");
922
        }
923
924
        return $object;
925
    }
926
927
    /**
928
     * Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
929
     *
930
     * @param string $name
931
     * @param boolean $asSingleton
932
     * @param array $constructorArgs
933
     * @return mixed|null Instance of the specified object (if it exists)
934
     */
935
    protected function getNamedService($name, $asSingleton = true, $constructorArgs = [])
936
    {
937
        // Allow service names of the form "%$ServiceName"
938
        if (substr($name, 0, 2) == '%$') {
939
            $name = substr($name, 2);
940
        }
941
942
        // Normalise service / args
943
        list($name, $constructorArgs) = $this->normaliseArguments($name, $constructorArgs);
944
945
        // reassign the name as it might actually be a compound name
946
        if ($serviceName = $this->getServiceName($name)) {
947
            // check to see what the type of bean is. If it's a prototype,
948
            // we don't want to return the singleton version of it.
949
            $spec = $this->specs[$serviceName];
950
            $type = isset($spec['type']) ? $spec['type'] : null;
951
            // if we're explicitly a prototype OR we're not wanting a singleton
952
            if (($type && $type == 'prototype') || !$asSingleton) {
953
                if ($spec && $constructorArgs) {
954
                    $spec['constructor'] = $constructorArgs;
955
                } else {
956
                    // convert any _configured_ constructor args.
957
                    // we don't call this for get() calls where someone passes in
958
                    // constructor args, otherwise we end up calling convertServiceParams
959
                    // way too often
960
                    $this->updateSpecConstructor($spec);
961
                }
962
                return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
963
            } else {
964
                if (!isset($this->serviceCache[$serviceName])) {
965
                    $this->updateSpecConstructor($spec);
966
                    $this->instantiate($spec, $serviceName);
967
                }
968
                return $this->serviceCache[$serviceName];
969
            }
970
        }
971
        $config = $this->configLocator->locateConfigFor($name);
972
        if ($config) {
973
            $this->load(array($name => $config));
974
            if (isset($this->specs[$name])) {
975
                $spec = $this->specs[$name];
976
                $this->updateSpecConstructor($spec);
977
                if ($constructorArgs) {
978
                    $spec['constructor'] = $constructorArgs;
979
                }
980
                return $this->instantiate($spec, $name);
981
            }
982
        }
983
        // If we've got this far, we're dealing with a case of a user wanting
984
        // to create an object based on its name. So, we need to fake its config
985
        // if the user wants it managed as a singleton service style object
986
        $spec = array('class' => $name, 'constructor' => $constructorArgs);
987
        if ($asSingleton) {
988
            // need to load the spec in; it'll be given the singleton type by default
989
            $this->load(array($name => $spec));
990
            return $this->instantiate($spec, $name);
991
        }
992
993
        return $this->instantiate($spec);
994
    }
995
996
    /**
997
     * Detect service references with constructor arguments included.
998
     * These will be split out of the service name reference and appended
999
     * to the $args
1000
     *
1001
     * @param string $name
1002
     * @param array $args
1003
     * @return array Two items with name and new args
1004
     */
1005
    protected function normaliseArguments($name, $args = [])
1006
    {
1007
        if (strstr($name, '(')) {
1008
            list($name, $extraArgs) = ClassInfo::parse_class_spec($name);
1009
            if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1010
                $args = array_merge($args, $extraArgs);
1011
            } else {
1012
                $args = $extraArgs;
1013
            }
1014
        }
1015
        return [ $name, $args ];
1016
    }
1017
1018
    /**
1019
     * Magic method to return an item directly
1020
     *
1021
     * @param string $name
1022
     *              The named object to retrieve
1023
     * @return mixed
1024
     */
1025
    public function __get($name)
1026
    {
1027
        return $this->get($name);
1028
    }
1029
1030
    /**
1031
     * Similar to get() but always returns a new object of the given type
1032
     *
1033
     * Additional parameters are passed through as
1034
     *
1035
     * @param string $name
1036
     * @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...
1037
     * @return mixed A new instance of the specified object
1038
     */
1039
    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...
1040
    {
1041
        $constructorArgs = func_get_args();
1042
        array_shift($constructorArgs);
1043
        return $this->createWithArgs($name, $constructorArgs);
1044
    }
1045
1046
    /**
1047
     * Creates an object with the supplied argument array
1048
     *
1049
     * @param string $name Name of the class to create an object of
1050
     * @param array $constructorArgs Arguments to pass to the constructor
1051
     * @return mixed
1052
     */
1053
    public function createWithArgs($name, $constructorArgs)
1054
    {
1055
        return $this->get($name, false, $constructorArgs);
1056
    }
1057
}
1058