Ich habe jetzt einige Zeit an einer eigenen Implementierung eines DI-Containers gesessen und was dabei heraus gekommen ist, würde ich gerne mal zur Diskussion in den Raum werfen. Es ist noch nicht 100%ig durch getestet, aber man erkennt die Funktionsweise, denke ich. Schwerpunkte waren Performance und Flexibilität. So - los geht's:
Der eigentliche Container:
Eine Bsp.-Konfiguration:
Ein paar Beispiele:
Ich denke, die Konfiguration ist selbst erklärend. Wer sich wundert, dass es bei den "methods" immer doppelt verschachtelte Arrays gibt: Das hat den Grund, dass man einen Funktionsaufruf mehrmal durchführen kann (z. B. mit verschiedenen Parametern).
Was haltet ihr von der ganzen Sache? Gibt es was, was verbessert werden könnte? Was, was gar nicht geht?
Der eigentliche Container:
PHP-Code:
class ZN_DI_Container {
/**
* @var ZN_DI_Container
*/
protected static $instance = null;
/**
* @var array
*/
protected $config = array();
/**
* @var array
*/
protected $storage = array();
/**
* @var array
*/
protected $loading = array();
/**
* Indicator for binding references
*
* @var string
*/
protected $bindingPrefix = ':';
/**
* Indicator for service references
*
* @var string
*/
protected $servicePrefix = '@';
/**
* Affords a singleton mechanism if needed
*
* @param boolean $new
* @return ZN_DI_Container
*/
public static function instance($new = false) {
if ($new) {
return new self();
}
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function __construct() {
}
/**
* @param array $config
* @return ZN_DI_Container
*/
public function setConfig(array $config) {
$this->config = $config;
return $this;
}
/**
* @return array
*/
public function getConfig() {
return $this->config;
}
/**
* Retrieves an assembled service using the specified di configuration
*
* @param string $serviceId
* @param boolean $singleton = true
* @return object
*/
public function get($serviceId, $singleton = true) {
if (!$this->hasServiceConfig($serviceId) && !$this->hasStoredService($serviceId)) {
//If no config specified, the service must be injected manually
throw new Exception('Service "'.$serviceId.'" must be set manually');
}
if (isset($this->loading[$serviceId])) {
//prevent endless loops
throw new Exception('Circular reference on "'.$serviceId.'"');
}
$this->loading[$serviceId] = true;
if (
($singleton && $this->hasStoredService($serviceId)) ||
(!$this->hasServiceConfig($serviceId) && $this->hasStoredService($serviceId)) //Manually set service
) {
//If singleton or manually injected service -> return existing instance
unset($this->loading[$serviceId]);
return $this->storage[$serviceId];
}
$serviceConfig = $this->getServiceConfig($serviceId);
$class = $serviceConfig['class'];
$service = null;
$args = array();
if (isset($serviceConfig['args']) && is_array($serviceConfig['args']) && count($serviceConfig['args']) > 0) {
//Constructor args available
$ref = new ReflectionClass($class);
$service = $ref->newInstanceArgs($this->prepareParamSetForMethodCall($serviceConfig['args']));
} else {
$service = new $class();
}
//Call methods which are specified in the configuration
if (isset($serviceConfig['methods']) && is_array($serviceConfig['methods']) && count($serviceConfig['methods']) > 0) {
foreach ($serviceConfig['methods'] as $method=>$calls) {
foreach ($calls as $call=>$params) {
$this->callServiceMethod($service, $method, $this->prepareParamSetForMethodCall($params));
}
}
}
unset($this->loading[$serviceId]);
if ($singleton) {
$this->storage[$serviceId] = $service;
return $this->storage[$serviceId];
} else {
return $service;
}
}
/**
* Calls a method on a service depending on given arguments
*
* @param object $service
* @param string method
* @param array $args
* @return mixed
*/
protected function callServiceMethod($service, $method, array $args) {
if (count($args) > 1) {
//if there are more than one argument, use call_user_func_array
return call_user_func_array(array($service, $method, $this->prepareParamSetForMethodCall($args)));
} else if (count($args) == 1) {
return $service->$method($args[0]);
} else {
return $service->$method();
}
}
/**
* Prepares argument lists based on the specified di configuration
*
* @param mixed
* @return mixed
*/
public function prepareParamSetForMethodCall($params) {
if (is_object($params)) {
throw new UnexpectedValueException('Invalid data format');
}
if (is_array($params)) {
foreach ($params as $k=>$v) {
if (is_array($v)) {
$params[$k] = $this->prepareParamSetForMethodCall($v);
} else if ($this->isBindingReference($v)) {
$params[$k] = $this->getBinding($this->getBindingIdFromString($v));
} else if ($this->isServiceReference($v)) {
$params[$k] = $this->get($this->getServiceIdFromString($v));
}
}
} else {
if ($this->isBindingReference($params)) {
$params = $this->getBinding($this->getBindingIdFromString($params));
} else if ($this->isServiceReference($params)) {
$params = $this->get($this->getServiceIdFromString($params));
}
}
return $params;
}
/**
* Injects a service object
*
* @param string $serviceId
* @param object $service
* @param boolean $overwrite = true
*/
public function set($serviceId, $service, $overwrite = true) {
if ($this->hasStoredService($serviceId) && !$overwrite) {
throw new Exception('Service "'.$serviceId.'" is already set');
}
$this->storage[$serviceId] = $service;
return $this;
}
/**
* Checks if a service is already stored (injected or singleton)
*
* @param string $serviceId
* @return boolean
*/
public function hasStoredService($serviceId) {
return isset($this->storage[$serviceId]);
}
/**
* Checks if a configuration set for a service is set
*
* @param string $serviceId
* @return boolean
*/
public function hasServiceConfig($serviceId) {
return isset($this->config['services'][$serviceId]);
}
/**
* Retrieves a configuration set for a cetain service
*
* @param string $serviceId
* @return array
*/
public function getServiceConfig($serviceId) {
if (!$this->hasServiceConfig($serviceId)) {
throw new UnexpectedValueException('Config for service "'.$serviceId.'" not specified');
}
return $this->config['services'][$serviceId];
}
/**
* Specifies a configuration set for a service
*
* @param string $serviceId
* @param array $config
* @$param boolean $overwrite = true
* @return ZN_DI_Container
*/
public function setServiceConfig($serviceId, array $config, $overwrite = true) {
if ($this->hasServiceConfig($serviceId) && !$overwrite) {
throw new Exception('Service config for "'.$serviceId.'" is already set');
}
if (!isset($config['class'])) {
throw new Exception('No class set for serice "'.$serviceId.'"');
}
$this->config['services'][$serviceId] = $config;
return $this;
}
/**
* Checks if a string is a reference to another service
*
* @param string
* @return boolean
*/
public function isServiceReference($serviceId) {
return is_string($serviceId) && strpos($serviceId, $this->getServicePrefix()) === 0;
}
/**
* @param string $servicePrefix
* @return ZN_DI_Container
*/
public function setServicePrefix($servicePrefix) {
$this->servicePrefix = $servicePrefix;
return $this;
}
/**
* @return string
*/
public function getServicePrefix() {
return $this->servicePrefix;
}
/**
* Retrieves the service id from a string which indicates a service reference
*
* @param string $str
* @return string
*/
public function getServiceIdFromString($str) {
return substr($str, strlen($this->getServicePrefix()));
}
/**
* @param $bindingId
* @return boolean
*/
public function hasBinding($bindingId) {
return isset($this->config['bindings'][$bindingId]);
}
/**
* Checks if a string is a reference to binding
*
* @param string
* @return boolean
*/
public function isBindingReference($str) {
return is_string($str) && strpos($str, $this->getBindingPrefix()) === 0;
}
/**
* Retrieves the binding id from a string which indicates a binding reference
*
* @param string $str
* @return string
*/
public function getBindingIdFromString($str) {
return substr($str, strlen($this->getBindingPrefix()));
}
/**
* Injects a binding
*
* @param string bindingId
* @param mixed $value
* @return ZN_DI_Container
*/
public function setBinding($bindingId, $value) {
$this->config['bindings'][$bindingId] = $value;
return $this;
}
/**
* Retrieves a specified binding by an id. If $recursive
* is true the result is checked if it is a binding reference
* itself
*
* @param string $bindingId
* @param boolean $recursive = true
*/
public function getBinding($bindingId, $recursive = true) {
if (!$this->hasBinding($bindingId)) {
throw new UnexpectedValueException('Binding "'.$bindingId.'" not specified or invalid');
}
$rsl = $this->config['bindings'][$bindingId];
if ($this->isBindingReference($rsl)) {
return $this->getBinding($this->getBindingIdFromString($rsl), $recursive);
} else {
return $rsl;
}
}
/**
* @param string $bindingPrefix
* @return ZN_DI_Container
*/
public function setBindingPrefix($bindingPrefix) {
$this->bindingPrefix = $bindingPrefix;
return $this;
}
/**
* @return string
*/
public function getBindingPrefix() {
return $this->bindingPrefix;
}
}
PHP-Code:
return array(
'bindings'=>array(
'dbDSN'=>'mysql:host=localhost;dbname=...',
'dbUser'=>'root',
'dbPassword'=>'123456',
'dbEncodingCommand'=>'SET NAMES UTF8',
'dbDefaultFetchMode'=>PDO::FETCH_ASSOC
),
'services'=>array(
'test'=>array(
'class'=>'TestClass',
'args'=>array(),
'methods'=>array(
'setService'=>array(
array('@service') //Muss manuell injiziert werden, da nicht vorhanden
),
'setDb'=>array(
array('@dbConnection')
)
)
),
'dbConnection'=>array(
'class'=>'PDO',
'args'=>array(
':dbDSN',
':dbUser',
':dbPassword',
array(
PDO::MYSQL_ATTR_INIT_COMMAND=>':dbEncodingCommand',
PDO::ATTR_DEFAULT_FETCH_MODE=>':dbDefaultFetchMode'
)
)
)
)
);
PHP-Code:
class TestClass {
public function setService(StdClass $service) {
}
public function setDb(PDO $db) {
}
}
require_once(dirname(__FILE__).'/zn/di/container.php');
$di = ZN_DI_Container::instance();
$di->setConfig(require(dirname(__FILE__).'/config.php'));
//Binding setzen
$di->setBinding('key', 'value');
//Service manuell setzen
$obj = new StdClass();
$di->set('service', $obj);
var_dump($di->get('test'));
//$db = $di->get('dbConnection');
Was haltet ihr von der ganzen Sache? Gibt es was, was verbessert werden könnte? Was, was gar nicht geht?
Kommentar