Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
Marcus Nyeholt authored sminnee committed
6 _config/RequestProcessor.yml
View
@@ -0,0 +1,6 @@
+name: RequestProcessor
+---
+# Providing an empty config so it can be overridden at a later point
+Injector:
+ RequestProcessor:
+ 0:
37 control/Director.php
View
@@ -97,8 +97,14 @@ static function direct($url, DataModel $model) {
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 @@ static function direct($url, DataModel $model) {
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 @@ static function direct($url, DataModel $model) {
$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 @@ protected static function handleRequest(SS_HTTPRequest $request, Session $sessio
} else {
Director::$urlParams = $arguments;
- $controllerObj = new $controller();
+ $controllerObj = Injector::inst()->create($controller);
$controllerObj->setSession($session);
try {
24 control/RequestFilter.php
View
@@ -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);
+}
37 control/RequestProcessor.php
View
@@ -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;
+ }
+ }
+ }
+}
27 control/injector/AfterCallAspect.php
View
@@ -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);
+}
41 control/injector/AopProxyService.php
View
@@ -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;
+ }
+ }
+ }
+}
27 control/injector/BeforeCallAspect.php
View
@@ -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);
+}
827 control/injector/Injector.php
View
@@ -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;
+ }
+ }
+}
4 core/Core.php
View
@@ -281,6 +281,10 @@
*/
Debug::loadErrorHandlers();
+// initialise the dependency injector
+$default_options = array('locator' => 'SilverStripeServiceConfigurationLocator');
+Injector::inst($default_options)->addAutoProperty('injector', Injector::inst());
+
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
6 core/Object.php
View
@@ -104,11 +104,7 @@ public static function create() {
$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();
20 docs/en/reference/director.md
View
@@ -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
184 docs/en/reference/injector.md
View
@@ -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.
597 tests/injector/InjectorTest.php
View
@@ -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);
+ }
+}
6 tests/injector/testservices/AnotherService.php
View
@@ -0,0 +1,6 @@
+<?php
+
+class AnotherService
+{
+ public $filters = array();
+}
12 tests/injector/testservices/SampleService.php
View
@@ -0,0 +1,12 @@
+<?php
+
+class SampleService
+{
+ public $constructorVarOne;
+ public $constructorVarTwo;
+
+ public function __construct($v1 = null, $v2 = null) {
+ $this->constructorVarOne = $v1;
+ $this->constructorVarTwo = $v2;
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.