You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
18 KiB
457 lines
18 KiB
<?php |
|
/** |
|
* @link http://www.yiiframework.com/ |
|
* @copyright Copyright (c) 2008 Yii Software LLC |
|
* @license http://www.yiiframework.com/license/ |
|
*/ |
|
|
|
namespace yii\di; |
|
|
|
use ReflectionClass; |
|
use yii\base\Component; |
|
use yii\base\InvalidConfigException; |
|
|
|
/** |
|
* Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container. |
|
* |
|
* A dependency injection (DI) container is an object that knows how to instantiate and configure objects and |
|
* all their dependent objects. For more information about DI, please refer to |
|
* [Martin Fowler's article](http://martinfowler.com/articles/injection.html). |
|
* |
|
* Container supports constructor injection as well as property injection. |
|
* |
|
* To use Container, you first need to set up the class dependencies by calling [[set()]]. |
|
* You then call [[get()]] to create a new class object. Container will automatically instantiate |
|
* dependent objects, inject them into the object being created, configure and finally return the newly created object. |
|
* |
|
* By default, [[\Yii::$container]] refers to a Container instance which is used by [[\Yii::createObject()]] |
|
* to create new object instances. You may use this method to replace the `new` operator |
|
* when creating a new object, which gives you the benefit of automatic dependency resolution and default |
|
* property configuration. |
|
* |
|
* Below is an example of using Container: |
|
* |
|
* ```php |
|
* namespace app\models; |
|
* |
|
* use yii\base\Object; |
|
* use yii\db\Connection; |
|
* use yii\di\Container; |
|
* |
|
* interface UserFinderInterface |
|
* { |
|
* function findUser(); |
|
* } |
|
* |
|
* class UserFinder extends Object implements UserFinderInterface |
|
* { |
|
* public $db; |
|
* |
|
* public function __construct(Connection $db, $config = []) |
|
* { |
|
* $this->db = $db; |
|
* parent::__construct($config); |
|
* } |
|
* |
|
* public function findUser() |
|
* { |
|
* } |
|
* } |
|
* |
|
* class UserLister extends Object |
|
* { |
|
* public $finder; |
|
* |
|
* public function __construct(UserFinderInterface $finder, $config = []) |
|
* { |
|
* $this->finder = $finder; |
|
* parent::__construct($config); |
|
* } |
|
* } |
|
* |
|
* $container = new Container; |
|
* $container->set('yii\db\Connection', [ |
|
* 'dsn' => '...', |
|
* ]); |
|
* $container->set('app\models\UserFinderInterface', [ |
|
* 'class' => 'app\models\UserFinder', |
|
* ]); |
|
* $container->set('userLister', 'app\models\UserLister'); |
|
* |
|
* $lister = $container->get('userLister'); |
|
* |
|
* // which is equivalent to: |
|
* |
|
* $db = new \yii\db\Connection(['dsn' => '...']); |
|
* $finder = new UserFinder($db); |
|
* $lister = new UserLister($finder); |
|
* ``` |
|
* |
|
* @property array $definitions The list of the object definitions or the loaded shared objects (type or ID => |
|
* definition or instance). This property is read-only. |
|
* |
|
* @author Qiang Xue <qiang.xue@gmail.com> |
|
* @since 2.0 |
|
*/ |
|
class Container extends Component |
|
{ |
|
/** |
|
* @var array singleton objects indexed by their types |
|
*/ |
|
private $_singletons = []; |
|
/** |
|
* @var array object definitions indexed by their types |
|
*/ |
|
private $_definitions = []; |
|
/** |
|
* @var array constructor parameters indexed by object types |
|
*/ |
|
private $_params = []; |
|
/** |
|
* @var array cached ReflectionClass objects indexed by class/interface names |
|
*/ |
|
private $_reflections = []; |
|
/** |
|
* @var array cached dependencies indexed by class/interface names. Each class name |
|
* is associated with a list of constructor parameter types or default values. |
|
*/ |
|
private $_dependencies = []; |
|
|
|
|
|
/** |
|
* Returns an instance of the requested class. |
|
* |
|
* You may provide constructor parameters (`$params`) and object configurations (`$config`) |
|
* that will be used during the creation of the instance. |
|
* |
|
* If the class implements [[\yii\base\Configurable]], the `$config` parameter will be passed as the last |
|
* parameter to the class constructor; Otherwise, the configuration will be applied *after* the object is |
|
* instantiated. |
|
* |
|
* Note that if the class is declared to be singleton by calling [[setSingleton()]], |
|
* the same instance of the class will be returned each time this method is called. |
|
* In this case, the constructor parameters and object configurations will be used |
|
* only if the class is instantiated the first time. |
|
* |
|
* @param string $class the class name or an alias name (e.g. `foo`) that was previously registered via [[set()]] |
|
* or [[setSingleton()]]. |
|
* @param array $params a list of constructor parameter values. The parameters should be provided in the order |
|
* they appear in the constructor declaration. If you want to skip some parameters, you should index the remaining |
|
* ones with the integers that represent their positions in the constructor parameter list. |
|
* @param array $config a list of name-value pairs that will be used to initialize the object properties. |
|
* @return object an instance of the requested class. |
|
* @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition |
|
*/ |
|
public function get($class, $params = [], $config = []) |
|
{ |
|
if (isset($this->_singletons[$class])) { |
|
// singleton |
|
return $this->_singletons[$class]; |
|
} elseif (!isset($this->_definitions[$class])) { |
|
return $this->build($class, $params, $config); |
|
} |
|
|
|
$definition = $this->_definitions[$class]; |
|
|
|
if (is_callable($definition, true)) { |
|
$params = $this->resolveDependencies($this->mergeParams($class, $params)); |
|
$object = call_user_func($definition, $this, $params, $config); |
|
} elseif (is_array($definition)) { |
|
$concrete = $definition['class']; |
|
unset($definition['class']); |
|
|
|
$config = array_merge($definition, $config); |
|
$params = $this->mergeParams($class, $params); |
|
|
|
if ($concrete === $class) { |
|
$object = $this->build($class, $params, $config); |
|
} else { |
|
$object = $this->get($concrete, $params, $config); |
|
} |
|
} elseif (is_object($definition)) { |
|
return $this->_singletons[$class] = $definition; |
|
} else { |
|
throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition)); |
|
} |
|
|
|
if (array_key_exists($class, $this->_singletons)) { |
|
// singleton |
|
$this->_singletons[$class] = $object; |
|
} |
|
|
|
return $object; |
|
} |
|
|
|
/** |
|
* Registers a class definition with this container. |
|
* |
|
* For example, |
|
* |
|
* ```php |
|
* // register a class name as is. This can be skipped. |
|
* $container->set('yii\db\Connection'); |
|
* |
|
* // register an interface |
|
* // When a class depends on the interface, the corresponding class |
|
* // will be instantiated as the dependent object |
|
* $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); |
|
* |
|
* // register an alias name. You can use $container->get('foo') |
|
* // to create an instance of Connection |
|
* $container->set('foo', 'yii\db\Connection'); |
|
* |
|
* // register a class with configuration. The configuration |
|
* // will be applied when the class is instantiated by get() |
|
* $container->set('yii\db\Connection', [ |
|
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', |
|
* 'username' => 'root', |
|
* 'password' => '', |
|
* 'charset' => 'utf8', |
|
* ]); |
|
* |
|
* // register an alias name with class configuration |
|
* // In this case, a "class" element is required to specify the class |
|
* $container->set('db', [ |
|
* 'class' => 'yii\db\Connection', |
|
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', |
|
* 'username' => 'root', |
|
* 'password' => '', |
|
* 'charset' => 'utf8', |
|
* ]); |
|
* |
|
* // register a PHP callable |
|
* // The callable will be executed when $container->get('db') is called |
|
* $container->set('db', function ($container, $params, $config) { |
|
* return new \yii\db\Connection($config); |
|
* }); |
|
* ``` |
|
* |
|
* If a class definition with the same name already exists, it will be overwritten with the new one. |
|
* You may use [[has()]] to check if a class definition already exists. |
|
* |
|
* @param string $class class name, interface name or alias name |
|
* @param mixed $definition the definition associated with `$class`. It can be one of the followings: |
|
* |
|
* - a PHP callable: The callable will be executed when [[get()]] is invoked. The signature of the callable |
|
* should be `function ($container, $params, $config)`, where `$params` stands for the list of constructor |
|
* parameters, `$config` the object configuration, and `$container` the container object. The return value |
|
* of the callable will be returned by [[get()]] as the object instance requested. |
|
* - a configuration array: the array contains name-value pairs that will be used to initialize the property |
|
* values of the newly created object when [[get()]] is called. The `class` element stands for the |
|
* the class of the object to be created. If `class` is not specified, `$class` will be used as the class name. |
|
* - a string: a class name, an interface name or an alias name. |
|
* @param array $params the list of constructor parameters. The parameters will be passed to the class |
|
* constructor when [[get()]] is called. |
|
* @return static the container itself |
|
*/ |
|
public function set($class, $definition = [], array $params = []) |
|
{ |
|
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition); |
|
$this->_params[$class] = $params; |
|
unset($this->_singletons[$class]); |
|
return $this; |
|
} |
|
|
|
/** |
|
* Registers a class definition with this container and marks the class as a singleton class. |
|
* |
|
* This method is similar to [[set()]] except that classes registered via this method will only have one |
|
* instance. Each time [[get()]] is called, the same instance of the specified class will be returned. |
|
* |
|
* @param string $class class name, interface name or alias name |
|
* @param mixed $definition the definition associated with `$class`. See [[set()]] for more details. |
|
* @param array $params the list of constructor parameters. The parameters will be passed to the class |
|
* constructor when [[get()]] is called. |
|
* @return static the container itself |
|
* @see set() |
|
*/ |
|
public function setSingleton($class, $definition = [], array $params = []) |
|
{ |
|
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition); |
|
$this->_params[$class] = $params; |
|
$this->_singletons[$class] = null; |
|
return $this; |
|
} |
|
|
|
/** |
|
* Returns a value indicating whether the container has the definition of the specified name. |
|
* @param string $class class name, interface name or alias name |
|
* @return boolean whether the container has the definition of the specified name.. |
|
* @see set() |
|
*/ |
|
public function has($class) |
|
{ |
|
return isset($this->_definitions[$class]); |
|
} |
|
|
|
/** |
|
* Returns a value indicating whether the given name corresponds to a registered singleton. |
|
* @param string $class class name, interface name or alias name |
|
* @param boolean $checkInstance whether to check if the singleton has been instantiated. |
|
* @return boolean whether the given name corresponds to a registered singleton. If `$checkInstance` is true, |
|
* the method should return a value indicating whether the singleton has been instantiated. |
|
*/ |
|
public function hasSingleton($class, $checkInstance = false) |
|
{ |
|
return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons); |
|
} |
|
|
|
/** |
|
* Removes the definition for the specified name. |
|
* @param string $class class name, interface name or alias name |
|
*/ |
|
public function clear($class) |
|
{ |
|
unset($this->_definitions[$class], $this->_singletons[$class]); |
|
} |
|
|
|
/** |
|
* Normalizes the class definition. |
|
* @param string $class class name |
|
* @param string|array|callable $definition the class definition |
|
* @return array the normalized class definition |
|
* @throws InvalidConfigException if the definition is invalid. |
|
*/ |
|
protected function normalizeDefinition($class, $definition) |
|
{ |
|
if (empty($definition)) { |
|
return ['class' => $class]; |
|
} elseif (is_string($definition)) { |
|
return ['class' => $definition]; |
|
} elseif (is_callable($definition, true) || is_object($definition)) { |
|
return $definition; |
|
} elseif (is_array($definition)) { |
|
if (!isset($definition['class'])) { |
|
if (strpos($class, '\\') !== false) { |
|
$definition['class'] = $class; |
|
} else { |
|
throw new InvalidConfigException("A class definition requires a \"class\" member."); |
|
} |
|
} |
|
return $definition; |
|
} else { |
|
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition)); |
|
} |
|
} |
|
|
|
/** |
|
* Returns the list of the object definitions or the loaded shared objects. |
|
* @return array the list of the object definitions or the loaded shared objects (type or ID => definition or instance). |
|
*/ |
|
public function getDefinitions() |
|
{ |
|
return $this->_definitions; |
|
} |
|
|
|
/** |
|
* Creates an instance of the specified class. |
|
* This method will resolve dependencies of the specified class, instantiate them, and inject |
|
* them into the new instance of the specified class. |
|
* @param string $class the class name |
|
* @param array $params constructor parameters |
|
* @param array $config configurations to be applied to the new instance |
|
* @return object the newly created instance of the specified class |
|
*/ |
|
protected function build($class, $params, $config) |
|
{ |
|
/* @var $reflection ReflectionClass */ |
|
list ($reflection, $dependencies) = $this->getDependencies($class); |
|
|
|
foreach ($params as $index => $param) { |
|
$dependencies[$index] = $param; |
|
} |
|
|
|
$dependencies = $this->resolveDependencies($dependencies, $reflection); |
|
if (empty($config)) { |
|
return $reflection->newInstanceArgs($dependencies); |
|
} |
|
|
|
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) { |
|
// set $config as the last parameter (existing one will be overwritten) |
|
$dependencies[count($dependencies) - 1] = $config; |
|
return $reflection->newInstanceArgs($dependencies); |
|
} else { |
|
$object = $reflection->newInstanceArgs($dependencies); |
|
foreach ($config as $name => $value) { |
|
$object->$name = $value; |
|
} |
|
return $object; |
|
} |
|
} |
|
|
|
/** |
|
* Merges the user-specified constructor parameters with the ones registered via [[set()]]. |
|
* @param string $class class name, interface name or alias name |
|
* @param array $params the constructor parameters |
|
* @return array the merged parameters |
|
*/ |
|
protected function mergeParams($class, $params) |
|
{ |
|
if (empty($this->_params[$class])) { |
|
return $params; |
|
} elseif (empty($params)) { |
|
return $this->_params[$class]; |
|
} else { |
|
$ps = $this->_params[$class]; |
|
foreach ($params as $index => $value) { |
|
$ps[$index] = $value; |
|
} |
|
return $ps; |
|
} |
|
} |
|
|
|
/** |
|
* Returns the dependencies of the specified class. |
|
* @param string $class class name, interface name or alias name |
|
* @return array the dependencies of the specified class. |
|
*/ |
|
protected function getDependencies($class) |
|
{ |
|
if (isset($this->_reflections[$class])) { |
|
return [$this->_reflections[$class], $this->_dependencies[$class]]; |
|
} |
|
|
|
$dependencies = []; |
|
$reflection = new ReflectionClass($class); |
|
|
|
$constructor = $reflection->getConstructor(); |
|
if ($constructor !== null) { |
|
foreach ($constructor->getParameters() as $param) { |
|
if ($param->isDefaultValueAvailable()) { |
|
$dependencies[] = $param->getDefaultValue(); |
|
} else { |
|
$c = $param->getClass(); |
|
$dependencies[] = Instance::of($c === null ? null : $c->getName()); |
|
} |
|
} |
|
} |
|
|
|
$this->_reflections[$class] = $reflection; |
|
$this->_dependencies[$class] = $dependencies; |
|
|
|
return [$reflection, $dependencies]; |
|
} |
|
|
|
/** |
|
* Resolves dependencies by replacing them with the actual object instances. |
|
* @param array $dependencies the dependencies |
|
* @param ReflectionClass $reflection the class reflection associated with the dependencies |
|
* @return array the resolved dependencies |
|
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled. |
|
*/ |
|
protected function resolveDependencies($dependencies, $reflection = null) |
|
{ |
|
foreach ($dependencies as $index => $dependency) { |
|
if ($dependency instanceof Instance) { |
|
if ($dependency->id !== null) { |
|
$dependencies[$index] = $this->get($dependency->id); |
|
} elseif ($reflection !== null) { |
|
$name = $reflection->getConstructor()->getParameters()[$index]->getName(); |
|
$class = $reflection->getName(); |
|
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\"."); |
|
} |
|
} |
|
} |
|
return $dependencies; |
|
} |
|
}
|
|
|