Completed
Push — master ( 90072e...c81959 )
by Daniel
11:23
created

Injector::convertServiceProperty()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
484
            if (isset($this->serviceCache[$id])) {
485
                $this->instantiate(array('class'=>$id), $id);
486
            }
487
        }
488
    }
489
490
    /**
491
     * Update a class specification to convert constructor configuration information if needed
492
     *
493
     * We do this as a separate process to avoid unneeded calls to convertServiceProperty
494
     *
495
     * @param array $spec
496
     *          The class specification to update
497
     */
498
    protected function updateSpecConstructor(&$spec)
499
    {
500
        if (isset($spec['constructor'])) {
501
            $spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
502
        }
503
    }
504
505
    /**
506
     * Recursively convert a value into its proper representation with service references
507
     * resolved to actual objects
508
     *
509
     * @param string $value
510
     * @return array|mixed|string
511
     */
512
    public function convertServiceProperty($value)
513
    {
514
        if (is_array($value)) {
515
            $newVal = array();
516
            foreach ($value as $k => $v) {
517
                $newVal[$k] = $this->convertServiceProperty($v);
518
            }
519
            return $newVal;
520
        }
521
522
        // Evaluate service references
523
        if (is_string($value) && strpos($value, '%$') === 0) {
524
            $id = substr($value, 2);
525
            return $this->get($id);
526
        }
527
528
        // Evaluate constants surrounded by back ticks
529
        if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
530
            $value = defined($matches['name']) ? constant($matches['name']) : null;
531
        }
532
533
        return $value;
534
    }
535
536
    /**
537
     * Instantiate a managed object
538
     *
539
     * Given a specification of the form
540
     *
541
     * array(
542
     *        'class' => 'ClassName',
543
     *        'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
544
     *        'id' => 'ServiceId',
545
     *        'type' => 'singleton|prototype'
546
     * )
547
     *
548
     * will create a new object, store it in the service registry, and
549
     * set any relevant properties
550
     *
551
     * Optionally, you can pass a class name directly for creation
552
     *
553
     * To access this from the outside, you should call ->get('Name') to ensure
554
     * the appropriate checks are made on the specific type.
555
     *
556
     *
557
     * @param array $spec
558
     *                The specification of the class to instantiate
559
     * @param string $id
560
     *                The name of the object being created. If not supplied, then the id will be inferred from the
561
     *                object being created
562
     * @param string $type
563
     *                Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
564
     *                wants the object to be returned
565
     * @return object
566
     */
567
    protected function instantiate($spec, $id = null, $type = null)
568
    {
569
        if (is_string($spec)) {
570
            $spec = array('class' => $spec);
571
        }
572
        $class = $spec['class'];
573
574
        // create the object, using any constructor bindings
575
        $constructorParams = array();
576
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
577
            $constructorParams = $spec['constructor'];
578
        }
579
580
        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
581
        $object = $factory->create($class, $constructorParams);
582
583
        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
584
        // that we don't manage directly; we don't want to store these in the service cache below
585
        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...
586
            $id = isset($spec['id']) ? $spec['id'] : null;
587
        }
588
589
        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
590
        // created anew each time
591
        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...
592
            $type = isset($spec['type']) ? $spec['type'] : null;
593
        }
594
595
        if ($id && (!$type || $type != 'prototype')) {
596
            // this ABSOLUTELY must be set before the object is injected.
597
            // This prevents circular reference errors down the line
598
            $this->serviceCache[$id] = $object;
599
        }
600
601
        // now inject safely
602
        $this->inject($object, $id);
603
604
        return $object;
605
    }
606
607
    /**
608
     * Inject $object with available objects from the service cache
609
     *
610
     * @todo Track all the existing objects that have had a service bound
611
     * into them, so we can update that binding at a later point if needbe (ie
612
     * if the managed service changes)
613
     *
614
     * @param object $object
615
     *              The object to inject
616
     * @param string $asType
617
     *              The ID this item was loaded as. This is so that the property configuration
618
     *              for a type is referenced correctly in case $object is no longer the same
619
     *              type as the loaded config specification had it as.
620
     */
621
    public function inject($object, $asType = null)
622
    {
623
        $objtype = $asType ? $asType : get_class($object);
624
        $mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
625
626
        $spec = empty($this->specs[$objtype]) ? array() : $this->specs[$objtype];
627
628
        // first off, set any properties defined in the service specification for this
629
        // object type
630
        if (!empty($spec['properties']) && is_array($spec['properties'])) {
631
            foreach ($this->specs[$objtype]['properties'] as $key => $value) {
632
                $val = $this->convertServiceProperty($value);
633
                $this->setObjectProperty($object, $key, $val);
634
            }
635
        }
636
637
        // Populate named methods
638
        if (!empty($spec['calls']) && is_array($spec['calls'])) {
639
            foreach ($spec['calls'] as $method) {
640
                // Ignore any blank entries from the array; these may be left in due to config system limitations
641
                if (!$method) {
642
                    continue;
643
                }
644
645
                // Format validation
646
                if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
647
                    throw new \InvalidArgumentException(
648
                        "'calls' entries in service definition should be 1 or 2 element arrays."
649
                    );
650
                }
651
                if (!is_string($method[0])) {
652
                    throw new \InvalidArgumentException("1st element of a 'calls' entry should be a string");
653
                }
654
                if (isset($method[1]) && !is_array($method[1])) {
655
                    throw new \InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
656
                }
657
658
                // Check that the method exists and is callable
659
                $objectMethod = array($object, $method[0]);
660
                if (!is_callable($objectMethod)) {
661
                    throw new \InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
662
                }
663
664
                // Call it
665
                call_user_func_array(
666
                    $objectMethod,
667
                    $this->convertServiceProperty(
668
                        isset($method[1]) ? $method[1] : array()
669
                    )
670
                );
671
            }
672
        }
673
674
        // now, use any cached information about what properties this object type has
675
        // and set based on name resolution
676
        if (!$mapping) {
677
            if ($this->autoScanProperties) {
678
                // we use an object to prevent array copies if/when passed around
679
                $mapping = new ArrayObject();
680
681
                // This performs public variable based injection
682
                $robj = new ReflectionObject($object);
683
                $properties = $robj->getProperties();
684
685
                foreach ($properties as $propertyObject) {
686
                    /* @var $propertyObject ReflectionProperty */
687
                    if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
688
                        $origName = $propertyObject->getName();
689
                        $name = ucfirst($origName);
690
                        if ($this->hasService($name)) {
691
                            // Pull the name out of the registry
692
                            $value = $this->get($name);
693
                            $propertyObject->setValue($object, $value);
694
                            $mapping[$origName] = array('name' => $name, 'type' => 'property');
695
                        }
696
                    }
697
                }
698
699
                // and this performs setter based injection
700
                $methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
701
702
                foreach ($methods as $methodObj) {
703
                    /* @var $methodObj ReflectionMethod */
704
                    $methName = $methodObj->getName();
705
                    if (strpos($methName, 'set') === 0) {
706
                        $pname = substr($methName, 3);
707
                        if ($this->hasService($pname)) {
708
                            // Pull the name out of the registry
709
                            $value = $this->get($pname);
710
                            $methodObj->invoke($object, $value);
711
                            $mapping[$methName] = array('name' => $pname, 'type' => 'method');
712
                        }
713
                    }
714
                }
715
716
                // we store the information about what needs to be injected for objects of this
717
                // type here
718
                $this->injectMap[get_class($object)] = $mapping;
719
            }
720
        } else {
721
            foreach ($mapping as $prop => $spec) {
722
                if ($spec['type'] == 'property') {
723
                    $value = $this->get($spec['name']);
724
                    $object->$prop = $value;
725
                } else {
726
                    $method = $prop;
727
                    $value = $this->get($spec['name']);
728
                    $object->$method($value);
729
                }
730
            }
731
        }
732
733
        $injections = Config::inst()->get(get_class($object), 'dependencies');
734
        // If the type defines some injections, set them here
735
        if ($injections && count($injections)) {
736
            foreach ($injections as $property => $value) {
737
                // we're checking empty in case it already has a property at this name
738
                // this doesn't catch privately set things, but they will only be set by a setter method,
739
                // which should be responsible for preventing further setting if it doesn't want it.
740
                if (empty($object->$property)) {
741
                    $value = $this->convertServiceProperty($value);
742
                    $this->setObjectProperty($object, $property, $value);
743
                }
744
            }
745
        }
746
747
        foreach ($this->autoProperties as $property => $value) {
748
            if (!isset($object->$property)) {
749
                $value = $this->convertServiceProperty($value);
750
                $this->setObjectProperty($object, $property, $value);
751
            }
752
        }
753
754
        // Call the 'injected' method if it exists
755
        if (method_exists($object, 'injected')) {
756
            $object->injected();
757
        }
758
    }
759
760
    /**
761
     * Helper to set a property's value
762
     *
763
     * @param object $object
764
     *                  Set an object's property to a specific value
765
     * @param string $name
766
     *                  The name of the property to set
767
     * @param mixed $value
768
     *                  The value to set
769
     */
770
    protected function setObjectProperty($object, $name, $value)
771
    {
772
        if (method_exists($object, 'set'.$name)) {
773
            $object->{'set'.$name}($value);
774
        } else {
775
            $object->$name = $value;
776
        }
777
    }
778
779
    /**
780
     * Does the given service exist, and if so, what's the stored name for it?
781
     *
782
     * We do a special check here for services that are using compound names. For example,
783
     * we might want to say that a property should be injected with Log.File or Log.Memory,
784
     * but have only registered a 'Log' service, we'll instead return that.
785
     *
786
     * Will recursively call hasService for each depth of dotting
787
     *
788
     * @param string $name
789
     * @return string The name of the service (as it might be different from the one passed in)
790
     * The name of the service (as it might be different from the one passed in)
791
     */
792
    public function hasService($name)
793
    {
794
        // common case, get it overwith first
795
        if (isset($this->specs[$name])) {
796
            return $name;
797
        }
798
799
        // okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
800
        // invalid name
801
        if (!strpos($name, '.')) {
802
            return null;
803
        }
804
805
        return $this->hasService(substr($name, 0, strrpos($name, '.')));
806
    }
807
808
    /**
809
     * Register a service object with an optional name to register it as the
810
     * service for
811
     *
812
     * @param object $service The object to register
813
     * @param string $replace The name of the object to replace (if different to the
814
     * class name of the object to register)
815
     */
816
    public function registerService($service, $replace = null)
817
    {
818
        $registerAt = get_class($service);
819
        if ($replace != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $replace of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
820
            $registerAt = $replace;
821
        }
822
823
        $this->specs[$registerAt] = array('class' => get_class($service));
824
        $this->serviceCache[$registerAt] = $service;
825
        $this->inject($service);
826
    }
827
828
    /**
829
     * Removes a named object from the cached list of objects managed
830
     * by the inject
831
     *
832
     * @param string $name The name to unregister
833
     */
834
    public function unregisterNamedObject($name)
835
    {
836
        unset($this->serviceCache[$name]);
837
    }
838
839
    /**
840
     * Clear out all objects that are managed by the injetor.
841
     */
842
    public function unregisterAllObjects()
843
    {
844
        $this->serviceCache = array('Injector' => $this);
845
    }
846
847
    /**
848
     * Get a named managed object
849
     *
850
     * Will first check to see if the item has been registered as a configured service/bean
851
     * and return that if so.
852
     *
853
     * Next, will check to see if there's any registered configuration for the given type
854
     * and will then try and load that
855
     *
856
     * Failing all of that, will just return a new instance of the
857
     * specificied object.
858
     *
859
     * @param string $name
860
     *              the name of the service to retrieve. If not a registered
861
     *              service, then a class of the given name is instantiated
862
     * @param boolean $asSingleton
863
     *              Whether to register the created object as a singleton
864
     *              if no other configuration is found
865
     * @param array $constructorArgs
866
     *              Optional set of arguments to pass as constructor arguments
867
     *              if this object is to be created from scratch
868
     *              (ie asSingleton = false)
869
     * @return mixed the instance of the specified object
870
     */
871
    public function get($name, $asSingleton = true, $constructorArgs = null)
872
    {
873
        // reassign the name as it might actually be a compound name
874
        if ($serviceName = $this->hasService($name)) {
875
            // check to see what the type of bean is. If it's a prototype,
876
            // we don't want to return the singleton version of it.
877
            $spec = $this->specs[$serviceName];
878
            $type = isset($spec['type']) ? $spec['type'] : null;
879
880
            // if we're explicitly a prototype OR we're not wanting a singleton
881
            if (($type && $type == 'prototype') || !$asSingleton) {
882
                if ($spec && $constructorArgs) {
883
                    $spec['constructor'] = $constructorArgs;
884
                } else {
885
                    // convert any _configured_ constructor args.
886
                    // we don't call this for get() calls where someone passes in
887
                    // constructor args, otherwise we end up calling convertServiceParams
888
                    // way too often
889
                    $this->updateSpecConstructor($spec);
890
                }
891
                return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
892
            } else {
893
                if (!isset($this->serviceCache[$serviceName])) {
894
                    $this->updateSpecConstructor($spec);
895
                    $this->instantiate($spec, $serviceName);
896
                }
897
                return $this->serviceCache[$serviceName];
898
            }
899
        }
900
901
        $config = $this->configLocator->locateConfigFor($name);
902
        if ($config) {
903
            $this->load(array($name => $config));
904
            if (isset($this->specs[$name])) {
905
                $spec = $this->specs[$name];
906
                $this->updateSpecConstructor($spec);
907
                if ($constructorArgs) {
908
                    $spec['constructor'] = $constructorArgs;
909
                }
910
                return $this->instantiate($spec, $name);
911
            }
912
        }
913
914
        // If we've got this far, we're dealing with a case of a user wanting
915
        // to create an object based on its name. So, we need to fake its config
916
        // if the user wants it managed as a singleton service style object
917
        $spec = array('class' => $name, 'constructor' => $constructorArgs);
918
        if ($asSingleton) {
919
            // need to load the spec in; it'll be given the singleton type by default
920
            $this->load(array($name => $spec));
921
            return $this->instantiate($spec, $name);
922
        }
923
924
        return $this->instantiate($spec);
925
    }
926
927
    /**
928
     * Magic method to return an item directly
929
     *
930
     * @param string $name
931
     *              The named object to retrieve
932
     * @return mixed
933
     */
934
    public function __get($name)
935
    {
936
        return $this->get($name);
937
    }
938
939
    /**
940
     * Similar to get() but always returns a new object of the given type
941
     *
942
     * Additional parameters are passed through as
943
     *
944
     * @param string $name
945
     * @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...
946
     * @return mixed A new instance of the specified object
947
     */
948
    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...
949
    {
950
        $constructorArgs = func_get_args();
951
        array_shift($constructorArgs);
952
        return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
953
    }
954
955
    /**
956
     * Creates an object with the supplied argument array
957
     *
958
     * @param string $name Name of the class to create an object of
959
     * @param array $constructorArgs Arguments to pass to the constructor
960
     * @return mixed
961
     */
962
    public function createWithArgs($name, $constructorArgs)
963
    {
964
        return $this->get($name, false, $constructorArgs);
965
    }
966
}
967