前言
依賴注入,也叫控制反轉。簡單的來說就是:一個類中用到其他類的實例時,我們不在該類中創建實例,而是在類外創建實例後把實例作爲參數傳入類中。
當需要創建大量的類的實例的時候,我們爲了方便管理,把類實例化的過程分離出來,並存儲起來統一管理,這就叫 容器
初級實現
我們通過實現一個緩存模塊,來展示依賴注入和容器的基本使用。
首先,建立一個容器類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');