FEATURE: Added dependency injector for managing creation of new objects 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.
This commit is contained in:
Marcus Nyeholt 2012-05-09 22:26:29 +10:00 committed by Sam Minnee
parent 3412a0e58d
commit b269badfbe
15 changed files with 1839 additions and 16 deletions

View File

@ -0,0 +1,6 @@
name: RequestProcessor
---
# Providing an empty config so it can be overridden at a later point
Injector:
RequestProcessor:
0:

View File

@ -97,8 +97,14 @@ class Director implements TemplateGlobalProvider {
if(!isset($_SESSION) && (isset($_COOKIE[session_name()]) || isset($_REQUEST[session_name()]))) Session::start();
// Initiate an empty session - doesn't initialize an actual PHP session until saved (see belwo)
$session = new Session(isset($_SESSION) ? $_SESSION : null);
$output = Injector::inst()->get('RequestProcessor')->preRequest($req, $session, $model);
// Main request handling
if ($output === false) {
// @TODO Need to NOT proceed with the request in an elegant manner
throw new SS_HTTPResponse_Exception(_t('Director.INVALID_REQUEST', 'Invalid request'), 400);
}
$result = Director::handleRequest($req, $session, $model);
// Save session data (and start/resume it if required)
@ -108,8 +114,10 @@ class Director implements TemplateGlobalProvider {
if(is_string($result) && substr($result,0,9) == 'redirect:') {
$response = new SS_HTTPResponse();
$response->redirect(substr($result, 9));
$response->output();
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
$response->output();
}
// Handle a controller
} else if($result) {
if($result instanceof SS_HTTPResponse) {
@ -120,15 +128,22 @@ class Director implements TemplateGlobalProvider {
$response->setBody($result);
}
// ?debug_memory=1 will output the number of bytes of memory used for this request
if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
Debug::message(sprintf(
"Peak memory usage in bytes: %s",
number_format(memory_get_peak_usage(),0)
));
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
// ?debug_memory=1 will output the number of bytes of memory used for this request
if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
Debug::message(sprintf(
"Peak memory usage in bytes: %s",
number_format(memory_get_peak_usage(),0)
));
} else {
$response->output();
}
} else {
$response->output();
// @TODO Proper response here.
throw new SS_HTTPResponse_Exception("Invalid response");
}
//$controllerObj->getSession()->inst_save();
}
@ -255,7 +270,7 @@ class Director implements TemplateGlobalProvider {
} else {
Director::$urlParams = $arguments;
$controllerObj = new $controller();
$controllerObj = Injector::inst()->create($controller);
$controllerObj->setSession($session);
try {

24
control/RequestFilter.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* A request filter is an object that's executed before and after a
* request occurs. By returning 'false' from the preRequest method,
* request execution will be stopped from continuing
*
* @author marcus@silverstripe.com.au
* @license BSD License http://silverstripe.org/bsd-license/
*/
interface RequestFilter {
/**
* Filter executed before a request processes
*
* @return boolean (optional)
* Whether to continue processing other filters
*/
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model);
/**
* Filter executed AFTER a request
*/
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model);
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Description of RequestProcessor
*
* @author marcus@silverstripe.com.au
* @license BSD License http://silverstripe.org/bsd-license/
*/
class RequestProcessor {
private $filters = array();
public function __construct($filters = array()) {
$this->filters = $filters;
}
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
foreach ($this->filters as $filter) {
$res = $filter->preRequest($request, $session, $model);
if ($res === false) {
return false;
}
}
}
/**
* Filter executed AFTER a request
*/
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
foreach ($this->filters as $filter) {
$res = $filter->postRequest($request, $response, $model);
if ($res === false) {
return false;
}
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* An AfterCallAspect is run after a method is executed
*
* This is a declared interface, but isn't actually required
* as PHP doesn't really care about types...
*
* @author Marcus Nyeholt <marcus@silverstripe.com.au>
* @package sapphire
* @subpackage injector
* @license BSD http://silverstripe.org/BSD-license
*/
interface AfterCallAspect {
/**
* Call this aspect after a method is executed
*
* @param object $proxied
* The object having the method called upon it.
* @param string $method
* The name of the method being called
* @param string $args
* The arguments that were passed to the method call
*/
public function afterCall($proxied, $method, $args);
}

View File

@ -0,0 +1,41 @@
<?php
/**
* A class that proxies another, allowing various functionality to be
* injected
*
* @author marcus@silverstripe.com.au
* @package sapphire
* @subpackage injector
*
* @license http://silverstripe.org/bsd-license/
*/
class AopProxyService {
public $beforeCall = array();
public $afterCall = array();
public $proxied;
public function __call($method, $args) {
if (method_exists($this->proxied, $method)) {
$continue = true;
if (isset($this->beforeCall[$method])) {
$result = $this->beforeCall[$method]->beforeCall($this->proxied, $method, $args);
if ($result === false) {
$continue = false;
}
}
if ($continue) {
$result = call_user_func_array(array($this->proxied, $method), $args);
if (isset($this->afterCall[$method])) {
$this->afterCall[$method]->afterCall($this->proxied, $method, $args, $result);
}
return $result;
}
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* A BeforeCallAspect is run before a method is executed
*
* This is a declared interface, but isn't actually required
* as PHP doesn't really care about types...
*
* @author Marcus Nyeholt <marcus@silverstripe.com.au>
* @package sapphire
* @subpackage injector
* @license BSD http://silverstripe.org/BSD-license
*/
interface BeforeCallAspect {
/**
* Call this aspect before a method is executed
*
* @param object $proxied
* The object having the method called upon it.
* @param string $method
* The name of the method being called
* @param string $args
* The arguments that were passed to the method call
*/
public function beforeCall($proxied, $method, $args);
}

View File

@ -0,0 +1,827 @@
<?php
/**
* A simple injection manager that manages creating objects and injecting
* dependencies between them. It borrows quite a lot from ideas taken from
* Spring's configuration, but is adapted to the stateless PHP way of doing
* things.
*
* In its simplest form, the dependency injector can be used as a mechanism to
* instantiate objects. Simply call
*
* Injector::inst()->get('ClassName')
*
* and a new instance of ClassName will be created and returned to you.
*
* Classes can have specific configuration defined for them to
* indicate dependencies that should be injected. This takes the form of
* a static variable $dependencies defined in the class (or configuration),
* which indicates the name of a property that should be set.
*
* eg
*
* <code>
* class MyController extends Controller {
*
* public $permissions;
* public $defaultText;
*
* static $dependencies = array(
* 'defaultText' => 'Override in configuration',
* 'permissions' => '%$PermissionService',
* );
* }
* </code>
*
* will result in an object of type MyController having the defaultText property
* set to 'Override in configuration', and an object identified
* as PermissionService set into the property called 'permissions'. The %$
* syntax tells the injector to look the provided name up as an item to be created
* by the Injector itself.
*
* A key concept of the injector is whether to manage the object as
*
* * A pseudo-singleton, in that only one item will be created for a particular
* identifier (but the same class could be used for multiple identifiers)
* * A prototype, where the same configuration is used, but a new object is
* created each time
* * unmanaged, in which case a new object is created and injected, but no
* information about its state is managed.
*
* Additional configuration of items managed by the injector can be done by
* providing configuration for the types, either by manually loading in an
* array describing the configuration, or by specifying the configuration
* for a type via SilverStripe's configuration mechanism.
*
* Specify a configuration array of the format
*
* array(
* array(
* 'id' => 'BeanId', // the name to be used if diff from the filename
* 'priority' => 1, // priority. If another bean is defined with the same ID,
* // but has a lower priority, it is NOT overridden
* 'class' => 'ClassName', // the name of the PHP class
* 'src' => '/path/to/file' // the location of the class
* 'type' => 'singleton|prototype' // if you want prototype object generation, set it as the type
* // By default, singleton is assumed
*
* 'construct' => array( // properties to set at construction
* 'scalar',
* '%$BeanId',
* )
* 'properties' => array(
* 'name' => 'value' // scalar value
* 'name' => '%$BeanId', // a reference to another bean
* 'name' => array(
* 'scalar',
* '%$BeanId'
* )
* )
* )
* // alternatively
* 'MyBean' => array(
* 'class' => 'ClassName',
* )
* // or simply
* 'OtherBean' => 'SomeClass',
* )
*
* In addition to specifying the bindings directly in the configuration,
* you can simply create a publicly accessible property on the target
* class which will automatically be injected if the autoScanProperties
* option is set to true. This means a class defined as
*
* <code>
* class MyController extends Controller {
*
* private $permissionService;
*
* public setPermissionService($p) {
* $this->permissionService = $p;
* }
* }
* </code>
*
* will have setPermissionService called if
*
* * Injector::inst()->setAutoScanProperties(true) is called and
* * A service named 'PermissionService' has been configured
*
* @author marcus@silverstripe.com.au
* @package sapphire
* @subpackage injector
* @license BSD License http://silverstripe.org/bsd-license/
*/
class Injector {
/**
* Local store of all services
*
* @var array
*/
private $serviceCache;
/**
* Cache of items that need to be mapped for each service that gets injected
*
* @var array
*/
private $injectMap;
/**
* A store of all the service configurations that have been defined.
*
* @var array
*/
private $specs;
/**
* A map of all the properties that should be automagically set on all
* objects instantiated by the injector
*/
private $autoProperties;
/**
* A singleton if you want to use it that way
*
* @var Injector
*/
private static $instance;
/**
* Indicates whether or not to automatically scan properties in injected objects to auto inject
* stuff, similar to the way grails does things.
*
* @var boolean
*/
private $autoScanProperties = false;
/**
* The object used to create new class instances
*
* Use a custom class here to change the way classes are created to use
* a custom creation method. By default the InjectionCreator class is used,
* which simply creates a new class via 'new', however this could be overridden
* to use, for example, SilverStripe's Object::create() method.
*
* @var InjectionCreator
*/
protected $objectCreator;
/**
* Create a new injector.
*
* @param array $config
* Service configuration
*/
public function __construct($config = null) {
$this->injectMap = array();
$this->serviceCache = array();
$this->autoProperties = array();
$this->specs = array();
$creatorClass = isset($config['creator']) ? $config['creator'] : 'InjectionCreator';
$locatorClass = isset($config['locator']) ? $config['locator'] : 'ServiceConfigurationLocator';
$this->objectCreator = new $creatorClass;
$this->configLocator = new $locatorClass;
if ($config) {
$this->load($config);
}
self::$instance = $this;
}
/**
* If a user wants to use the injector as a static reference
*
* @param array $config
* @return Injector
*/
public static function inst($config=null) {
if (!self::$instance) {
self::$instance = new Injector($config);
}
return self::$instance;
}
/**
* Indicate whether we auto scan injected objects for properties to set.
*
* @param boolean $val
*/
public function setAutoScanProperties($val) {
$this->autoScanProperties = $val;
}
/**
* Sets the object to use for creating new objects
*
* @param InjectionCreator $obj
*/
public function setObjectCreator($obj) {
$this->objectCreator = $obj;
}
/**
* Add in a specific mapping that should be catered for on a type.
* This allows configuration of what should occur when an object
* of a particular type is injected, and what items should be injected
* for those properties / methods.
*
* @param type $class
* The class to set a mapping for
* @param type $property
* The property to set the mapping for
* @param type $injectType
* The registered type that will be injected
* @param string $injectVia
* Whether to inject by setting a property or calling a setter
*/
public function setInjectMapping($class, $property, $toInject, $injectVia = 'property') {
$mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
$mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
$this->injectMap[$class] = $mapping;
}
/**
* Add an object that should be automatically set on managed objects
*
* This allows you to specify, for example, that EVERY managed object
* will be automatically inject with a log object by the following
*
* $injector->addAutoProperty('log', new Logger());
*
* @param string $property
* the name of the property
* @param object $object
* the object to be set
*/
public function addAutoProperty($property, $object) {
$this->autoProperties[$property] = $object;
return $this;
}
/**
* Load services using the passed in configuration for those services
*
* @param array $config
*/
public function load($config = array()) {
$services = array();
foreach ($config as $specId => $spec) {
if (is_string($spec)) {
$spec = array('class' => $spec);
}
$file = isset($spec['src']) ? $spec['src'] : null;
$name = null;
if (file_exists($file)) {
$filename = basename($file);
$name = substr($filename, 0, strrpos($filename, '.'));
}
// class is whatever's explicitly set,
$class = isset($spec['class']) ? $spec['class'] : $name;
// or the specid if nothing else available.
if (!$class && is_string($specId)) {
$class = $specId;
}
// make sure the class is set...
$spec['class'] = $class;
$id = is_string($specId) ? $specId : (isset($spec['id']) ? $spec['id'] : $class);
$priority = isset($spec['priority']) ? $spec['priority'] : 1;
// see if we already have this defined. If so, check priority weighting
if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
if ($this->specs[$id]['priority'] > $priority) {
return;
}
}
// okay, actually include it now we know we're going to use it
if (file_exists($file)) {
require_once $file;
}
// make sure to set the id for later when instantiating
// to ensure we get cached
$spec['id'] = $id;
// We've removed this check because new functionality means that the 'class' field doesn't need to refer
// specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
// functionality
//
// if (!class_exists($class)) {
// throw new Exception("Failed to load '$class' from $file");
// }
// store the specs for now - we lazy load on demand later on.
$this->specs[$id] = $spec;
// EXCEPT when there's already an existing instance at this id.
// if so, we need to instantiate and replace immediately
if (isset($this->serviceCache[$id])) {
$this->instantiate($spec, $id);
}
}
return $this;
}
/**
* Update the configuration of an already defined service
*
* Use this if you don't want to register a complete new config, just append
* to an existing configuration. Helpful to avoid overwriting someone else's changes
*
* updateSpec('RequestProcessor', 'filters', '%$MyFilter')
*
* @param string $id
* The name of the service to update the definition for
* @param string $property
* The name of the property to update.
* @param mixed $value
* The value to set
* @param boolean $append
* Whether to append (the default) when the property is an array
*/
public function updateSpec($id, $property, $value, $append = true) {
if (isset($this->specs[$id]['properties'][$property])) {
// by ref so we're updating the actual value
$current = &$this->specs[$id]['properties'][$property];
if (is_array($current) && $append) {
$current[] = $value;
} else {
$this->specs[$id]['properties'][$property] = $value;
}
// and reload the object; existing bindings don't get
// updated though! (for now...)
if (isset($this->serviceCache[$id])) {
$this->instantiate($spec, $id);
}
}
}
/**
* Recursively convert a value into its proper representation with service references
* resolved to actual objects
*
* @param string $value
*/
public function convertServiceProperty($value) {
if (is_array($value)) {
$newVal = array();
foreach ($value as $k => $v) {
$newVal[$k] = $this->convertServiceProperty($v);
}
return $newVal;
}
if (is_string($value) && strpos($value, '%$') === 0) {
$id = substr($value, 2);
return $this->get($id);
}
return $value;
}
/**
* Instantiate a managed object
*
* Given a specification of the form
*
* array(
* 'class' => 'ClassName',
* 'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
* 'id' => 'ServiceId',
* 'type' => 'singleton|prototype'
* )
*
* will create a new object, store it in the service registry, and
* set any relevant properties
*
* Optionally, you can pass a class name directly for creation
*
* To access this from the outside, you should call ->get('Name') to ensure
* the appropriate checks are made on the specific type.
*
*
* @param array $spec
* The specification of the class to instantiate
*/
protected function instantiate($spec, $id=null) {
if (is_string($spec)) {
$spec = array('class' => $spec);
}
$class = $spec['class'];
// create the object, using any constructor bindings
$constructorParams = array();
if (isset($spec['constructor']) && is_array($spec['constructor'])) {
$constructorParams = $spec['constructor'];
}
$object = $this->objectCreator->create($this, $class, $constructorParams);
// figure out if we have a specific id set or not. In some cases, we might be instantiating objects
// that we don't manage directly; we don't want to store these in the service cache below
if (!$id) {
$id = isset($spec['id']) ? $spec['id'] : null;
}
// now set the service in place if needbe. This is NOT done for prototype beans, as they're
// created anew each time
$type = isset($spec['type']) ? $spec['type'] : null;
if ($id && (!$type || $type != 'prototype')) {
// this ABSOLUTELY must be set before the object is injected.
// This prevents circular reference errors down the line
$this->serviceCache[$id] = $object;
}
// now inject safely
$this->inject($object, $id);
return $object;
}
/**
* Inject $object with available objects from the service cache
*
* @todo Track all the existing objects that have had a service bound
* into them, so we can update that binding at a later point if needbe (ie
* if the managed service changes)
*
* @param object $object
* The object to inject
* @param string $asType
* The ID this item was loaded as. This is so that the property configuration
* for a type is referenced correctly in case $object is no longer the same
* type as the loaded config specification had it as.
*/
public function inject($object, $asType=null) {
$objtype = $asType ? $asType : get_class($object);
$mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
// first off, set any properties defined in the service specification for this
// object type
if (isset($this->specs[$objtype]) && isset($this->specs[$objtype]['properties'])) {
foreach ($this->specs[$objtype]['properties'] as $key => $value) {
$val = $this->convertServiceProperty($value);
$this->setObjectProperty($object, $key, $val);
}
}
// now, use any cached information about what properties this object type has
// and set based on name resolution
if (!$mapping) {
if ($this->autoScanProperties) {
// we use an object to prevent array copies if/when passed around
$mapping = new ArrayObject();
// This performs public variable based injection
$robj = new ReflectionObject($object);
$properties = $robj->getProperties();
foreach ($properties as $propertyObject) {
/* @var $propertyObject ReflectionProperty */
if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
$origName = $propertyObject->getName();
$name = ucfirst($origName);
if ($this->hasService($name)) {
// Pull the name out of the registry
$value = $this->get($name);
$propertyObject->setValue($object, $value);
$mapping[$origName] = array('name' => $name, 'type' => 'property');
}
}
}
// and this performs setter based injection
$methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $methodObj) {
/* @var $methodObj ReflectionMethod */
$methName = $methodObj->getName();
if (strpos($methName, 'set') === 0) {
$pname = substr($methName, 3);
if ($this->hasService($pname)) {
// Pull the name out of the registry
$value = $this->get($pname);
$methodObj->invoke($object, $value);
$mapping[$methName] = array('name' => $pname, 'type' => 'method');
}
}
}
// we store the information about what needs to be injected for objects of this
// type here
$this->injectMap[get_class($object)] = $mapping;
}
} else {
foreach ($mapping as $prop => $spec) {
if ($spec['type'] == 'property') {
$value = $this->get($spec['name']);
$object->$prop = $value;
} else {
$method = $prop;
$value = $this->get($spec['name']);
$object->$method($value);
}
}
}
$injections = Config::inst()->get(get_class($object), 'dependencies');
// If the type defines some injections, set them here
if ($injections && count($injections)) {
foreach ($injections as $property => $value) {
// we're checking isset in case it already has a property at this name
// this doesn't catch privately set things, but they will only be set by a setter method,
// which should be responsible for preventing further setting if it doesn't want it.
if (!isset($object->$property)) {
$value = $this->convertServiceProperty($value);
$this->setObjectProperty($object, $property, $value);
}
}
}
foreach ($this->autoProperties as $property => $value) {
if (!isset($object->$property)) {
$value = $this->convertServiceProperty($value);
$this->setObjectProperty($object, $property, $value);
}
}
// Call the 'injected' method if it exists
if (method_exists($object, 'injected')) {
$object->injected();
}
}
/**
* Helper to set a property's value
*
* @param object $object
* Set an object's property to a specific value
* @param string $name
* The name of the property to set
* @param mixed $value
* The value to set
*/
protected function setObjectProperty($object, $name, $value) {
if (method_exists($object, 'set'.$name)) {
$object->{'set'.$name}($value);
} else {
$object->$name = $value;
}
}
/**
* Does the given service exist, and if so, what's the stored name for it?
*
* We do a special check here for services that are using compound names. For example,
* we might want to say that a property should be injected with Log.File or Log.Memory,
* but have only registered a 'Log' service, we'll instead return that.
*
* Will recursively call hasService for each depth of dotting
*
* @return string
* The name of the service (as it might be different from the one passed in)
*/
public function hasService($name) {
// common case, get it overwith first
if (isset($this->specs[$name])) {
return $name;
}
// okay, check whether we've got a compound name - don't worry about 0 index, cause that's an
// invalid name
if (!strpos($name, '.')) {
return null;
}
return $this->hasService(substr($name, 0, strrpos($name, '.')));
}
/**
* Register a service object with an optional name to register it as the
* service for
*
* @param stdClass $service
* The object to register
* @param string $replace
* The name of the object to replace (if different to the
* class name of the object to register)
*
*/
public function registerService($service, $replace=null) {
$registerAt = get_class($service);
if ($replace != null) {
$registerAt = $replace;
}
$this->serviceCache[$registerAt] = $service;
$this->inject($service);
}
/**
* Register a service with an explicit name
*/
public function registerNamedService($name, $service) {
$this->serviceCache[$name] = $service;
$this->inject($service);
}
/**
* Get a named managed object
*
* Will first check to see if the item has been registered as a configured service/bean
* and return that if so.
*
* Next, will check to see if there's any registered configuration for the given type
* and will then try and load that
*
* Failing all of that, will just return a new instance of the
* specificied object.
*
* @param string $name
* the name of the service to retrieve. If not a registered
* service, then a class of the given name is instantiated
* @param boolean $asSingleton
* Whether to register the created object as a singleton
* if no other configuration is found
* @param array $constructorArgs
* Optional set of arguments to pass as constructor arguments
* if this object is to be created from scratch
* (ie asSingleton = false)
*
*/
public function get($name, $asSingleton = true, $constructorArgs = null) {
// reassign the name as it might actually be a compound name
if ($serviceName = $this->hasService($name)) {
// check to see what the type of bean is. If it's a prototype,
// we don't want to return the singleton version of it.
$spec = $this->specs[$serviceName];
$type = isset($spec['type']) ? $spec['type'] : null;
// if we're a prototype OR we're not wanting a singleton
if (($type && $type == 'prototype') || !$asSingleton) {
if ($spec) {
$spec['constructor'] = $constructorArgs;
}
return $this->instantiate($spec, $serviceName);
} else {
if (!isset($this->serviceCache[$serviceName])) {
$this->instantiate($spec, $serviceName);
}
return $this->serviceCache[$serviceName];
}
}
$config = $this->configLocator->locateConfigFor($name);
if ($config) {
$this->load(array($name => $config));
if (isset($this->specs[$name])) {
$spec = $this->specs[$name];
return $this->instantiate($spec, $name);
}
}
// If we've got this far, we're dealing with a case of a user wanting
// to create an object based on its name. So, we need to fake its config
// if the user wants it managed as a singleton service style object
$spec = array('class' => $name, 'constructor' => $constructorArgs);
if ($asSingleton) {
// need to load the spec in; it'll be given the singleton type by default
$this->load(array($name => $spec));
return $this->instantiate($spec, $name);
}
return $this->instantiate($spec);
}
/**
* Similar to get() but always returns a new object of the given type
*
* Additional parameters are passed through as
*
* @param type $name
*/
public function create($name) {
$constructorArgs = func_get_args();
array_shift($constructorArgs);
return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
}
/**
* Creates an object with the supplied argument array
*
* @param string $name
* Name of the class to create an object of
* @param array $args
* Arguments to pass to the constructor
* @return mixed
*/
public function createWithArgs($name, $constructorArgs) {
return $this->get($name, false, $constructorArgs);
}
}
/**
* A class for creating new objects by the injector
*/
class InjectionCreator {
/**
*
* @param string $object
* A string representation of the class to create
* @param array $params
* An array of parameters to be passed to the constructor
*/
public function create(Injector $injector, $class, $params = array()) {
$reflector = new ReflectionClass($class);
if (count($params)) {
return $reflector->newInstanceArgs($injector->convertServiceProperty($params));
}
return $reflector->newInstance();
}
}
class SilverStripeInjectionCreator {
/**
*
* @param string $object
* A string representation of the class to create
* @param array $params
* An array of parameters to be passed to the constructor
*/
public function create(Injector $injector, $class, $params = array()) {
$class = Object::getCustomClass($class);
$reflector = new ReflectionClass($class);
return $reflector->newInstanceArgs($injector->convertServiceProperty($params));
}
}
/**
* Used to locate configuration for a particular named service.
*
* If it isn't found, return null
*/
class ServiceConfigurationLocator {
public function locateConfigFor($name) {
}
}
/**
* Use the SilverStripe configuration system to lookup config for a particular service
*/
class SilverStripeServiceConfigurationLocator {
private $configs = array();
public function locateConfigFor($name) {
if (isset($this->configs[$name])) {
return $this->configs[$name];
}
$config = Config::inst()->get('Injector', $name);
if ($config) {
$this->configs[$name] = $config;
return $config;
}
// do parent lookup if it's a class
if (class_exists($name)) {
$parents = array_reverse(array_keys(ClassInfo::ancestry($name)));
array_shift($parents);
foreach ($parents as $parent) {
// have we already got for this?
if (isset($this->configs[$parent])) {
return $this->configs[$parent];
}
$config = Config::inst()->get('Injector', $parent);
if ($config) {
$this->configs[$name] = $config;
return $config;
} else {
$this->configs[$parent] = false;
}
}
// there is no parent config, so we'll record that as false so we don't do the expensive
// lookup through parents again
$this->configs[$name] = false;
}
}
}

View File

@ -281,6 +281,10 @@ if(Director::isLive()) {
*/
Debug::loadErrorHandlers();
// initialise the dependency injector
$default_options = array('locator' => 'SilverStripeServiceConfigurationLocator');
Injector::inst($default_options)->addAutoProperty('injector', Injector::inst());
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS

View File

@ -104,11 +104,7 @@ abstract class Object {
$class = self::getCustomClass($class);
$reflector = new ReflectionClass($class);
if($reflector->getConstructor()) {
return $reflector->newInstanceArgs($args);
}
return new $class;
return Injector::inst()->createWithArgs($class, $args);
}
private static $_cache_inst_args = array();

View File

@ -29,6 +29,26 @@ following two conditions are true:
redirectBack().
## Request processing
The `[api:Director]` is the entry point in Silverstring Framework for processing a request. You can read through
the execution steps in `[api:Director]``::direct()`, but in short
* File uploads are first analysed to remove potentially harmful uploads (this will likely change!)
* The `[api:SS_HTTPRequest]` object is created
* The session object is created
* The `[api:Injector]` is first referenced, and asks the registered `[api:RequestProcessor]` to pre-process
the request object. This allows for analysis of the current request, and allow filtering of parameters
etc before any of the core of the application executes
* The request is handled and response checked
* The `[api:RequestProcessor]` is called to post-process the request to allow further filtering before
content is sent to the end user.
* The response is output
The framework provides the ability to hook into the request both before and after it is handled to allow
developers to bind in their own custom pre- or post- request logic; see the `[api:RequestFilter]` to see how
this can be used to authenticate the request before the request is handled.
## Custom Rewrite Rules
You can influence the way URLs are resolved one of 2 ways

View File

@ -0,0 +1,184 @@
# Injector
## Introduction
The `[api:Injector]` class is the central manager of inter-class dependencies
in the SilverStripe Framework. In its simplest form it can be considered as
a replacement for Object::create and singleton() calls, but also offers
developers the ability to declare the dependencies a class type has, or
to change the nature of the dependencies defined by other developers.
Some of the goals of dependency injection are
* Simplified instantiation of objects
* Providing a uniform way of declaring and managing inter-object dependencies
* Making class dependencies configurable
* Simplifying the process of overriding or replacing core behaviour
* Improve testability of code
* Promoting abstraction of logic
A key concept of the injector is whether the object should be managed as
* A pseudo-singleton, in that only one item will be created for a particular
identifier (but the same class could be used for multiple identifiers)
* A prototype, where the same configuration is used, but a new object is
created each time
* unmanaged, in which case a new object is created and injected, but no
information about its state is managed.
These concepts will be discussed further below
## Some simple examples
The following sums up the simplest usage of the injector
Assuming no other configuration is specified
:::php
$object = Injector::inst()->create('ClassName');
Creates a new object of type ClassName
:::php
$object = Injector::inst()->create('ClassName');
$object2 = Injector::inst()->create('ClassName');
$object !== $object2;
Repeated calls to create() create a new class each time. To create a singleton
object instead, use **get()**
:::php
// sets up ClassName as a singleton
$object = Injector::inst()->get('ClassName');
$object2 = Injector::inst()->get('ClassName');
$object === $object2;
The subsequent call returns the SAME object as the first call.
:::php
class MyController extends Controller {
// both of these properties will be automatically
// set by the injector on object creation
public $permissions;
public $textProperty;
static $dependencies = array(
'textProperty' => 'a string value',
'permissions' => '%$PermissionService',
);
}
$object = Injector::inst()->get('MyController');
// results in
$object->permissions instanceof PermissionService;
$object->textProperty == 'a string value';
In this case, on creation of the MyController object, the injector will
automatically instantiate the PermissionService object and set it as
the **permissions** property.
## Configuring objects managed by the dependency injector
The above declarative style of dependency management would cover a large
portion of usecases, but more complex dependency structures can be defined
via configuration files.
Configuration can be specified for two areas of dependency management
* Defining dependency overrides for individual classes
* Injector managed 'services'
### Dependency overrides
To override the **static $dependency;** declaration for a class, you could
define the following configuration file (module/_config/MyController.yml)
name: MyController
---
MyController:
dependencies:
textProperty: a string value
permissions: %$PermissionService
At runtime, the **dependencies** configuration would be read and used in
place of that declared on the object.
### Managed objects
Simple dependencies can be specified by the **dependencies**, but more complex
configurations are possible by specifying constructor arguments, or by
specifying more complex properties such as lists.
These more complex configurations are defined in 'Injector' configuration
blocks and are read by the injector at runtime
Assuming a class structure such as
:::php
class RestrictivePermissionService {
private $database;
public function setDatabase($d) {
$this->database = $d;
}
}
class MySQLDatabase {
private $username;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
}
and the following configuration
name: MyController
---
MyController:
dependencies:
permissions: %$PermissionService
Injector:
PermissionService:
class: RestrictivePermissionService
properties:
database: %$MySQLDatabase
MySQLDatabase
constructor:
0: 'dbusername'
1: 'dbpassword'
calling
:::php
// sets up ClassName as a singleton
$controller = Injector::inst()->get('MyController');
would
* Create an object of type MyController
* Look through the **dependencies** and call get('PermissionService')
* Load the configuration for PermissionService, and create an object of
type RestrictivePermissionService
* Look at the properties to be injected and look for the config for
MySQLDatabase
* Create a MySQLDatabase class, passing dbusername and dbpassword as the
parameters to the constructor
### What are Services?
Without diving too deep down the rabbit hole, the term 'Service' is commonly
used to describe a piece of code that acts as an interface between the
controller layer and model layer of an MVC architecture. Rather than having
a controller action directly operate on data objects, a service layer provides
that logic abstraction, stopping controllers from implementing business logic,
and keeping that logic packaged in a way that is easily reused from other
classes.
By default, objects are managed like a singleton, in that there is only one
object instance used for a named service, and all references to that service
are returned the same object.

View File

@ -0,0 +1,597 @@
<?php
define('TEST_SERVICES', dirname(__FILE__) . '/testservices');
/**
* Tests for the dependency injector
*
* Note that these are SS conversions of the existing Simpletest unit tests
*
* @author marcus@silverstripe.com.au
* @license BSD License http://silverstripe.org/bsd-license/
*/
class InjectorTest extends SapphireTest {
public function testBasicInjector() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$config = array(array('src' => TEST_SERVICES . '/SampleService.php',));
$injector->load($config);
$this->assertTrue($injector->hasService('SampleService') == 'SampleService');
$myObject = new TestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->sampleService), 'SampleService');
}
public function testConfiguredInjector() {
$injector = new Injector();
$services = array(
array(
'src' => TEST_SERVICES . '/AnotherService.php',
'properties' => array('config_property' => 'Value'),
),
array(
'src' => TEST_SERVICES . '/SampleService.php',
)
);
$injector->load($services);
$this->assertTrue($injector->hasService('SampleService') == 'SampleService');
// We expect a false because the 'AnotherService' is actually
// just a replacement of the SampleService
$this->assertTrue($injector->hasService('AnotherService') == 'AnotherService');
$item = $injector->get('AnotherService');
$this->assertEquals('Value', $item->config_property);
}
public function testIdToNameMap() {
$injector = new Injector();
$services = array(
'FirstId' => 'AnotherService',
'SecondId' => 'SampleService',
);
$injector->load($services);
$this->assertTrue($injector->hasService('FirstId') == 'FirstId');
$this->assertTrue($injector->hasService('SecondId') == 'SecondId');
$this->assertTrue($injector->get('FirstId') instanceof AnotherService);
$this->assertTrue($injector->get('SecondId') instanceof SampleService);
}
public function testReplaceService() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$config = array(array('src' => TEST_SERVICES . '/SampleService.php'));
// load
$injector->load($config);
// inject
$myObject = new TestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->sampleService), 'SampleService');
// also tests that ID can be the key in the array
$config = array('SampleService' => array('src' => TEST_SERVICES . '/AnotherService.php')); // , 'id' => 'SampleService'));
// load
$injector->load($config);
$injector->inject($myObject);
$this->assertEquals('AnotherService', get_class($myObject->sampleService));
}
public function testUpdateSpec() {
$injector = new Injector();
$services = array(
'AnotherService' => array(
'src' => TEST_SERVICES . '/AnotherService.php',
'properties' => array(
'filters' => array(
'One',
'Two',
)
),
)
);
$injector->load($services);
$injector->updateSpec('AnotherService', 'filters', 'Three');
$another = $injector->get('AnotherService');
$this->assertEquals(3, count($another->filters));
$this->assertEquals('Three', $another->filters[2]);
}
public function testAutoSetInjector() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$injector->addAutoProperty('auto', 'somevalue');
$config = array(array('src' => TEST_SERVICES . '/SampleService.php',));
$injector->load($config);
$this->assertTrue($injector->hasService('SampleService') == 'SampleService');
// We expect a false because the 'AnotherService' is actually
// just a replacement of the SampleService
$myObject = new TestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->sampleService), 'SampleService');
$this->assertEquals($myObject->auto, 'somevalue');
}
public function testSettingSpecificProperty() {
$injector = new Injector();
$config = array('AnotherService');
$injector->load($config);
$injector->setInjectMapping('TestObject', 'sampleService', 'AnotherService');
$testObject = $injector->get('TestObject');
$this->assertEquals(get_class($testObject->sampleService), 'AnotherService');
}
public function testSettingSpecificMethod() {
$injector = new Injector();
$config = array('AnotherService');
$injector->load($config);
$injector->setInjectMapping('TestObject', 'setSomething', 'AnotherService', 'method');
$testObject = $injector->get('TestObject');
$this->assertEquals(get_class($testObject->sampleService), 'AnotherService');
}
public function testInjectingScopedService() {
$injector = new Injector();
$config = array(
'AnotherService',
'AnotherService.DottedChild' => 'SampleService',
);
$injector->load($config);
$service = $injector->get('AnotherService.DottedChild');
$this->assertEquals(get_class($service), 'SampleService');
$service = $injector->get('AnotherService.Subset');
$this->assertEquals(get_class($service), 'AnotherService');
$injector->setInjectMapping('TestObject', 'sampleService', 'AnotherService.Geronimo');
$testObject = $injector->create('TestObject');
$this->assertEquals(get_class($testObject->sampleService), 'AnotherService');
$injector->setInjectMapping('TestObject', 'sampleService', 'AnotherService.DottedChild.AnotherDown');
$testObject = $injector->create('TestObject');
$this->assertEquals(get_class($testObject->sampleService), 'SampleService');
}
public function testInjectUsingConstructor() {
$injector = new Injector();
$config = array(array(
'src' => TEST_SERVICES . '/SampleService.php',
'constructor' => array(
'val1',
'val2',
)
));
$injector->load($config);
$sample = $injector->get('SampleService');
$this->assertEquals($sample->constructorVarOne, 'val1');
$this->assertEquals($sample->constructorVarTwo, 'val2');
$injector = new Injector();
$config = array(
'AnotherService',
array(
'src' => TEST_SERVICES . '/SampleService.php',
'constructor' => array(
'val1',
'%$AnotherService',
)
)
);
$injector->load($config);
$sample = $injector->get('SampleService');
$this->assertEquals($sample->constructorVarOne, 'val1');
$this->assertEquals(get_class($sample->constructorVarTwo), 'AnotherService');
}
public function testInjectUsingSetter() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$config = array(array('src' => TEST_SERVICES . '/SampleService.php',));
$injector->load($config);
$this->assertTrue($injector->hasService('SampleService') == 'SampleService');
$myObject = new OtherTestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->s()), 'SampleService');
// and again because it goes down a different code path when setting things
// based on the inject map
$myObject = new OtherTestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->s()), 'SampleService');
}
// make sure we can just get any arbitrary object - it should be created for us
public function testInstantiateAnObjectViaGet() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$config = array(array('src' => TEST_SERVICES . '/SampleService.php',));
$injector->load($config);
$this->assertTrue($injector->hasService('SampleService') == 'SampleService');
$myObject = $injector->get('OtherTestObject');
$this->assertEquals(get_class($myObject->s()), 'SampleService');
// and again because it goes down a different code path when setting things
// based on the inject map
$myObject = $injector->get('OtherTestObject');
$this->assertEquals(get_class($myObject->s()), 'SampleService');
}
public function testCircularReference() {
$services = array('CircularOne', 'CircularTwo');
$injector = new Injector($services);
$injector->setAutoScanProperties(true);
$obj = $injector->get('NeedsBothCirculars');
$this->assertTrue($obj->circularOne instanceof CircularOne);
$this->assertTrue($obj->circularTwo instanceof CircularTwo);
}
public function testPrototypeObjects() {
$services = array('CircularOne', 'CircularTwo', array('class' => 'NeedsBothCirculars', 'type' => 'prototype'));
$injector = new Injector($services);
$injector->setAutoScanProperties(true);
$obj1 = $injector->get('NeedsBothCirculars');
$obj2 = $injector->get('NeedsBothCirculars');
// if this was the same object, then $obj1->var would now be two
$obj1->var = 'one';
$obj2->var = 'two';
$this->assertTrue($obj1->circularOne instanceof CircularOne);
$this->assertTrue($obj1->circularTwo instanceof CircularTwo);
$this->assertEquals($obj1->circularOne, $obj2->circularOne);
$this->assertNotEquals($obj1, $obj2);
}
public function testSimpleInstantiation() {
$services = array('CircularOne', 'CircularTwo');
$injector = new Injector($services);
// similar to the above, but explicitly instantiating this object here
$obj1 = $injector->create('NeedsBothCirculars');
$obj2 = $injector->create('NeedsBothCirculars');
// if this was the same object, then $obj1->var would now be two
$obj1->var = 'one';
$obj2->var = 'two';
$this->assertEquals($obj1->circularOne, $obj2->circularOne);
$this->assertNotEquals($obj1, $obj2);
}
public function testCreateWithConstructor() {
$injector = new Injector();
$obj = $injector->create('CircularTwo', 'param');
$this->assertEquals($obj->otherVar, 'param');
}
public function testSimpleSingleton() {
$injector = new Injector();
$one = $injector->create('CircularOne');
$two = $injector->create('CircularOne');
$this->assertFalse($one === $two);
$one = $injector->get('CircularTwo');
$two = $injector->get('CircularTwo');
$this->assertTrue($one === $two);
}
public function testOverridePriority() {
$injector = new Injector();
$injector->setAutoScanProperties(true);
$config = array(
array(
'src' => TEST_SERVICES . '/SampleService.php',
'priority' => 10,
)
);
// load
$injector->load($config);
// inject
$myObject = new TestObject();
$injector->inject($myObject);
$this->assertEquals(get_class($myObject->sampleService), 'SampleService');
$config = array(
array(
'src' => TEST_SERVICES . '/AnotherService.php',
'id' => 'SampleService',
'priority' => 1,
)
);
// load
$injector->load($config);
$injector->inject($myObject);
$this->assertEquals('SampleService', get_class($myObject->sampleService));
}
/**
* Specific test method to illustrate various ways of setting a requirements backend
*/
public function testRequirementsSettingOptions() {
$injector = new Injector();
$config = array(
'OriginalRequirementsBackend',
'NewRequirementsBackend',
'DummyRequirements' => array(
'constructor' => array(
'%$OriginalRequirementsBackend'
)
)
);
$injector->load($config);
$requirements = $injector->get('DummyRequirements');
$this->assertEquals('OriginalRequirementsBackend', get_class($requirements->backend));
// just overriding the definition here
$injector->load(array(
'DummyRequirements' => array(
'constructor' => array(
'%$NewRequirementsBackend'
)
)
));
// requirements should have been reinstantiated with the new bean setting
$requirements = $injector->get('DummyRequirements');
$this->assertEquals('NewRequirementsBackend', get_class($requirements->backend));
}
/**
* disabled for now
*/
public function testStaticInjections() {
$injector = new Injector();
$config = array(
'NewRequirementsBackend',
);
$injector->load($config);
$si = $injector->get('TestStaticInjections');
$this->assertEquals('NewRequirementsBackend', get_class($si->backend));
}
public function testCustomObjectCreator() {
$injector = new Injector();
$injector->setObjectCreator(new SSObjectCreator());
$config = array(
'OriginalRequirementsBackend',
'DummyRequirements' => array(
'class' => 'DummyRequirements(\'%$OriginalRequirementsBackend\')'
)
);
$injector->load($config);
$requirements = $injector->get('DummyRequirements');
$this->assertEquals('OriginalRequirementsBackend', get_class($requirements->backend));
}
public function testInheritedConfig() {
$injector = new Injector(array('locator' => 'SilverStripeServiceConfigurationLocator'));
Config::inst()->update('Injector', 'MyParentClass', array('properties' => array('one' => 'the one')));
$obj = $injector->get('MyParentClass');
$this->assertEquals($obj->one, 'the one');
$obj = $injector->get('MyChildClass');
$this->assertEquals($obj->one, 'the one');
}
}
class TestObject {
public $sampleService;
public function setSomething($v) {
$this->sampleService = $v;
}
}
class OtherTestObject {
private $sampleService;
public function setSampleService($s) {
$this->sampleService = $s;
}
public function s() {
return $this->sampleService;
}
}
class CircularOne {
public $circularTwo;
}
class CircularTwo {
public $circularOne;
public $otherVar;
public function __construct($value = null) {
$this->otherVar = $value;
}
}
class NeedsBothCirculars {
public $circularOne;
public $circularTwo;
public $var;
}
class MyParentClass {
public $one;
}
class MyChildClass extends MyParentClass {
}
class DummyRequirements {
public $backend;
public function __construct($backend) {
$this->backend = $backend;
}
public function setBackend($backend) {
$this->backend = $backend;
}
}
class OriginalRequirementsBackend {
}
class NewRequirementsBackend {
}
class TestStaticInjections {
public $backend;
static $dependencies = array(
'backend' => '%$NewRequirementsBackend'
);
}
/**
* An example object creator that uses the SilverStripe class(arguments) mechanism for
* creating new objects
*
* @see https://github.com/silverstripe/sapphire
*/
class SSObjectCreator extends InjectionCreator {
public function create(Injector $injector, $class, $params = array()) {
if (strpos($class, '(') === false) {
return parent::create($injector, $class, $params);
} else {
list($class, $params) = self::parse_class_spec($class);
return parent::create($injector, $class, $params);
}
}
/**
* Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
* Returns a 2-elemnent array, with classname and arguments
*/
static function parse_class_spec($classSpec) {
$tokens = token_get_all("<?php $classSpec");
$class = null;
$args = array();
$passedBracket = false;
// Keep track of the current bucket that we're putting data into
$bucket = &$args;
$bucketStack = array();
foreach($tokens as $token) {
$tName = is_array($token) ? $token[0] : $token;
// Get the class naem
if($class == null && is_array($token) && $token[0] == T_STRING) {
$class = $token[1];
// Get arguments
} else if(is_array($token)) {
switch($token[0]) {
case T_CONSTANT_ENCAPSED_STRING:
$argString = $token[1];
switch($argString[0]) {
case '"': $argString = stripcslashes(substr($argString,1,-1)); break;
case "'": $argString = str_replace(array("\\\\", "\\'"),array("\\", "'"), substr($argString,1,-1)); break;
default: throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
}
$bucket[] = $argString;
break;
case T_DNUMBER:
$bucket[] = (double)$token[1];
break;
case T_LNUMBER:
$bucket[] = (int)$token[1];
break;
case T_STRING:
switch($token[1]) {
case 'true': $args[] = true; break;
case 'false': $args[] = false; break;
default: throw new Exception("Bad T_STRING arg '{$token[1]}'");
}
case T_ARRAY:
// Add an empty array to the bucket
$bucket[] = array();
$bucketStack[] = &$bucket;
$bucket = &$bucket[sizeof($bucket)-1];
}
} else {
if($tName == ')') {
// Pop-by-reference
$bucket = &$bucketStack[sizeof($bucketStack)-1];
array_pop($bucketStack);
}
}
}
return array($class, $args);
}
}

View File

@ -0,0 +1,6 @@
<?php
class AnotherService
{
public $filters = array();
}

View File

@ -0,0 +1,12 @@
<?php
class SampleService
{
public $constructorVarOne;
public $constructorVarTwo;
public function __construct($v1 = null, $v2 = null) {
$this->constructorVarOne = $v1;
$this->constructorVarTwo = $v2;
}
}