Skip to content
This repository
Browse code

FEATURE: Added dependency injector for managing creation of new objec…

…ts and their dependencies.

API CHANGE: Pass Object::create() calls to Injector::create().
API CHANGE: Add "RequestProcessor" injection point in Director, that Director will call preRequest() and postRequest() on.
  • Loading branch information...
commit b269badfbe8daa77c4d80d9de1106160d6206d91 1 parent 3412a0e
authored sminnee committed
6  _config/RequestProcessor.yml
... ...
@@ -0,0 +1,6 @@
  1
+name: RequestProcessor
  2
+---
  3
+# Providing an empty config so it can be overridden at a later point
  4
+Injector:
  5
+  RequestProcessor: 
  6
+      0: 
37  control/Director.php
@@ -97,8 +97,14 @@ static function direct($url, DataModel $model) {
97 97
 		if(!isset($_SESSION) && (isset($_COOKIE[session_name()]) || isset($_REQUEST[session_name()]))) Session::start();
98 98
 		// Initiate an empty session - doesn't initialize an actual PHP session until saved (see belwo)
99 99
 		$session = new Session(isset($_SESSION) ? $_SESSION : null);
  100
+
  101
+		$output = Injector::inst()->get('RequestProcessor')->preRequest($req, $session, $model);
100 102
 		
101  
-		// Main request handling
  103
+		if ($output === false) {
  104
+			// @TODO Need to NOT proceed with the request in an elegant manner
  105
+			throw new SS_HTTPResponse_Exception(_t('Director.INVALID_REQUEST', 'Invalid request'), 400);
  106
+		}
  107
+
102 108
 		$result = Director::handleRequest($req, $session, $model);
103 109
 
104 110
 		// Save session data (and start/resume it if required)
@@ -108,8 +114,10 @@ static function direct($url, DataModel $model) {
108 114
 		if(is_string($result) && substr($result,0,9) == 'redirect:') {
109 115
 			$response = new SS_HTTPResponse();
110 116
 			$response->redirect(substr($result, 9));
111  
-			$response->output();
112  
-
  117
+			$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
  118
+			if ($res !== false) {
  119
+				$response->output();
  120
+			}
113 121
 		// Handle a controller
114 122
 		} else if($result) {
115 123
 			if($result instanceof SS_HTTPResponse) {
@@ -120,15 +128,22 @@ static function direct($url, DataModel $model) {
120 128
 				$response->setBody($result);
121 129
 			}
122 130
 			
123  
-			// ?debug_memory=1 will output the number of bytes of memory used for this request
124  
-			if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
125  
-				Debug::message(sprintf(
126  
-					"Peak memory usage in bytes: %s", 
127  
-					number_format(memory_get_peak_usage(),0)
128  
-				));
  131
+			$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
  132
+			if ($res !== false) {
  133
+				// ?debug_memory=1 will output the number of bytes of memory used for this request
  134
+				if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
  135
+					Debug::message(sprintf(
  136
+						"Peak memory usage in bytes: %s", 
  137
+						number_format(memory_get_peak_usage(),0)
  138
+					));
  139
+				} else {
  140
+					$response->output();
  141
+				}
129 142
 			} else {
130  
-				$response->output();
  143
+				// @TODO Proper response here.
  144
+				throw new SS_HTTPResponse_Exception("Invalid response");
131 145
 			}
  146
+			
132 147
 
133 148
 			//$controllerObj->getSession()->inst_save();
134 149
 		}
@@ -255,7 +270,7 @@ protected static function handleRequest(SS_HTTPRequest $request, Session $sessio
255 270
 
256 271
 					} else {
257 272
 						Director::$urlParams = $arguments;
258  
-						$controllerObj = new $controller();
  273
+						$controllerObj = Injector::inst()->create($controller);
259 274
 						$controllerObj->setSession($session);
260 275
 
261 276
 						try {
24  control/RequestFilter.php
... ...
@@ -0,0 +1,24 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * A request filter is an object that's executed before and after a
  5
+ * request occurs. By returning 'false' from the preRequest method,
  6
+ * request execution will be stopped from continuing
  7
+ *
  8
+ * @author marcus@silverstripe.com.au
  9
+ * @license BSD License http://silverstripe.org/bsd-license/
  10
+ */
  11
+interface RequestFilter {
  12
+	/**
  13
+	 * Filter executed before a request processes
  14
+	 * 
  15
+	 * @return boolean (optional)
  16
+	 *				Whether to continue processing other filters
  17
+	 */
  18
+	public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model);
  19
+
  20
+	/**
  21
+	 * Filter executed AFTER a request
  22
+	 */
  23
+	public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model);
  24
+}
37  control/RequestProcessor.php
... ...
@@ -0,0 +1,37 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * Description of RequestProcessor
  5
+ *
  6
+ * @author marcus@silverstripe.com.au
  7
+ * @license BSD License http://silverstripe.org/bsd-license/
  8
+ */
  9
+class RequestProcessor {
  10
+
  11
+	private $filters = array();
  12
+
  13
+	public function __construct($filters = array()) {
  14
+		$this->filters = $filters;
  15
+	}
  16
+
  17
+	public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
  18
+		foreach ($this->filters as $filter) {
  19
+			$res = $filter->preRequest($request, $session, $model);
  20
+			if ($res === false) {
  21
+				return false;
  22
+			}
  23
+		}
  24
+	}
  25
+
  26
+	/**
  27
+	 * Filter executed AFTER a request
  28
+	 */
  29
+	public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
  30
+		foreach ($this->filters as $filter) {
  31
+			$res = $filter->postRequest($request, $response, $model);
  32
+			if ($res === false) {
  33
+				return false;
  34
+			}
  35
+		}
  36
+	}
  37
+}
27  control/injector/AfterCallAspect.php
... ...
@@ -0,0 +1,27 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * An AfterCallAspect is run after a method is executed
  5
+ * 
  6
+ * This is a declared interface, but isn't actually required
  7
+ * as PHP doesn't really care about types... 
  8
+ *
  9
+ * @author Marcus Nyeholt <marcus@silverstripe.com.au>
  10
+ * @package sapphire
  11
+ * @subpackage injector
  12
+ * @license BSD http://silverstripe.org/BSD-license
  13
+ */
  14
+interface AfterCallAspect {
  15
+	
  16
+	/**
  17
+	 * Call this aspect after a method is executed
  18
+	 * 
  19
+	 * @param object $proxied
  20
+	 *				The object having the method called upon it. 
  21
+	 * @param string $method
  22
+	 *				The name of the method being called
  23
+	 * @param string $args
  24
+	 *				The arguments that were passed to the method call
  25
+	 */
  26
+	public function afterCall($proxied, $method, $args);
  27
+}
41  control/injector/AopProxyService.php
... ...
@@ -0,0 +1,41 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * A class that proxies another, allowing various functionality to be
  5
+ * injected
  6
+ * 
  7
+ * @author marcus@silverstripe.com.au
  8
+ * @package sapphire
  9
+ * @subpackage injector
  10
+ * 
  11
+ * @license http://silverstripe.org/bsd-license/
  12
+ */
  13
+class AopProxyService {
  14
+	public $beforeCall = array();
  15
+
  16
+	public $afterCall = array();
  17
+
  18
+	public $proxied;
  19
+
  20
+	public function __call($method, $args) {
  21
+		if (method_exists($this->proxied, $method)) {
  22
+			$continue = true;
  23
+			if (isset($this->beforeCall[$method])) {
  24
+				$result = $this->beforeCall[$method]->beforeCall($this->proxied, $method, $args);
  25
+				if ($result === false) {
  26
+					$continue = false;
  27
+				}
  28
+			}
  29
+
  30
+			if ($continue) {
  31
+				$result = call_user_func_array(array($this->proxied, $method), $args);
  32
+			
  33
+				if (isset($this->afterCall[$method])) {
  34
+					$this->afterCall[$method]->afterCall($this->proxied, $method, $args, $result);
  35
+				}
  36
+
  37
+				return $result;
  38
+			}
  39
+        }
  40
+	}
  41
+}
27  control/injector/BeforeCallAspect.php
... ...
@@ -0,0 +1,27 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * A BeforeCallAspect is run before a method is executed
  5
+ *
  6
+ * This is a declared interface, but isn't actually required
  7
+ * as PHP doesn't really care about types... 
  8
+ * 
  9
+ * @author Marcus Nyeholt <marcus@silverstripe.com.au>
  10
+ * @package sapphire
  11
+ * @subpackage injector
  12
+ * @license BSD http://silverstripe.org/BSD-license
  13
+ */
  14
+interface BeforeCallAspect {
  15
+	
  16
+	/**
  17
+	 * Call this aspect before a method is executed
  18
+	 * 
  19
+	 * @param object $proxied
  20
+	 *				The object having the method called upon it. 
  21
+	 * @param string $method
  22
+	 *				The name of the method being called
  23
+	 * @param string $args
  24
+	 *				The arguments that were passed to the method call
  25
+	 */
  26
+	public function beforeCall($proxied, $method, $args);
  27
+}
827  control/injector/Injector.php
... ...
@@ -0,0 +1,827 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * A simple injection manager that manages creating objects and injecting
  5
+ * dependencies between them. It borrows quite a lot from ideas taken from
  6
+ * Spring's configuration, but is adapted to the stateless PHP way of doing
  7
+ * things. 
  8
+ * 
  9
+ * In its simplest form, the dependency injector can be used as a mechanism to
  10
+ * instantiate objects. Simply call
  11
+ * 
  12
+ * Injector::inst()->get('ClassName')
  13
+ * 
  14
+ * and a new instance of ClassName will be created and returned to you. 
  15
+ * 
  16
+ * Classes can have specific configuration defined for them to 
  17
+ * indicate dependencies that should be injected. This takes the form of 
  18
+ * a static variable $dependencies defined in the class (or configuration),
  19
+ * which indicates the name of a property that should be set. 
  20
+ * 
  21
+ * eg 
  22
+ * 
  23
+ * <code>
  24
+ * class MyController extends Controller {
  25
+ * 
  26
+ *		public $permissions;
  27
+ *		public $defaultText;
  28
+ * 
  29
+ *		static $dependencies = array(
  30
+ *			'defaultText'		=> 'Override in configuration',
  31
+ *			'permissions'		=> '%$PermissionService',
  32
+ *		);
  33
+ * }
  34
+ * </code>
  35
+ * 
  36
+ * will result in an object of type MyController having the defaultText property
  37
+ * set to 'Override in configuration', and an object identified
  38
+ * as PermissionService set into the property called 'permissions'. The %$ 
  39
+ * syntax tells the injector to look the provided name up as an item to be created
  40
+ * by the Injector itself. 
  41
+ * 
  42
+ * A key concept of the injector is whether to manage the object as
  43
+ * 
  44
+ * * A pseudo-singleton, in that only one item will be created for a particular
  45
+ *   identifier (but the same class could be used for multiple identifiers)
  46
+ * * A prototype, where the same configuration is used, but a new object is
  47
+ *   created each time
  48
+ * * unmanaged, in which case a new object is created and injected, but no 
  49
+ *   information about its state is managed.
  50
+ * 
  51
+ * Additional configuration of items managed by the injector can be done by 
  52
+ * providing configuration for the types, either by manually loading in an 
  53
+ * array describing the configuration, or by specifying the configuration
  54
+ * for a type via SilverStripe's configuration mechanism. 
  55
+ *
  56
+ * Specify a configuration array of the format
  57
+ *
  58
+ * array(
  59
+ *		array(
  60
+ *			'id'			=> 'BeanId',					// the name to be used if diff from the filename
  61
+ *			'priority'		=> 1,							// priority. If another bean is defined with the same ID, 
  62
+ *															// but has a lower priority, it is NOT overridden
  63
+ *			'class'			=> 'ClassName',					// the name of the PHP class
  64
+ *			'src'			=> '/path/to/file'				// the location of the class
  65
+ *			'type'			=> 'singleton|prototype'		// if you want prototype object generation, set it as the type
  66
+ *															// By default, singleton is assumed
  67
+ *
  68
+ *			'construct'		=> array(						// properties to set at construction
  69
+ *				'scalar',									
  70
+ *				'%$BeanId',
  71
+ *			)
  72
+ *			'properties'	=> array(
  73
+ *				'name' => 'value'							// scalar value
  74
+ *				'name' => '%$BeanId',						// a reference to another bean
  75
+ *				'name' => array(
  76
+ *					'scalar',
  77
+ *					'%$BeanId'
  78
+ *				)
  79
+ *			)
  80
+ *		)
  81
+ *		// alternatively
  82
+ *		'MyBean'		=> array(
  83
+ *			'class'			=> 'ClassName',
  84
+ *		)
  85
+ *		// or simply
  86
+ *		'OtherBean'		=> 'SomeClass',
  87
+ * )
  88
+ *
  89
+ * In addition to specifying the bindings directly in the configuration,
  90
+ * you can simply create a publicly accessible property on the target
  91
+ * class which will automatically be injected if the autoScanProperties 
  92
+ * option is set to true. This means a class defined as
  93
+ * 
  94
+ * <code>
  95
+ * class MyController extends Controller {
  96
+ * 
  97
+ *		private $permissionService;
  98
+ * 
  99
+ *		public setPermissionService($p) {
  100
+ *			$this->permissionService = $p;
  101
+ *		} 
  102
+ * }
  103
+ * </code>
  104
+ * 
  105
+ * will have setPermissionService called if
  106
+ * 
  107
+ * * Injector::inst()->setAutoScanProperties(true) is called and
  108
+ * * A service named 'PermissionService' has been configured 
  109
+ * 
  110
+ * @author marcus@silverstripe.com.au
  111
+ * @package sapphire
  112
+ * @subpackage injector
  113
+ * @license BSD License http://silverstripe.org/bsd-license/
  114
+ */
  115
+class Injector {
  116
+
  117
+	/**
  118
+	 * Local store of all services
  119
+	 *
  120
+	 * @var array
  121
+	 */
  122
+	private $serviceCache;
  123
+
  124
+	/**
  125
+	 * Cache of items that need to be mapped for each service that gets injected
  126
+	 *
  127
+	 * @var array
  128
+	 */
  129
+	private $injectMap;
  130
+
  131
+	/**
  132
+	 * A store of all the service configurations that have been defined.
  133
+	 *
  134
+	 * @var array
  135
+	 */
  136
+	private $specs;
  137
+	
  138
+	/**
  139
+	 * A map of all the properties that should be automagically set on all 
  140
+	 * objects instantiated by the injector
  141
+	 */
  142
+	private $autoProperties;
  143
+
  144
+	/**
  145
+	 * A singleton if you want to use it that way
  146
+	 *
  147
+	 * @var Injector
  148
+	 */
  149
+	private static $instance;
  150
+	
  151
+	/**
  152
+	 * Indicates whether or not to automatically scan properties in injected objects to auto inject
  153
+	 * stuff, similar to the way grails does things. 
  154
+	 * 
  155
+	 * @var boolean
  156
+	 */
  157
+	private $autoScanProperties = false;
  158
+	
  159
+	/**
  160
+	 * The object used to create new class instances
  161
+	 * 
  162
+	 * Use a custom class here to change the way classes are created to use
  163
+	 * a custom creation method. By default the InjectionCreator class is used,
  164
+	 * which simply creates a new class via 'new', however this could be overridden
  165
+	 * to use, for example, SilverStripe's Object::create() method.
  166
+	 *
  167
+	 * @var InjectionCreator
  168
+	 */
  169
+	protected $objectCreator;
  170
+
  171
+	/**
  172
+	 * Create a new injector. 
  173
+	 *
  174
+	 * @param array $config
  175
+	 *				Service configuration
  176
+	 */
  177
+	public function __construct($config = null) {
  178
+		$this->injectMap = array();
  179
+		$this->serviceCache = array();
  180
+		$this->autoProperties = array();
  181
+		$this->specs = array();
  182
+
  183
+		$creatorClass = isset($config['creator']) ? $config['creator'] : 'InjectionCreator';
  184
+		$locatorClass = isset($config['locator']) ? $config['locator'] : 'ServiceConfigurationLocator';
  185
+		
  186
+		$this->objectCreator = new $creatorClass;
  187
+		$this->configLocator = new $locatorClass;
  188
+		
  189
+		if ($config) {
  190
+			$this->load($config);
  191
+		}
  192
+		
  193
+		self::$instance = $this;
  194
+	}
  195
+
  196
+	/**
  197
+	 * If a user wants to use the injector as a static reference
  198
+	 *
  199
+	 * @param array $config
  200
+	 * @return Injector
  201
+	 */
  202
+	public static function inst($config=null) {
  203
+		if (!self::$instance) {
  204
+			self::$instance = new Injector($config);
  205
+		}
  206
+		return self::$instance;
  207
+	}
  208
+	
  209
+	/**
  210
+	 * Indicate whether we auto scan injected objects for properties to set. 
  211
+	 *
  212
+	 * @param boolean $val
  213
+	 */
  214
+	public function setAutoScanProperties($val) {
  215
+		$this->autoScanProperties = $val;
  216
+	}
  217
+	
  218
+	/**
  219
+	 * Sets the object to use for creating new objects
  220
+	 *
  221
+	 * @param InjectionCreator $obj 
  222
+	 */
  223
+	public function setObjectCreator($obj) {
  224
+		$this->objectCreator = $obj;
  225
+	}
  226
+	
  227
+	/**
  228
+	 * Add in a specific mapping that should be catered for on a type. 
  229
+	 * This allows configuration of what should occur when an object
  230
+	 * of a particular type is injected, and what items should be injected
  231
+	 * for those properties / methods.
  232
+	 *
  233
+	 * @param type $class
  234
+	 *					The class to set a mapping for
  235
+	 * @param type $property
  236
+	 *					The property to set the mapping for
  237
+	 * @param type $injectType 
  238
+	 *					The registered type that will be injected
  239
+	 * @param string $injectVia
  240
+	 *					Whether to inject by setting a property or calling a setter
  241
+	 */
  242
+	public function setInjectMapping($class, $property, $toInject, $injectVia = 'property') {
  243
+		$mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
  244
+		
  245
+		$mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
  246
+		
  247
+		$this->injectMap[$class] = $mapping;
  248
+	}
  249
+	
  250
+	/**
  251
+	 * Add an object that should be automatically set on managed objects
  252
+	 *
  253
+	 * This allows you to specify, for example, that EVERY managed object
  254
+	 * will be automatically inject with a log object by the following
  255
+	 *
  256
+	 * $injector->addAutoProperty('log', new Logger());
  257
+	 *
  258
+	 * @param string $property
  259
+	 *				the name of the property
  260
+	 * @param object $object
  261
+	 *				the object to be set
  262
+	 */
  263
+	public function addAutoProperty($property, $object) {
  264
+		$this->autoProperties[$property] = $object;
  265
+		return $this;
  266
+	}
  267
+
  268
+	/**
  269
+	 * Load services using the passed in configuration for those services
  270
+	 *
  271
+	 * @param array $config
  272
+	 */
  273
+	public function load($config = array()) {
  274
+		$services = array();
  275
+
  276
+		foreach ($config as $specId => $spec) {
  277
+			if (is_string($spec)) {
  278
+				$spec = array('class' => $spec);
  279
+			}
  280
+
  281
+			$file = isset($spec['src']) ? $spec['src'] : null; 
  282
+			$name = null;
  283
+
  284
+			if (file_exists($file)) {
  285
+				$filename = basename($file);
  286
+				$name = substr($filename, 0, strrpos($filename, '.'));
  287
+			}
  288
+
  289
+			// class is whatever's explicitly set, 
  290
+			$class = isset($spec['class']) ? $spec['class'] : $name;
  291
+			
  292
+			// or the specid if nothing else available.
  293
+			if (!$class && is_string($specId)) {
  294
+				$class = $specId;
  295
+			}
  296
+			
  297
+			// make sure the class is set...
  298
+			$spec['class'] = $class;
  299
+
  300
+			$id = is_string($specId) ? $specId : (isset($spec['id']) ? $spec['id'] : $class); 
  301
+			
  302
+			$priority = isset($spec['priority']) ? $spec['priority'] : 1;
  303
+			
  304
+			// see if we already have this defined. If so, check priority weighting
  305
+			if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
  306
+				if ($this->specs[$id]['priority'] > $priority) {
  307
+					return;
  308
+				}
  309
+			}
  310
+
  311
+			// okay, actually include it now we know we're going to use it
  312
+			if (file_exists($file)) {
  313
+				require_once $file;
  314
+			}
  315
+
  316
+			// make sure to set the id for later when instantiating
  317
+			// to ensure we get cached
  318
+			$spec['id'] = $id;
  319
+
  320
+//			We've removed this check because new functionality means that the 'class' field doesn't need to refer
  321
+//			specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
  322
+//			functionality
  323
+//			
  324
+//			if (!class_exists($class)) {
  325
+//				throw new Exception("Failed to load '$class' from $file");
  326
+//			}
  327
+
  328
+			// store the specs for now - we lazy load on demand later on. 
  329
+			$this->specs[$id] = $spec;
  330
+
  331
+			// EXCEPT when there's already an existing instance at this id.
  332
+			// if so, we need to instantiate and replace immediately
  333
+			if (isset($this->serviceCache[$id])) {
  334
+				$this->instantiate($spec, $id);
  335
+			}
  336
+		}
  337
+
  338
+		return $this;
  339
+	}
  340
+	
  341
+	/**
  342
+	 * Update the configuration of an already defined service
  343
+	 * 
  344
+	 * Use this if you don't want to register a complete new config, just append
  345
+	 * to an existing configuration. Helpful to avoid overwriting someone else's changes
  346
+	 * 
  347
+	 * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
  348
+	 *
  349
+	 * @param string $id
  350
+	 *				The name of the service to update the definition for
  351
+	 * @param string $property
  352
+	 *				The name of the property to update. 
  353
+	 * @param mixed $value 
  354
+	 *				The value to set
  355
+	 * @param boolean $append
  356
+	 *				Whether to append (the default) when the property is an array
  357
+	 */
  358
+	public function updateSpec($id, $property, $value, $append = true) {
  359
+		if (isset($this->specs[$id]['properties'][$property])) {
  360
+			// by ref so we're updating the actual value
  361
+			$current = &$this->specs[$id]['properties'][$property];
  362
+			if (is_array($current) && $append) {
  363
+				$current[] = $value;
  364
+			} else {
  365
+				$this->specs[$id]['properties'][$property] = $value;
  366
+			}
  367
+			
  368
+			// and reload the object; existing bindings don't get
  369
+			// updated though! (for now...) 
  370
+			if (isset($this->serviceCache[$id])) {
  371
+				$this->instantiate($spec, $id);
  372
+			}
  373
+		}
  374
+	}
  375
+
  376
+	/**
  377
+	 * Recursively convert a value into its proper representation with service references
  378
+	 * resolved to actual objects
  379
+	 *
  380
+	 * @param string $value 
  381
+	 */
  382
+	public function convertServiceProperty($value) {
  383
+		if (is_array($value)) {
  384
+			$newVal = array();
  385
+			foreach ($value as $k => $v) {
  386
+				$newVal[$k] = $this->convertServiceProperty($v);
  387
+			}
  388
+			return $newVal;
  389
+		}
  390
+		
  391
+		if (is_string($value) && strpos($value, '%$') === 0) {
  392
+			$id = substr($value, 2);
  393
+			return $this->get($id);
  394
+		}
  395
+		return $value;
  396
+	}
  397
+
  398
+	/**
  399
+	 * Instantiate a managed object
  400
+	 *
  401
+	 * Given a specification of the form
  402
+	 *
  403
+	 * array(
  404
+	 *		'class' => 'ClassName',
  405
+	 *		'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
  406
+	 *		'id' => 'ServiceId',
  407
+	 *		'type' => 'singleton|prototype'
  408
+	 * )
  409
+	 *
  410
+	 * will create a new object, store it in the service registry, and
  411
+	 * set any relevant properties
  412
+	 *
  413
+	 * Optionally, you can pass a class name directly for creation
  414
+	 * 
  415
+	 * To access this from the outside, you should call ->get('Name') to ensure
  416
+	 * the appropriate checks are made on the specific type. 
  417
+	 * 
  418
+	 *
  419
+	 * @param array $spec
  420
+	 *				The specification of the class to instantiate
  421
+	 */
  422
+	protected function instantiate($spec, $id=null) {
  423
+		if (is_string($spec)) {
  424
+			$spec = array('class' => $spec);
  425
+		}
  426
+		$class = $spec['class'];
  427
+
  428
+		// create the object, using any constructor bindings
  429
+		$constructorParams = array();
  430
+		if (isset($spec['constructor']) && is_array($spec['constructor'])) {
  431
+			$constructorParams = $spec['constructor'];
  432
+		}
  433
+
  434
+		$object = $this->objectCreator->create($this, $class, $constructorParams);
  435
+		
  436
+		// figure out if we have a specific id set or not. In some cases, we might be instantiating objects
  437
+		// that we don't manage directly; we don't want to store these in the service cache below
  438
+		if (!$id) {
  439
+			$id = isset($spec['id']) ? $spec['id'] : null;
  440
+		}
  441
+
  442
+		// now set the service in place if needbe. This is NOT done for prototype beans, as they're
  443
+		// created anew each time
  444
+		$type = isset($spec['type']) ? $spec['type'] : null; 
  445
+		if ($id && (!$type || $type != 'prototype')) {
  446
+			// this ABSOLUTELY must be set before the object is injected.
  447
+			// This prevents circular reference errors down the line
  448
+			$this->serviceCache[$id] = $object;
  449
+		}
  450
+
  451
+		// now inject safely
  452
+		$this->inject($object, $id);
  453
+
  454
+		return $object;
  455
+	}
  456
+
  457
+	/**
  458
+	 * Inject $object with available objects from the service cache
  459
+	 * 
  460
+	 * @todo Track all the existing objects that have had a service bound
  461
+	 * into them, so we can update that binding at a later point if needbe (ie
  462
+	 * if the managed service changes)
  463
+	 *
  464
+	 * @param object $object
  465
+	 *				The object to inject
  466
+	 * @param string $asType
  467
+	 *				The ID this item was loaded as. This is so that the property configuration
  468
+	 *				for a type is referenced correctly in case $object is no longer the same
  469
+	 *				type as the loaded config specification had it as. 
  470
+	 */
  471
+	public function inject($object, $asType=null) {
  472
+		$objtype = $asType ? $asType : get_class($object);
  473
+		$mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
  474
+		
  475
+		// first off, set any properties defined in the service specification for this
  476
+		// object type
  477
+		if (isset($this->specs[$objtype]) && isset($this->specs[$objtype]['properties'])) {
  478
+			foreach ($this->specs[$objtype]['properties'] as $key => $value) {
  479
+				$val = $this->convertServiceProperty($value);
  480
+				$this->setObjectProperty($object, $key, $val);
  481
+			}
  482
+		}
  483
+
  484
+		// now, use any cached information about what properties this object type has
  485
+		// and set based on name resolution
  486
+		if (!$mapping) {
  487
+			if ($this->autoScanProperties) {
  488
+				// we use an object to prevent array copies if/when passed around
  489
+				$mapping = new ArrayObject();
  490
+
  491
+				// This performs public variable based injection
  492
+				$robj = new ReflectionObject($object);
  493
+				$properties = $robj->getProperties();
  494
+
  495
+				foreach ($properties as $propertyObject) {
  496
+					/* @var $propertyObject ReflectionProperty */
  497
+					if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
  498
+						$origName = $propertyObject->getName();
  499
+						$name = ucfirst($origName);
  500
+						if ($this->hasService($name)) {
  501
+							// Pull the name out of the registry
  502
+							$value = $this->get($name);
  503
+							$propertyObject->setValue($object, $value);
  504
+							$mapping[$origName] = array('name' => $name, 'type' => 'property');
  505
+						}
  506
+					}
  507
+				}
  508
+
  509
+				// and this performs setter based injection
  510
+				$methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
  511
+
  512
+				foreach ($methods as $methodObj) {
  513
+					/* @var $methodObj ReflectionMethod */
  514
+					$methName = $methodObj->getName();
  515
+					if (strpos($methName, 'set') === 0) {
  516
+						$pname = substr($methName, 3);
  517
+						if ($this->hasService($pname)) {
  518
+							// Pull the name out of the registry
  519
+							$value = $this->get($pname);
  520
+							$methodObj->invoke($object, $value);
  521
+							$mapping[$methName] = array('name' => $pname, 'type' => 'method');
  522
+						}
  523
+					}
  524
+				}
  525
+
  526
+				// we store the information about what needs to be injected for objects of this
  527
+				// type here
  528
+				$this->injectMap[get_class($object)] = $mapping;
  529
+			}
  530
+		} else {
  531
+			foreach ($mapping as $prop => $spec) {
  532
+				if ($spec['type'] == 'property') {
  533
+					$value = $this->get($spec['name']);
  534
+					$object->$prop = $value;
  535
+				} else {
  536
+					$method = $prop;
  537
+					$value = $this->get($spec['name']);
  538
+					$object->$method($value);
  539
+				}
  540
+			}
  541
+		}
  542
+
  543
+		$injections = Config::inst()->get(get_class($object), 'dependencies');
  544
+		// If the type defines some injections, set them here
  545
+		if ($injections && count($injections)) {
  546
+			foreach ($injections as $property => $value) {
  547
+				// we're checking isset in case it already has a property at this name
  548
+				// this doesn't catch privately set things, but they will only be set by a setter method, 
  549
+				// which should be responsible for preventing further setting if it doesn't want it. 
  550
+				if (!isset($object->$property)) {
  551
+					$value = $this->convertServiceProperty($value);
  552
+					$this->setObjectProperty($object, $property, $value);
  553
+				}
  554
+			}
  555
+		}
  556
+
  557
+		foreach ($this->autoProperties as $property => $value) {
  558
+			if (!isset($object->$property)) {
  559
+				$value = $this->convertServiceProperty($value);
  560
+				$this->setObjectProperty($object, $property, $value);
  561
+			}
  562
+		}
  563
+
  564
+		// Call the 'injected' method if it exists
  565
+		if (method_exists($object, 'injected')) {
  566
+			$object->injected();
  567
+		}
  568
+	}
  569
+
  570
+	/**
  571
+	 * Helper to set a property's value
  572
+	 *
  573
+	 * @param object $object
  574
+	 *					Set an object's property to a specific value
  575
+	 * @param string $name
  576
+	 *					The name of the property to set
  577
+	 * @param mixed $value 
  578
+	 *					The value to set
  579
+	 */
  580
+	protected function setObjectProperty($object, $name, $value) {
  581
+		if (method_exists($object, 'set'.$name)) {
  582
+			$object->{'set'.$name}($value);
  583
+		} else {
  584
+			$object->$name = $value;
  585
+		}
  586
+	}
  587
+
  588
+	/**
  589
+	 * Does the given service exist, and if so, what's the stored name for it?
  590
+	 * 
  591
+	 * We do a special check here for services that are using compound names. For example, 
  592
+	 * we might want to say that a property should be injected with Log.File or Log.Memory,
  593
+	 * but have only registered a 'Log' service, we'll instead return that. 
  594
+	 * 
  595
+	 * Will recursively call hasService for each depth of dotting
  596
+	 * 
  597
+	 * @return string 
  598
+	 *				The name of the service (as it might be different from the one passed in)
  599
+	 */
  600
+	public function hasService($name) {
  601
+		// common case, get it overwith first
  602
+		if (isset($this->specs[$name])) {
  603
+			return $name;
  604
+		}
  605
+		
  606
+		// okay, check whether we've got a compound name - don't worry about 0 index, cause that's an 
  607
+		// invalid name
  608
+		if (!strpos($name, '.')) {
  609
+			return null;
  610
+		}
  611
+		
  612
+		return $this->hasService(substr($name, 0, strrpos($name, '.')));
  613
+	}
  614
+
  615
+	/**
  616
+	 * Register a service object with an optional name to register it as the
  617
+	 * service for
  618
+	 * 
  619
+	 * @param stdClass $service
  620
+	 *					The object to register
  621
+	 * @param string $replace
  622
+	 *					The name of the object to replace (if different to the 
  623
+	 *					class name of the object to register)
  624
+	 * 
  625
+	 */
  626
+	public function registerService($service, $replace=null) {
  627
+		$registerAt = get_class($service);
  628
+		if ($replace != null) {
  629
+			$registerAt = $replace;
  630
+		}
  631
+		
  632
+		$this->serviceCache[$registerAt] = $service;
  633
+		$this->inject($service);
  634
+	}
  635
+	
  636
+	/**
  637
+	 * Register a service with an explicit name
  638
+	 */
  639
+	public function registerNamedService($name, $service) {
  640
+		$this->serviceCache[$name] = $service;
  641
+		$this->inject($service);
  642
+	}
  643
+	
  644
+	/**
  645
+	 * Get a named managed object
  646
+	 * 
  647
+	 * Will first check to see if the item has been registered as a configured service/bean
  648
+	 * and return that if so. 
  649
+	 * 
  650
+	 * Next, will check to see if there's any registered configuration for the given type
  651
+	 * and will then try and load that
  652
+	 * 
  653
+	 * Failing all of that, will just return a new instance of the 
  654
+	 * specificied object.
  655
+	 * 
  656
+	 * @param string $name 
  657
+	 *				the name of the service to retrieve. If not a registered 
  658
+	 *				service, then a class of the given name is instantiated
  659
+	 * @param boolean $asSingleton
  660
+	 *				Whether to register the created object as a singleton
  661
+	 *				if no other configuration is found
  662
+	 * @param array $constructorArgs
  663
+	 *				Optional set of arguments to pass as constructor arguments
  664
+	 *				if this object is to be created from scratch 
  665
+	 *				(ie asSingleton = false)
  666
+	 * 
  667
+	 */
  668
+	public function get($name, $asSingleton = true, $constructorArgs = null) {
  669
+		// reassign the name as it might actually be a compound name
  670
+		if ($serviceName = $this->hasService($name)) {
  671
+			// check to see what the type of bean is. If it's a prototype,
  672
+			// we don't want to return the singleton version of it.
  673
+			$spec = $this->specs[$serviceName];
  674
+			$type = isset($spec['type']) ? $spec['type'] : null;
  675
+
  676
+			// if we're a prototype OR we're not wanting a singleton
  677
+			if (($type && $type == 'prototype') || !$asSingleton) {
  678
+				if ($spec) {
  679
+					$spec['constructor'] = $constructorArgs;
  680
+				}
  681
+				return $this->instantiate($spec, $serviceName);
  682
+			} else {
  683
+				if (!isset($this->serviceCache[$serviceName])) {
  684
+					$this->instantiate($spec, $serviceName);
  685
+				}
  686
+				return $this->serviceCache[$serviceName];
  687
+			}
  688
+		}
  689
+		
  690
+		$config = $this->configLocator->locateConfigFor($name);
  691
+		if ($config) {
  692
+			$this->load(array($name => $config));
  693
+			if (isset($this->specs[$name])) {
  694
+				$spec = $this->specs[$name];
  695
+				return $this->instantiate($spec, $name);
  696
+			}
  697
+		}
  698
+
  699
+		// If we've got this far, we're dealing with a case of a user wanting 
  700
+		// to create an object based on its name. So, we need to fake its config
  701
+		// if the user wants it managed as a singleton service style object
  702
+		$spec = array('class' => $name, 'constructor' => $constructorArgs);
  703
+		if ($asSingleton) {
  704
+			// need to load the spec in; it'll be given the singleton type by default
  705
+			$this->load(array($name => $spec));
  706
+			return $this->instantiate($spec, $name);
  707
+		}
  708
+
  709
+		return $this->instantiate($spec);
  710
+	}
  711
+
  712
+	/**
  713
+	 * Similar to get() but always returns a new object of the given type
  714
+	 * 
  715
+	 * Additional parameters are passed through as 
  716
+	 * 
  717
+	 * @param type $name 
  718
+	 */
  719
+	public function create($name) {
  720
+		$constructorArgs = func_get_args();
  721
+		array_shift($constructorArgs);
  722
+		return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
  723
+	}
  724
+	
  725
+	/**
  726
+	 * Creates an object with the supplied argument array
  727
+	 *   
  728
+	 * @param string $name
  729
+	 *				Name of the class to create an object of
  730
+	 * @param array $args
  731
+	 *				Arguments to pass to the constructor
  732
+	 * @return mixed
  733
+	 */
  734
+	public function createWithArgs($name, $constructorArgs) {
  735
+		return $this->get($name, false, $constructorArgs);
  736
+	}
  737
+}
  738
+
  739
+/**
  740
+ * A class for creating new objects by the injector
  741
+ */
  742
+class InjectionCreator {
  743
+	/**
  744
+	 *
  745
+	 * @param string $object
  746
+	 *					A string representation of the class to create
  747
+	 * @param array $params
  748
+	 *					An array of parameters to be passed to the constructor
  749
+	 */
  750
+	public function create(Injector $injector, $class, $params = array()) {
  751
+		$reflector = new ReflectionClass($class);
  752
+		if (count($params)) {
  753
+			return $reflector->newInstanceArgs($injector->convertServiceProperty($params));
  754
+		}
  755
+		return $reflector->newInstance();
  756
+	}
  757
+}
  758
+
  759
+class SilverStripeInjectionCreator {
  760
+	/**
  761
+	 *
  762
+	 * @param string $object
  763
+	 *					A string representation of the class to create
  764
+	 * @param array $params
  765
+	 *					An array of parameters to be passed to the constructor
  766
+	 */
  767
+	public function create(Injector $injector, $class, $params = array()) {
  768
+		$class = Object::getCustomClass($class);
  769
+		$reflector = new ReflectionClass($class);
  770
+		return $reflector->newInstanceArgs($injector->convertServiceProperty($params));
  771
+	}
  772
+}
  773
+
  774
+/**
  775
+ * Used to locate configuration for a particular named service. 
  776
+ * 
  777
+ * If it isn't found, return null 
  778
+ */
  779
+class ServiceConfigurationLocator {
  780
+	public function locateConfigFor($name) {
  781
+		
  782
+	}
  783
+}
  784
+
  785
+/**
  786
+ * Use the SilverStripe configuration system to lookup config for a particular service
  787
+ */
  788
+class SilverStripeServiceConfigurationLocator {
  789
+	
  790
+	private $configs = array();
  791
+	
  792
+	public function locateConfigFor($name) {
  793
+		
  794
+		if (isset($this->configs[$name])) {
  795
+			return $this->configs[$name];
  796
+		}
  797
+		
  798
+		$config = Config::inst()->get('Injector', $name);
  799
+		if ($config) {
  800
+			$this->configs[$name] = $config;
  801
+			return $config;
  802
+		}
  803
+		
  804
+		// do parent lookup if it's a class
  805
+		if (class_exists($name)) {
  806
+			$parents = array_reverse(array_keys(ClassInfo::ancestry($name)));
  807
+			array_shift($parents);
  808
+			foreach ($parents as $parent) {
  809
+				// have we already got for this? 
  810
+				if (isset($this->configs[$parent])) {
  811
+					return $this->configs[$parent];
  812
+				}
  813
+				$config = Config::inst()->get('Injector', $parent);
  814
+				if ($config) {
  815
+					$this->configs[$name] = $config;
  816
+					return $config;
  817
+				} else {
  818
+					$this->configs[$parent] = false;
  819
+				}
  820
+			}
  821
+			
  822
+			// there is no parent config, so we'll record that as false so we don't do the expensive
  823
+			// lookup through parents again
  824
+			$this->configs[$name] = false;
  825
+		}
  826
+	}
  827
+}
4  core/Core.php
@@ -281,6 +281,10 @@
281 281
  */
282 282
 Debug::loadErrorHandlers();
283 283
 
  284
+// initialise the dependency injector
  285
+$default_options = array('locator' => 'SilverStripeServiceConfigurationLocator');
  286
+Injector::inst($default_options)->addAutoProperty('injector', Injector::inst()); 
  287
+
284 288
 ///////////////////////////////////////////////////////////////////////////////
285 289
 // HELPER FUNCTIONS
286 290
 
6  core/Object.php
@@ -104,11 +104,7 @@ public static function create() {
104 104
 		
105 105
 		$class = self::getCustomClass($class);
106 106
 		
107  
-		$reflector = new ReflectionClass($class);
108  
-		if($reflector->getConstructor()) {
109  
-			return $reflector->newInstanceArgs($args);
110  
-		}
111  
-		return new $class;
  107
+		return Injector::inst()->createWithArgs($class, $args);
112 108
 	}
113 109
 	
114 110
 	private static $_cache_inst_args = array();
20  docs/en/reference/director.md
Source Rendered
@@ -29,6 +29,26 @@ following two conditions are true:
29 29
 redirectBack().
30 30
 
31 31
 
  32
+## Request processing
  33
+
  34
+The `[api:Director]` is the entry point in Silverstring Framework for processing a request. You can read through
  35
+the execution steps in `[api:Director]``::direct()`, but in short
  36
+
  37
+* File uploads are first analysed to remove potentially harmful uploads (this will likely change!)
  38
+* The `[api:SS_HTTPRequest]` object is created
  39
+* The session object is created
  40
+* The `[api:Injector]` is first referenced, and asks the registered `[api:RequestProcessor]` to pre-process
  41
+  the request object. This allows for analysis of the current request, and allow filtering of parameters
  42
+  etc before any of the core of the application executes
  43
+* The request is handled and response checked
  44
+* The `[api:RequestProcessor]` is called to post-process the request to allow further filtering before
  45
+  content is sent to the end user. 
  46
+* The response is output
  47
+
  48
+The framework provides the ability to hook into the request both before and after it is handled to allow
  49
+developers to bind in their own custom pre- or post- request logic; see the `[api:RequestFilter]` to see how
  50
+this can be used to authenticate the request before the request is handled. 
  51
+
32 52
 ## Custom Rewrite Rules
33 53
 
34 54
 You can influence the way URLs are resolved one of 2 ways