Completed
Push — 3.5 ( 1a9180...1bec8a )
by Daniel
24s
created

Injector::instantiate()   F

Complexity

Conditions 15
Paths 288

Size

Total Lines 43
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 19
nc 288
nop 3
dl 0
loc 43
rs 3.7313
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 128 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
require_once FRAMEWORK_PATH . '/src/SilverStripe/Framework/Injector/Factory.php';
4
5
require_once __DIR__ . '/InjectionCreator.php';
6
require_once __DIR__ . '/SilverStripeInjectionCreator.php';
7
require_once __DIR__ . '/ServiceConfigurationLocator.php';
8
require_once __DIR__ . '/SilverStripeServiceConfigurationLocator.php';
9
10
use SilverStripe\Framework\Injector\Factory;
11
12
/**
13
 * A simple injection manager that manages creating objects and injecting
14
 * dependencies between them. It borrows quite a lot from ideas taken from
15
 * Spring's configuration, but is adapted to the stateless PHP way of doing
16
 * things.
17
 *
18
 * In its simplest form, the dependency injector can be used as a mechanism to
19
 * instantiate objects. Simply call
20
 *
21
 * Injector::inst()->get('ClassName')
22
 *
23
 * and a new instance of ClassName will be created and returned to you.
24
 *
25
 * Classes can have specific configuration defined for them to
26
 * indicate dependencies that should be injected. This takes the form of
27
 * a static variable $dependencies defined in the class (or configuration),
28
 * which indicates the name of a property that should be set.
29
 *
30
 * eg
31
 *
32
 * <code>
33
 * class MyController extends Controller {
34
 *
35
 *		public $permissions;
36
 *		public $defaultText;
37
 *
38
 *		static $dependencies = array(
39
 *			'defaultText'		=> 'Override in configuration',
40
 *			'permissions'		=> '%$PermissionService',
41
 *		);
42
 * }
43
 * </code>
44
 *
45
 * will result in an object of type MyController having the defaultText property
46
 * set to 'Override in configuration', and an object identified
47
 * as PermissionService set into the property called 'permissions'. The %$
48
 * syntax tells the injector to look the provided name up as an item to be created
49
 * by the Injector itself.
50
 *
51
 * A key concept of the injector is whether to manage the object as
52
 *
53
 * * A pseudo-singleton, in that only one item will be created for a particular
54
 *   identifier (but the same class could be used for multiple identifiers)
55
 * * A prototype, where the same configuration is used, but a new object is
56
 *   created each time
57
 * * unmanaged, in which case a new object is created and injected, but no
58
 *   information about its state is managed.
59
 *
60
 * Additional configuration of items managed by the injector can be done by
61
 * providing configuration for the types, either by manually loading in an
62
 * array describing the configuration, or by specifying the configuration
63
 * for a type via SilverStripe's configuration mechanism.
64
 *
65
 * Specify a configuration array of the format
66
 *
67
 * <code>
68
 * array(
69
 *		array(
70
 *			'id'			=> 'BeanId',					// the name to be used if diff from the filename
71
 *			'priority'		=> 1,							// priority. If another bean is defined with the same ID,
72
 *															// but has a lower priority, it is NOT overridden
73
 *			'class'			=> 'ClassName',					// the name of the PHP class
74
 *			'src'			=> '/path/to/file'				// the location of the class
75
 *			'type'			=> 'singleton|prototype'		// if you want prototype object generation, set it as the
76
 *			                                                // type
77
 *															// By default, singleton is assumed
78
 *
79
 *			'factory' => 'FactoryService'					// A factory service to use to create instances.
80
 *			'construct'		=> array(						// properties to set at construction
81
 *				'scalar',
82
 *				'%$BeanId',
83
 *			)
84
 *			'properties'	=> array(
85
 *				'name' => 'value'							// scalar value
86
 *				'name' => '%$BeanId',						// a reference to another bean
87
 *				'name' => array(
88
 *					'scalar',
89
 *					'%$BeanId'
90
 *				)
91
 *			)
92
 *		)
93
 *		// alternatively
94
 *		'MyBean'		=> array(
95
 *			'class'			=> 'ClassName',
96
 *		)
97
 *		// or simply
98
 *		'OtherBean'		=> 'SomeClass',
99
 * )
100
 * </code>
101
 *
102
 * In addition to specifying the bindings directly in the configuration,
103
 * you can simply create a publicly accessible property on the target
104
 * class which will automatically be injected if the autoScanProperties
105
 * option is set to true. This means a class defined as
106
 *
107
 * <code>
108
 * class MyController extends Controller {
109
 *
110
 *		private $permissionService;
111
 *
112
 *		public setPermissionService($p) {
113
 *			$this->permissionService = $p;
114
 *		}
115
 * }
116
 * </code>
117
 *
118
 * will have setPermissionService called if
119
 *
120
 * * Injector::inst()->setAutoScanProperties(true) is called and
121
 * * A service named 'PermissionService' has been configured
122
 *
123
 * @author [email protected]
124
 * @package framework
125
 * @subpackage injector
126
 * @license BSD License http://silverstripe.org/bsd-license/
127
 */
128
class Injector {
129
130
	/**
131
	 * Local store of all services
132
	 *
133
	 * @var array
134
	 */
135
	private $serviceCache;
136
137
	/**
138
	 * Cache of items that need to be mapped for each service that gets injected
139
	 *
140
	 * @var array
141
	 */
142
	private $injectMap;
143
144
	/**
145
	 * A store of all the service configurations that have been defined.
146
	 *
147
	 * @var array
148
	 */
149
	private $specs;
150
151
	/**
152
	 * A map of all the properties that should be automagically set on all
153
	 * objects instantiated by the injector
154
	 */
155
	private $autoProperties;
156
157
	/**
158
	 * A singleton if you want to use it that way
159
	 *
160
	 * @var Injector
161
	 */
162
	private static $instance;
163
164
	/**
165
	 * Indicates whether or not to automatically scan properties in injected objects to auto inject
166
	 * stuff, similar to the way grails does things.
167
	 *
168
	 * @var boolean
169
	 */
170
	private $autoScanProperties = false;
171
172
	/**
173
	 * The default factory used to create new instances.
174
	 *
175
	 * The {@link InjectionCreator} is used by default, which simply directly
176
	 * creates objects. This can be changed to use a different default creation
177
	 * method if desired.
178
	 *
179
	 * Each individual component can also specify a custom factory to use by
180
	 * using the `factory` parameter.
181
	 *
182
	 * @var Factory
183
	 */
184
	protected $objectCreator;
185
186
	/**
187
	 * Locator for determining Config properties for services
188
	 *
189
	 * @var ServiceConfigurationLocator
190
	 */
191
	protected $configLocator;
192
193
	/**
194
	 * Create a new injector.
195
	 *
196
	 * @param array $config
197
	 *				Service configuration
198
	 */
199
	public function __construct($config = null) {
200
		$this->injectMap = array();
201
		$this->serviceCache = array(
202
			'Injector'		=> $this,
203
		);
204
		$this->specs = array(
205
			'Injector'		=> array('class' => 'Injector')
206
		);
207
208
		$this->autoProperties = array();
209
210
211
		$creatorClass = isset($config['creator']) ? $config['creator'] : 'InjectionCreator';
212
		$locatorClass = isset($config['locator']) ? $config['locator'] : 'ServiceConfigurationLocator';
213
214
		$this->objectCreator = new $creatorClass;
215
		$this->configLocator = new $locatorClass;
216
217
		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...
218
			$this->load($config);
219
		}
220
	}
221
222
	/**
223
	 * The injector instance this one was copied from when Injector::nest() was called.
224
	 *
225
	 * @var Injector
226
	 */
227
	protected $nestedFrom = null;
228
229
	/**
230
	 * If a user wants to use the injector as a static reference
231
	 *
232
	 * @param array $config
233
	 * @return Injector
234
	 */
235
	public static function inst($config=null) {
236
		if (!self::$instance) {
237
			self::$instance = new Injector($config);
238
		}
239
		return self::$instance;
240
	}
241
242
	/**
243
	 * Sets the default global injector instance.
244
	 *
245
	 * @param Injector $instance
246
	 * @return Injector Reference to new active Injector instance
247
	 */
248
	public static function set_inst(Injector $instance) {
249
		return self::$instance = $instance;
250
	}
251
252
	/**
253
	 * Make the newly active {@link Injector} be a copy of the current active
254
	 * {@link Injector} instance.
255
	 *
256
	 * You can then make changes to the injector with methods such as
257
	 * {@link Injector::inst()->registerService()} which will be discarded
258
	 * upon a subsequent call to {@link Injector::unnest()}
259
	 *
260
	 * @return Injector Reference to new active Injector instance
261
	 */
262
	public static function nest() {
263
		$current = self::inst();
264
265
		$new = clone $current;
266
		$new->nestedFrom = $current;
267
		return self::set_inst($new);
268
	}
269
270
	/**
271
	 * Change the active Injector back to the Injector instance the current active
272
	 * Injector object was copied from.
273
	 *
274
	 * @return Injector Reference to restored active Injector instance
275
	 */
276
	public static function unnest() {
277
		if (self::inst()->nestedFrom) {
278
			self::set_inst(self::inst()->nestedFrom);
279
		}
280
		else {
281
			user_error(
282
				"Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
283
				E_USER_WARNING
284
			);
285
		}
286
		return self::inst();
287
	}
288
289
	/**
290
	 * Indicate whether we auto scan injected objects for properties to set.
291
	 *
292
	 * @param boolean $val
293
	 */
294
	public function setAutoScanProperties($val) {
295
		$this->autoScanProperties = $val;
296
	}
297
298
	/**
299
	 * Sets the default factory to use for creating new objects.
300
	 *
301
	 * @param Factory $obj
302
	 */
303
	public function setObjectCreator(Factory $obj) {
304
		$this->objectCreator = $obj;
305
	}
306
307
	/**
308
	 * @return Factory
309
	 */
310
	public function getObjectCreator() {
311
		return $this->objectCreator;
312
	}
313
314
	/**
315
	 * Set the configuration locator
316
	 * @param ServiceConfigurationLocator $configLocator
317
	 */
318
	public function setConfigLocator($configLocator) {
319
		$this->configLocator = $configLocator;
320
	}
321
322
	/**
323
	 * Retrieve the configuration locator
324
	 * @return ServiceConfigurationLocator
325
	 */
326
	public function getConfigLocator() {
327
		return $this->configLocator;
328
	}
329
330
	/**
331
	 * Add in a specific mapping that should be catered for on a type.
332
	 * This allows configuration of what should occur when an object
333
	 * of a particular type is injected, and what items should be injected
334
	 * for those properties / methods.
335
	 *
336
	 * @param string $class The class to set a mapping for
337
	 * @param string $property The property to set the mapping for
338
	 * @param string $toInject The registered type that will be injected
339
	 * @param string $injectVia Whether to inject by setting a property or calling a setter
340
	 */
341
	public function setInjectMapping($class, $property, $toInject, $injectVia = 'property') {
342
		$mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
343
344
		$mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
345
346
		$this->injectMap[$class] = $mapping;
347
	}
348
349
	/**
350
	 * Add an object that should be automatically set on managed objects
351
	 *
352
	 * This allows you to specify, for example, that EVERY managed object
353
	 * will be automatically inject with a log object by the following
354
	 *
355
	 * $injector->addAutoProperty('log', new Logger());
356
	 *
357
	 * @param string $property
358
	 *				the name of the property
359
	 * @param object $object
360
	 *				the object to be set
361
	 */
362
	public function addAutoProperty($property, $object) {
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
	 */
372
	public function load($config = array()) {
373
		$services = 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
			$name = null;
382
383
			if (file_exists($file)) {
384
				$filename = basename($file);
385
				$name = substr($filename, 0, strrpos($filename, '.'));
386
			}
387
388
			// class is whatever's explicitly set,
389
			$class = isset($spec['class']) ? $spec['class'] : $name;
390
391
			// or the specid if nothing else available.
392
			if (!$class && is_string($specId)) {
393
				$class = $specId;
394
			}
395
396
			// make sure the class is set...
397
			$spec['class'] = $class;
398
399
			$id = is_string($specId) ? $specId : (isset($spec['id']) ? $spec['id'] : $class);
400
401
			$priority = isset($spec['priority']) ? $spec['priority'] : 1;
402
403
			// see if we already have this defined. If so, check priority weighting
404
			if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
405
				if ($this->specs[$id]['priority'] > $priority) {
406
					return;
407
				}
408
			}
409
410
			// okay, actually include it now we know we're going to use it
411
			if (file_exists($file)) {
412
				require_once $file;
413
			}
414
415
			// make sure to set the id for later when instantiating
416
			// to ensure we get cached
417
			$spec['id'] = $id;
418
419
//			We've removed this check because new functionality means that the 'class' field doesn't need to refer
420
//			specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
421
//			functionality
422
//
423
//			if (!class_exists($class)) {
424
//				throw new Exception("Failed to load '$class' from $file");
425
//			}
426
427
			// store the specs for now - we lazy load on demand later on.
428
			$this->specs[$id] = $spec;
429
430
			// EXCEPT when there's already an existing instance at this id.
431
			// if so, we need to instantiate and replace immediately
432
			if (isset($this->serviceCache[$id])) {
433
				$this->updateSpecConstructor($spec);
434
				$this->instantiate($spec, $id);
435
			}
436
		}
437
438
		return $this;
439
	}
440
441
	/**
442
	 * Update the configuration of an already defined service
443
	 *
444
	 * Use this if you don't want to register a complete new config, just append
445
	 * to an existing configuration. Helpful to avoid overwriting someone else's changes
446
	 *
447
	 * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
448
	 *
449
	 * @param string $id
450
	 *				The name of the service to update the definition for
451
	 * @param string $property
452
	 *				The name of the property to update.
453
	 * @param mixed $value
454
	 *				The value to set
455
	 * @param boolean $append
456
	 *				Whether to append (the default) when the property is an array
457
	 */
458
	public function updateSpec($id, $property, $value, $append = true) {
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...)
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...
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
		if (isset($spec['constructor'])) {
486
			$spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
487
		}
488
	}
489
490
	/**
491
	 * Recursively convert a value into its proper representation with service references
492
	 * resolved to actual objects
493
	 *
494
	 * @param string $value
495
	 */
496
	public function convertServiceProperty($value) {
497
		if (is_array($value)) {
498
			$newVal = array();
499
			foreach ($value as $k => $v) {
500
				$newVal[$k] = $this->convertServiceProperty($v);
501
			}
502
			return $newVal;
503
		}
504
505
		if (is_string($value) && strpos($value, '%$') === 0) {
506
			$id = substr($value, 2);
507
			return $this->get($id);
508
		}
509
		return $value;
510
	}
511
512
	/**
513
	 * Instantiate a managed object
514
	 *
515
	 * Given a specification of the form
516
	 *
517
	 * array(
518
	 *		'class' => 'ClassName',
519
	 *		'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
520
	 *		'id' => 'ServiceId',
521
	 *		'type' => 'singleton|prototype'
522
	 * )
523
	 *
524
	 * will create a new object, store it in the service registry, and
525
	 * set any relevant properties
526
	 *
527
	 * Optionally, you can pass a class name directly for creation
528
	 *
529
	 * To access this from the outside, you should call ->get('Name') to ensure
530
	 * the appropriate checks are made on the specific type.
531
	 *
532
	 *
533
	 * @param array $spec
534
	 *				The specification of the class to instantiate
535
	 * @param string $id
536
	 *				The name of the object being created. If not supplied, then the id will be inferred from the
537
	 *				object being created
538
	 * @param string $type
539
	 *				Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
540
	 *				wants the object to be returned
541
	 */
542
	protected function instantiate($spec, $id=null, $type = null) {
543
		if (is_string($spec)) {
544
			$spec = array('class' => $spec);
545
		}
546
		$class = $spec['class'];
547
548
		// create the object, using any constructor bindings
549
		$constructorParams = array();
550
		if (isset($spec['constructor']) && is_array($spec['constructor'])) {
551
			$constructorParams = $spec['constructor'];
552
		}
553
554
		// If we're dealing with a DataObject, pass through Singleton flag as second argument
555
		if ($type != 'prototype' && empty($constructorParams) && is_subclass_of($class, 'DataObject')) {
556
			$constructorParams = array(null, true);
557
		}
558
559
		$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
560
		$object = $factory->create($class, $constructorParams);
561
562
		// figure out if we have a specific id set or not. In some cases, we might be instantiating objects
563
		// that we don't manage directly; we don't want to store these in the service cache below
564
		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...
565
			$id = isset($spec['id']) ? $spec['id'] : null;
566
		}
567
568
		// now set the service in place if needbe. This is NOT done for prototype beans, as they're
569
		// created anew each time
570
		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...
571
			$type = isset($spec['type']) ? $spec['type'] : null;
572
		}
573
574
		if ($id && (!$type || $type != 'prototype')) {
575
			// this ABSOLUTELY must be set before the object is injected.
576
			// This prevents circular reference errors down the line
577
			$this->serviceCache[$id] = $object;
578
		}
579
580
		// now inject safely
581
		$this->inject($object, $id);
582
583
		return $object;
584
	}
585
586
	/**
587
	 * Inject $object with available objects from the service cache
588
	 *
589
	 * @todo Track all the existing objects that have had a service bound
590
	 * into them, so we can update that binding at a later point if needbe (ie
591
	 * if the managed service changes)
592
	 *
593
	 * @param object $object
594
	 *				The object to inject
595
	 * @param string $asType
596
	 *				The ID this item was loaded as. This is so that the property configuration
597
	 *				for a type is referenced correctly in case $object is no longer the same
598
	 *				type as the loaded config specification had it as.
599
	 */
600
	public function inject($object, $asType=null) {
601
		$objtype = $asType ? $asType : get_class($object);
602
		$mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
603
604
		// first off, set any properties defined in the service specification for this
605
		// object type
606
		if (isset($this->specs[$objtype]) && isset($this->specs[$objtype]['properties'])) {
607
			foreach ($this->specs[$objtype]['properties'] as $key => $value) {
608
				$val = $this->convertServiceProperty($value);
609
				$this->setObjectProperty($object, $key, $val);
610
			}
611
		}
612
613
		// now, use any cached information about what properties this object type has
614
		// and set based on name resolution
615
		if (!$mapping) {
616
			if ($this->autoScanProperties) {
617
				// we use an object to prevent array copies if/when passed around
618
				$mapping = new ArrayObject();
619
620
				// This performs public variable based injection
621
				$robj = new ReflectionObject($object);
622
				$properties = $robj->getProperties();
623
624
				foreach ($properties as $propertyObject) {
625
					/* @var $propertyObject ReflectionProperty */
626
					if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
627
						$origName = $propertyObject->getName();
628
						$name = ucfirst($origName);
629
						if ($this->hasService($name)) {
630
							// Pull the name out of the registry
631
							$value = $this->get($name);
632
							$propertyObject->setValue($object, $value);
633
							$mapping[$origName] = array('name' => $name, 'type' => 'property');
634
						}
635
					}
636
				}
637
638
				// and this performs setter based injection
639
				$methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
640
641
				foreach ($methods as $methodObj) {
642
					/* @var $methodObj ReflectionMethod */
643
					$methName = $methodObj->getName();
644
					if (strpos($methName, 'set') === 0) {
645
						$pname = substr($methName, 3);
646
						if ($this->hasService($pname)) {
647
							// Pull the name out of the registry
648
							$value = $this->get($pname);
649
							$methodObj->invoke($object, $value);
650
							$mapping[$methName] = array('name' => $pname, 'type' => 'method');
651
						}
652
					}
653
				}
654
655
				// we store the information about what needs to be injected for objects of this
656
				// type here
657
				$this->injectMap[get_class($object)] = $mapping;
658
			}
659
		} else {
660
			foreach ($mapping as $prop => $spec) {
661
				if ($spec['type'] == 'property') {
662
					$value = $this->get($spec['name']);
663
					$object->$prop = $value;
664
				} else {
665
					$method = $prop;
666
					$value = $this->get($spec['name']);
667
					$object->$method($value);
668
				}
669
			}
670
		}
671
672
		$injections = Config::inst()->get(get_class($object), 'dependencies');
673
		// If the type defines some injections, set them here
674
		if ($injections && count($injections)) {
675
			foreach ($injections as $property => $value) {
0 ignored issues
show
Bug introduced by
The expression $injections of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
676
				// we're checking empty in case it already has a property at this name
677
				// this doesn't catch privately set things, but they will only be set by a setter method,
678
				// which should be responsible for preventing further setting if it doesn't want it.
679
				if (empty($object->$property)) {
680
					$value = $this->convertServiceProperty($value);
681
					$this->setObjectProperty($object, $property, $value);
682
				}
683
			}
684
		}
685
686
		foreach ($this->autoProperties as $property => $value) {
687
			if (!isset($object->$property)) {
688
				$value = $this->convertServiceProperty($value);
689
				$this->setObjectProperty($object, $property, $value);
690
			}
691
		}
692
693
		// Call the 'injected' method if it exists
694
		if (method_exists($object, 'injected')) {
695
			$object->injected();
696
		}
697
	}
698
699
	/**
700
	 * Helper to set a property's value
701
	 *
702
	 * @param object $object
703
	 *					Set an object's property to a specific value
704
	 * @param string $name
705
	 *					The name of the property to set
706
	 * @param mixed $value
707
	 *					The value to set
708
	 */
709
	protected function setObjectProperty($object, $name, $value) {
710
		if (method_exists($object, 'set'.$name)) {
711
			$object->{'set'.$name}($value);
712
		} else {
713
			$object->$name = $value;
714
		}
715
	}
716
717
	/**
718
	 * Does the given service exist, and if so, what's the stored name for it?
719
	 *
720
	 * We do a special check here for services that are using compound names. For example,
721
	 * we might want to say that a property should be injected with Log.File or Log.Memory,
722
	 * but have only registered a 'Log' service, we'll instead return that.
723
	 *
724
	 * Will recursively call hasService for each depth of dotting
725
	 *
726
	 * @return string
727
	 *				The name of the service (as it might be different from the one passed in)
728
	 */
729
	public function hasService($name) {
730
		// common case, get it overwith first
731
		if (isset($this->specs[$name])) {
732
			return $name;
733
		}
734
735
		// okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
736
		// invalid name
737
		if (!strpos($name, '.')) {
738
			return null;
739
		}
740
741
		return $this->hasService(substr($name, 0, strrpos($name, '.')));
742
	}
743
744
	/**
745
	 * Register a service object with an optional name to register it as the
746
	 * service for
747
	 *
748
	 * @param stdClass $service
749
	 *					The object to register
750
	 * @param string $replace
751
	 *					The name of the object to replace (if different to the
752
	 *					class name of the object to register)
753
	 *
754
	 */
755
	public function registerService($service, $replace = null) {
756
		$registerAt = get_class($service);
757
		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...
758
			$registerAt = $replace;
759
		}
760
761
		$this->specs[$registerAt] = array('class' => get_class($service));
762
		$this->serviceCache[$registerAt] = $service;
763
		$this->inject($service);
764
	}
765
766
	/**
767
	 * Register a service with an explicit name
768
	 *
769
	 * @deprecated since 4.0
770
	 */
771
	public function registerNamedService($name, $service) {
772
		Deprecation::notice('4.0', 'registerNamedService is deprecated, use registerService instead');
773
		return $this->registerService($service, $name);
774
	}
775
776
	/**
777
	 * Removes a named object from the cached list of objects managed
778
	 * by the inject
779
	 *
780
	 * @param string $name The name to unregister
781
	 */
782
	public function unregisterNamedObject($name) {
783
		unset($this->serviceCache[$name]);
784
	}
785
786
	/**
787
	 * Clear out all objects that are managed by the injetor.
788
	 */
789
	public function unregisterAllObjects() {
790
		$this->serviceCache = array('Injector' => $this);
791
	}
792
793
	/**
794
	 * Get a named managed object
795
	 *
796
	 * Will first check to see if the item has been registered as a configured service/bean
797
	 * and return that if so.
798
	 *
799
	 * Next, will check to see if there's any registered configuration for the given type
800
	 * and will then try and load that
801
	 *
802
	 * Failing all of that, will just return a new instance of the
803
	 * specificied object.
804
	 *
805
	 * @param string $name
806
	 *				the name of the service to retrieve. If not a registered
807
	 *				service, then a class of the given name is instantiated
808
	 * @param boolean $asSingleton
809
	 *				Whether to register the created object as a singleton
810
	 *				if no other configuration is found
811
	 * @param array $constructorArgs
812
	 *				Optional set of arguments to pass as constructor arguments
813
	 *				if this object is to be created from scratch
814
	 *				(ie asSingleton = false)
815
	 * @return mixed the instance of the specified object
816
	 */
817
	public function get($name, $asSingleton = true, $constructorArgs = null) {
818
		// reassign the name as it might actually be a compound name
819
		if ($serviceName = $this->hasService($name)) {
820
			// check to see what the type of bean is. If it's a prototype,
821
			// we don't want to return the singleton version of it.
822
			$spec = $this->specs[$serviceName];
823
			$type = isset($spec['type']) ? $spec['type'] : null;
824
825
			// if we're explicitly a prototype OR we're not wanting a singleton
826
			if (($type && $type == 'prototype') || !$asSingleton) {
827
				if ($spec && $constructorArgs) {
828
					$spec['constructor'] = $constructorArgs;
829
				} else {
830
					// convert any _configured_ constructor args.
831
					// we don't call this for get() calls where someone passes in
832
					// constructor args, otherwise we end up calling convertServiceParams
833
					// way too often
834
					$this->updateSpecConstructor($spec);
835
				}
836
				return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
837
			} else {
838
				if (!isset($this->serviceCache[$serviceName])) {
839
					$this->updateSpecConstructor($spec);
840
					$this->instantiate($spec, $serviceName);
841
				}
842
				return $this->serviceCache[$serviceName];
843
			}
844
		}
845
846
		$config = $this->configLocator->locateConfigFor($name);
847
		if ($config) {
848
			$this->load(array($name => $config));
849
			if (isset($this->specs[$name])) {
850
				$spec = $this->specs[$name];
851
				$this->updateSpecConstructor($spec);
852
				if ($constructorArgs) {
853
					$spec['constructor'] = $constructorArgs;
854
				}
855
				return $this->instantiate($spec, $name);
856
			}
857
		}
858
859
		// If we've got this far, we're dealing with a case of a user wanting
860
		// to create an object based on its name. So, we need to fake its config
861
		// if the user wants it managed as a singleton service style object
862
		$spec = array('class' => $name, 'constructor' => $constructorArgs);
863
		if ($asSingleton) {
864
			// need to load the spec in; it'll be given the singleton type by default
865
			$this->load(array($name => $spec));
866
			return $this->instantiate($spec, $name);
867
		}
868
869
		return $this->instantiate($spec);
870
	}
871
872
	/**
873
	 * Magic method to return an item directly
874
	 *
875
	 * @param string $name
876
	 *				The named object to retrieve
877
	 * @return mixed
878
	 */
879
	public function __get($name) {
880
		return $this->get($name);
881
	}
882
883
	/**
884
	 * Similar to get() but always returns a new object of the given type
885
	 *
886
	 * Additional parameters are passed through as
887
	 *
888
	 * @param string $name
889
	 * @return mixed A new instance of the specified object
890
	 */
891
	public function create($name) {
892
		$constructorArgs = func_get_args();
893
		array_shift($constructorArgs);
894
		return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
895
	}
896
897
	/**
898
	 * Creates an object with the supplied argument array
899
	 *
900
	 * @param string $name
901
	 *				Name of the class to create an object of
902
	 * @param array $args
0 ignored issues
show
Bug introduced by
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

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

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

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

Loading history...
903
	 *				Arguments to pass to the constructor
904
	 * @return mixed
905
	 */
906
	public function createWithArgs($name, $constructorArgs) {
907
		return $this->get($name, false, $constructorArgs);
908
	}
909
}
910