前言
依赖注入,也叫控制反转。简单的来说就是:一个类中用到其他类的实例时,我们不在该类中创建实例,而是在类外创建实例后把实例作为参数传入类中。
当需要创建大量的类的实例的时候,我们为了方便管理,把类实例化的过程分离出来,并存储起来统一管理,这就叫 容器
初级实现
我们通过实现一个缓存模块,来展示依赖注入和容器的基本使用。
首先,建立一个容器类Di.php
<?php
class Di
{
protected $_definitions= [];//存储依赖实例
public function set($name, $definition)
{
$this->_definitions[$name] = $definition;
}
public function get($name)
{
if (isset($this->_definitions[$name])) {
$definition = $this->_definitions[$name];
} else {
throw new Exception("class not exist");
}
if (is_object($definition) || is_callable($definition)) {
$instance = call_user_func($definition);
}else{
throw new Exception("class not obj");
}
}
}
缓存类型有file、Db、Redis,我们建立这三种类型对应的操作类
interface BackendInterface{
public function find($key, $lifetime);
public function save($key, $value, $lifetime);
public function delete($key);
}
class redis implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class db implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class file implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
建立Cache类,即被注入类
class cache
{
protected $_di;
protected $_options;
protected $_connect;
public function __construct($options)
{
$this->_options = $type;
}
public function setDI($di)
{
$this->_di = $di;
$options = $this->_options;
if (isset($options['connect'])) {
$service = $options['connect'];
} else {
$service = 'redis';
}
//根据参数,从容器中找出对应的实例
$this->_connect = $this->_di->get($service);
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
$connect = $this->_connect;
return $connect->save($key, $lifetime);
}
public function delete($key)
{
$connect = $this->_connect;
$connect->delete($key, $lifetime);
}
}
/*****************调用cache************************/
$di = new Di();
// 往Di容器中注入需要用到的实例
$di->set('redis', function() {
return new redisDB([
'host' => '127.0.0.1',
'port' => 6379
]);
});
$di->set('cache', function() use ($di) {
$cache = new cache([
'connect' => 'redis'
]);
$cache->setDi($di);
return $cache;
});
// 调用
$cache = $di->get('cache');
高级实现
以上过程实现了简单的依赖注入和容器,但是我们发现,上面的依赖关系只有一层。如果此时Db类又依赖了其他类时,上面的代码可能要这么改
class db implements BackendInterface
{
public $_options;
public $_di;
public __construct($options){
if (isset($options['type'])) {
$service = $options['type'];
} else {
$service = 'mysql';
}
//根据参数,从容器中找出对应的实例
return $this->_di->get($service);
}
public function setDI($di)
{
$this->_di = $di;
}
public function find($key, $lifetime) {
$connect = $this->di->connect();
return $connect->find();
}
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
}
/*************************调用***********************************/
$di = new Di();
// 往Di容器中注入需要用到的实例
$di->set('mysql', function() {
return new mysql([
'host' => '127.0.0.1',
'port' => 6379
]);
});
$di->set('db', function() use ($di) {
$db = new db([
‘type’=> 'mysql',
]);
$db->setDi($di);
return $db;
});
$di->set('cache', function() use ($di) {
$cache = new cache([
'connect' => 'db'
]);
$cache->setDi($di);
return $cache;
});
$cache = $di->get('cache');
}
上面代码:缓存使用了db方式即数据库方式存储,然后db类型使用的mysql。所以我们先要把mysql类注入到容器,然后再把db类注入到容器,最后把cache注入容器并调用,而且顺序必须按照上面的样子,否则后面的类在注入时找不到依赖的类会报错。
当依赖层级较多的时候,一个个的注入不仅不方便,一旦顺序错误也会造成错误。这个时候我们就需要使用php的ReflectionClass反射机制,构建一个自动注入且不需要关心注入顺序的Di容器。
上面的例子中,我们需要先把依赖的对象注入到Di容器中,在被注入的类中需要用到时从Di容器中取。自动注入则是在被注入函数实例化或者方法被调用时,根据构造函数或方法中指定的参数类型,把对象类型的参数实例化后再传入被注入类中。而要获取类的各种信息,就需要用到反射类 ReflectionClass。改造后Di代码如下:
class Di {
protected $_definitions=[];
// 获得类的对象实例
public static function getInstance($className) {
if(isset($this->_definitions[$className])) return $this->_definitions[$className];
$paramArr = self::getMethodParams($className);
$this->_definition[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
return $this->_definition[$className];
}
//直接调用类的方法
public static function make($className, $methodName, $params = []) {
$instance = self::getInstance($className);
// 获取方法所需要依赖注入的参数
$paramArr = self::getMethodParams($className, $methodName);
return $instance->$methodName(array_merge($paramArr, $params));
}
// 获得类的方法参数,把对象类参数实例化
protected static function getMethodParams($className, $methodsName = '__construct') {
// 通过反射获得该类的信息
$class = new ReflectionClass($className);
$paramArr = []; // 记录参数,和参数类型
// 判断函数方法名是否存在
if ($class->hasMethod($methodsName)) {
$construct = $class->getMethod($methodsName);
// 判断函数方法是否有参数
$params = $construct->getParameters();
if (count($params) > 0) {
// 判断参数类型
foreach ($params as $key => $param) {
if (is_obj($param)) {
// 获得参数类型名称
$paramClassName = get_class($param);
//递归获取依赖的对象是否依赖其他对象
$args = self::getMethodParams($paramClassName);
//返回依赖对象的实例作为参数
$paramArr[] = (new ReflectionClass($paramClassName))->newInstanceArgs($args);
}
}
}
}
return $paramArr;
}
}
cache和db代码改造后如下
class cache
{
protected $_connect;
public function __construct(db $db)
{
$this->_connect= $db;
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
$connect = $this->_connect;
return $connect->save($key, $lifetime);
}
public function delete($key)
{
$connect = $this->_connect;
$connect->delete($key, $lifetime);
}
}
class db implements BackendInterface
{
public $_connect;
public __construct(mysql $mysql){
$this->_connect = $mysql->connect();
}
public function find($key, $lifetime) {
$connect = $this->_connect();
return $connect->find();
}
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
}
//调用
$cache = Di::getInstance('cache');
改造后我们无需注册依赖就可以直接调用实例化cache并调用方法。
但这个这个时候,代码的局限性很大,当我们需要调整cache的存储方式为redis时,发现不能通过传入参数调整。代码还需要优化,主要是Di中自动解析依赖的地方,要支持数组的解析。而且涉及多层依赖多层配置时,还是要引入注册机制,但此时是的注册只指定依赖关系,不实例化依赖对象,当Di解析依赖时,可根据注册的依赖关系获取依赖对象。且此时注册也不需要分先后顺序。
修改后代码如下
class Di {
protected $_dependencies=[];//存储依赖对象实例
protected $_definitions=[];//存储注册依赖信息
protected $_params=[];//存储映射对象的参数
// 注册依赖关系
public function set($className,$param) {
if(isset($this->_definitions[$className])) return $this->_definitions[$className];
$paramArr = self::getMethodParams($className);
$this->_definition[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
$this->_definitions[$className] = $param['class'];
$this->_params[$className] = $param;
}
// 获取依赖
public function get($className) {
if(isset($this->_dependencies($className))) return $this->_dependencies($className));
// 使用PHP的反射机制来获取类的有关信息,主要就是为了获取依赖信息
$reflection = new ReflectionClass($this->_definitions[$className]);
$dependencies = unset($this->params[$className]);
// 通过类的构建函数的参数来了解这个类依赖于哪些单元
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if ($param->isDefaultValueAvailable()) {
// 构造函数如果有默认值,将默认值作为依赖。即然是默认值了,
// 就肯定是简单类型了。
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
// 构造函数没有默认值,则为其创建一个引用。
// 就是前面提到的 Instance 类型。
$dependencies[] = $this->get($c->getName());
}
}
}
$obj = $reflection->newInstanceArgs($dependencies);
$this->_dependencies[$className] = $obj;
return $obj;
}
}
class cache
{
protected $_connect;
//类的依赖信息尽量放在构造函数中,便于在实例化类时解析相关依赖
//解析依赖时根据指定的实例路径生成映射,我们从上面Di的get代码中看到,获取依赖对象的映射时
//并不是直接使用的指定路径,而是通过指定的路径从_definitions中获取真实的依赖类信息,这样我们需要用到不同模块时,只要修改配置中的class等信息即可
public function __construct(BackendInterface $cache)
{
$this->_connect= $cache;
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
}
public function delete($key)
{
}
}
class db implements BackendInterface
{
public $connect;
public __construct(sql $db){
$this->connect = $db;
}
public function find($key, $lifetime) {
return $this->connect->find();
}
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
}
//调用
$di = new Di();
//注册部分先后顺序,因为此时并没有实例化,只是注册了依赖关系
$di->set('cache',['class'=>'\\cache'])
$di->set('BackendInterface ',['class'=>'\\app\\db']);//使用数据库存储缓存数据
//若我们要用redis方式存储缓存数据,可以修改后面的配置信息
//如:$di->set('BackendInterface ',['class'=>'\\app\\redis','host'=>'','name'=>'']);
$di->set('sql',['class'=>'\\sql\\mysql','host'=>'127.0.0.1','name'=>'test']);
//调用时自动解析依赖并实例化
$cache = $di->get('cache');