mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
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:
parent
3412a0e58d
commit
b269badfbe
6
_config/RequestProcessor.yml
Normal file
6
_config/RequestProcessor.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
name: RequestProcessor
|
||||||
|
---
|
||||||
|
# Providing an empty config so it can be overridden at a later point
|
||||||
|
Injector:
|
||||||
|
RequestProcessor:
|
||||||
|
0:
|
@ -98,7 +98,13 @@ class Director implements TemplateGlobalProvider {
|
|||||||
// Initiate an empty session - doesn't initialize an actual PHP session until saved (see belwo)
|
// Initiate an empty session - doesn't initialize an actual PHP session until saved (see belwo)
|
||||||
$session = new Session(isset($_SESSION) ? $_SESSION : null);
|
$session = new Session(isset($_SESSION) ? $_SESSION : null);
|
||||||
|
|
||||||
// Main request handling
|
$output = Injector::inst()->get('RequestProcessor')->preRequest($req, $session, $model);
|
||||||
|
|
||||||
|
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);
|
$result = Director::handleRequest($req, $session, $model);
|
||||||
|
|
||||||
// Save session data (and start/resume it if required)
|
// 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:') {
|
if(is_string($result) && substr($result,0,9) == 'redirect:') {
|
||||||
$response = new SS_HTTPResponse();
|
$response = new SS_HTTPResponse();
|
||||||
$response->redirect(substr($result, 9));
|
$response->redirect(substr($result, 9));
|
||||||
$response->output();
|
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
|
||||||
|
if ($res !== false) {
|
||||||
|
$response->output();
|
||||||
|
}
|
||||||
// Handle a controller
|
// Handle a controller
|
||||||
} else if($result) {
|
} else if($result) {
|
||||||
if($result instanceof SS_HTTPResponse) {
|
if($result instanceof SS_HTTPResponse) {
|
||||||
@ -120,16 +128,23 @@ class Director implements TemplateGlobalProvider {
|
|||||||
$response->setBody($result);
|
$response->setBody($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?debug_memory=1 will output the number of bytes of memory used for this request
|
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
|
||||||
if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
|
if ($res !== false) {
|
||||||
Debug::message(sprintf(
|
// ?debug_memory=1 will output the number of bytes of memory used for this request
|
||||||
"Peak memory usage in bytes: %s",
|
if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
|
||||||
number_format(memory_get_peak_usage(),0)
|
Debug::message(sprintf(
|
||||||
));
|
"Peak memory usage in bytes: %s",
|
||||||
|
number_format(memory_get_peak_usage(),0)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$response->output();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$response->output();
|
// @TODO Proper response here.
|
||||||
|
throw new SS_HTTPResponse_Exception("Invalid response");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//$controllerObj->getSession()->inst_save();
|
//$controllerObj->getSession()->inst_save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,7 +270,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
Director::$urlParams = $arguments;
|
Director::$urlParams = $arguments;
|
||||||
$controllerObj = new $controller();
|
$controllerObj = Injector::inst()->create($controller);
|
||||||
$controllerObj->setSession($session);
|
$controllerObj->setSession($session);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
24
control/RequestFilter.php
Normal file
24
control/RequestFilter.php
Normal 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);
|
||||||
|
}
|
37
control/RequestProcessor.php
Normal file
37
control/RequestProcessor.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
control/injector/AfterCallAspect.php
Normal file
27
control/injector/AfterCallAspect.php
Normal 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);
|
||||||
|
}
|
41
control/injector/AopProxyService.php
Normal file
41
control/injector/AopProxyService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
control/injector/BeforeCallAspect.php
Normal file
27
control/injector/BeforeCallAspect.php
Normal 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);
|
||||||
|
}
|
827
control/injector/Injector.php
Normal file
827
control/injector/Injector.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -281,6 +281,10 @@ if(Director::isLive()) {
|
|||||||
*/
|
*/
|
||||||
Debug::loadErrorHandlers();
|
Debug::loadErrorHandlers();
|
||||||
|
|
||||||
|
// initialise the dependency injector
|
||||||
|
$default_options = array('locator' => 'SilverStripeServiceConfigurationLocator');
|
||||||
|
Injector::inst($default_options)->addAutoProperty('injector', Injector::inst());
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
|
|
||||||
|
@ -104,11 +104,7 @@ abstract class Object {
|
|||||||
|
|
||||||
$class = self::getCustomClass($class);
|
$class = self::getCustomClass($class);
|
||||||
|
|
||||||
$reflector = new ReflectionClass($class);
|
return Injector::inst()->createWithArgs($class, $args);
|
||||||
if($reflector->getConstructor()) {
|
|
||||||
return $reflector->newInstanceArgs($args);
|
|
||||||
}
|
|
||||||
return new $class;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static $_cache_inst_args = array();
|
private static $_cache_inst_args = array();
|
||||||
|
@ -29,6 +29,26 @@ following two conditions are true:
|
|||||||
redirectBack().
|
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
|
## Custom Rewrite Rules
|
||||||
|
|
||||||
You can influence the way URLs are resolved one of 2 ways
|
You can influence the way URLs are resolved one of 2 ways
|
||||||
|
184
docs/en/reference/injector.md
Normal file
184
docs/en/reference/injector.md
Normal 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.
|
597
tests/injector/InjectorTest.php
Normal file
597
tests/injector/InjectorTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
6
tests/injector/testservices/AnotherService.php
Normal file
6
tests/injector/testservices/AnotherService.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class AnotherService
|
||||||
|
{
|
||||||
|
public $filters = array();
|
||||||
|
}
|
12
tests/injector/testservices/SampleService.php
Normal file
12
tests/injector/testservices/SampleService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user