一步一步带你实现Lumen容器
Ioc依赖反转
Laravel Container 其实就是一个Ioc容器,不过人家实现的更加灵活更加优雅。
简单的依赖反转
下面我们自己实现一个简单Ioc容器
class Ioc {
private $set = [];
private $parsed = [];
public function set ($name , $value) {
$this->set[$name] = $value;
}
public function get($name) {
#如果根本没设置就说明根本没有这个类
if(!isset($this->set[$name])) {
return false;
}
# 如果没有解析过那就解析一下
if(empty($this->parsed[$name])) {
$this->parsed[$name] = $this->set[$name]();
}
return $this->parsed[$name];
}
}
$ioc = new Ioc();
$ioc->set('redis' , function(){
$redis = new Redis();
$redis->connect();
return $redis;
});
$ioc->set('mysql-master', function(){
$master = new MySQL();
$master->connect();
return $master;
});
$ioc->get('redis'); // Redis Instance
$ioc->get('mysql-master'); // MySQL Instance
这就是Ioc,很好理解,如果不理解可以自己多敲几遍。 接下来继续
给自己的Ioc容器提一下B格
如果第一次看Laravel 、Lumen的容器源码可能会被里面的变量名搞的比较迷糊、毕竟很多人英文底子不是很好。
我也有这样的问题T-T、为了解决这个问题咱们简单装饰一下刚才的简单容器、可以初步了解lumen的容器中几个属性名的含义。
class Container {
private $bindings = [];
private $instances = [];
public function bind($abstract , $concrete) {
$this->bindings[$abstract] = $concrete;
}
public function make($abstract) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (empty($this->instances[$abstract])) {
$this->instances[$abstract] = $this->bindings[$abstract]();
}
return $this->instances[$abstract];
}
}
$container = new Container();
$container->bind('redis' , function(){
$redis = new Redis();
$redis->connect();
return $redis;
});
$container->bind('mysql-master' , function(){
$master = new MySQL();
$master->connect();
return $master;
});
$container->make('redis'); //Redis Instance;
$container->make('mysql-master'); //MySQL Instance;
是不是满满的逼格?
任何项目或者事情都是先做出来一个小雏形,在其基础上针对已有的问题做优化、升级;
那么我们现在就开始找问题,并且升级吧。带你一步步实现lumen容器。
问题一,如果想通过同一个类名,获取不同的对象,而不是单例对象怎么办?
class Container {
private $bindings = [];
private $instances = [];
public function bind($abstract , $concrete) {
$this->bindings[$abstract] = $concrete;
}
public function make($abstract) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (empty($this->instances[$abstract])) {
$this->instances[$abstract] = $this->bindings[$abstract]();
}
return $this->instances[$abstract];
}
}
class Person{
public $name;
}
$container = new Container();
$container->bind('person' , function(){
return new Person();
});
$lilei = $container->make('person');
$lilei->name = 'lilei';
$liming = $container->make('person');
$liming->name = 'liming';
echo $lilei->name , PHP_EOL;
echo $liming->name, PHP_EOL;
// 运行结果
// liming
// liming
// 并不是我们想要的结果
lumen的做法很简单就是在bind的时候加一个shared参数,用来告诉容器这个类是否共享的(单例)我们来解决这个问题
class Container {
private $bindings = [];
private $instances = [];
#添加 $shared 参数
public function bind($abstract , $concrete, $shared = false) {
#修改值为一个数组类型
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
];
}
public function make($abstract) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
# 通过数组concrete字段执行闭包
$object = $this->bindings[$abstract]['concrete']();
# 判断是否需要单例模式
if($this->bindings[$abstract]['shared']) {
$this->instances[$abstract] = $object;
}
return $object;
}
}
class Person{
public $name;
}
$container = new Container();
$container->bind('person' , function(){
return new Person();
},true);
$lilei = $container->make('person');
$lilei->name = 'lilei';
$liming = $container->make('person');
$liming->name = 'liming';
echo $lilei->name , PHP_EOL;
echo $liming->name, PHP_EOL;
// 运行结果
// lilei
// liming
// 想要的结果出现了
问题二,这个其实也不算问题,就是写法上的优化,我们是不是每次都要传递第二个参数并且每次都要写一个闭包进去?
# class Container# 复制上面的代码
class Person{
public $name;
}
$container = new Container();
$container->bind('person' , function(){
return new Person();
},true);
我们来分析一下,实际上我已经知道了Person类,那么我们能不能想下面代码中的这么实现呢
...
$container = new Container();
$container->bind('Person' , 'Person' , true);
$container->bind('Person');
...
接下来继续改进
class Container {
private $bindings = [];
private $instances = [];
#这里是生成闭包的地方
public function getClosure($concrete) {
return function() use($concrete) {
return new $concrete;
};
}
public function bind($abstract , $concrete=null, $shared = false)
{
if (is_null($concrete)) {
$concrete = $abstract;
}
# 如果$concrete 不是一个闭包生成一个闭包
if (!$concrete instanceof Closure) {
$concrete = $this->getClosure($concrete);
}
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
];
}
public function make($abstract) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$object = $this->bindings[$abstract]['concrete']();
if($this->bindings[$abstract]['shared']) {
$this->instances[$abstract] = $object;
}
return $object;
}
}
class Person{
public $name;
}
# testing
$container = new Container();
$container->bind('Person' , 'Person' , false);
$p1 = $container->make('Person');
$p2 = $container->make('Person');
var_dump($p1 , $p2 , $p1 === $p2);
$c1 = new Container();
$c1->bind('Person');
$p3 = $c1->make('Person');
$p4 = $c1->make('Person');
var_dump($p3 , $p4 , $p3 === $p4);
$c2 = new Container();
$c2->bind('Person','Person', true);
$p5 = $c2->make('Person');
$p6 = $c2->make('Person');
var_dump($p5 , $p6 , $p5 === $p6);
入门我们自己写这个container大部门人会在make里去判断是否是一个闭包,写完就不再优化了...
问题三,如果我们定义类构造函数的时候依赖其他的参数怎么办?
# class Container# 复制上面的代码
# testing
class Person{
private $name;
private $isProgrammer;
public function __construct($name,$isProgrammer = true) {
$this->name = $name;
$this->isProgrammer = $isProgrammer;
}
public function me() {
$message = $this->isProgrammer ? ',Ta是一个程序员' :'';
return "姓名: {$this->name} $message";
}
}
$container = new Container();
$container->bind('Person');
$p1 = $container->make('Person',[
'name' => 'lilei',
]);
echo $p1->me();
# 这样看肯定是不行的,会报出错误
接下来继续解决问题
class Container {
private $bindings = [];
private $instances = [];
public function getClosure($concrete) {
# 让闭包带参
return function($parameter = []) use($concrete) {
#将参数传递给具体的类、就算构造函数不需要参数这样写也不会有任何问题
return new $concrete($parameter);
};
}
public function bind($abstract , $concrete=null, $shared = false)
{
if (is_null($concrete)) {
$concrete = $abstract;
}
if (!$concrete instanceof Closure) {
$concrete = $this->getClosure($concrete);
}
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
];
}
# 在这里添加一个$paramters 参数
public function make($abstract ,array $parameters = []) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
# 那么其实在这里处理一下就可以了
# 需不需要参数?到底需不需要参数我们不知道
# 因为$this->bindings[$abstract]['concrete'] 是一个闭包函数
$concrete = $this->bindings[$abstract]['concrete'];
#$concrete($parameters) 相当于使用getClosure中闭包函数;
# $concrete = function() use($concrete1) {
// return new $concrete1;
// };
# 如果想传递一个参数给闭包那么应该修改一下getClosure方法,让闭包方法带参
$object = $concrete($parameters);
if($this->bindings[$abstract]['shared']) {
$this->instances[$abstract] = $object;
}
return $object;
}
}
#testing
class Person{
private $name;
public function __construct($param) {
$this->name = $param['name'] ?? 'unknown';
}
public function getName() {
return $this->name;
}
}
虽然看起来解决了带参问题,但是问题很明显。我们给构造函数传递的参数是一个数组,我们无法做到让每个人写的插件都把构造函数写成这样。透明性并不好。
问题四、我们现在遇到的问题是构建对象的时候需要明确构造函数的参数类型、数量。并且增加程序的透明度
# class Container# 复制上面的代码
class Person{
private $name;
public function __construct($param) {
$this->name = $param['name'] ?? 'unknown';
}
public function getName() {
return $this->name;
}
}
我们上一个版本的解决方案显然不是很理想。我们不能要求每个类的构造函数都传一个$param
数组,这样不透明也不现实
那么如果我们要这样使用一个容器呢?
class Person{
private $name;
private $isProgrammer;
public function __construct($name,$isProgrammer = true) {
$this->name = $name;
$this->isProgrammer = $isProgrammer;
}
public function me() {
$message = $this->isProgrammer ? ',Ta是一个程序员' :'';
return "姓名: {$this->name} $message";
}
}
$container = new Container();
$container->bind('Person');
$p1 = $container->make('Person',[
'name' => 'lilei',
]);
echo $p1->me();
接下来我们来解决上述问题
class Container {
private $bindings = [];
private $instances = [];
public function getClosure($concrete) {
return function($parameter = []) use($concrete) {
# 在这里我们找到了判断初始化函数的契机,利用反射我们可以做很多事,包括我们的问题
# 1.获得一个$concrete类反射
$reflector = new ReflectionClass($concrete);
# 2.判断这个类能否被实例化,例如 private function __construct(){}
if(!$reflector->isInstantiable()) {
throw new Exception("{$concrete} 无法被实例化");
}
# 3.获取构造函数反射方法
$constructor = $reflector->getConstructor();
# 4.获取参数列表
$parameters = $constructor->getParameters();
# 5.遍历参数列表
$instances = [];
foreach ($parameters as $_parameter) {
# 如果已经$parameter中已经设置了对应的参数
if(isset($parameter[$_parameter->name])) {
$instances[] = $parameter[$_parameter->name];
continue;
}
# 如果没设置判断一下这个参数是否存在默认值
if(!$_parameter->isDefaultValueAvailable()) {
throw new Exception("{$concrete} 无法被实例化,缺少参数{$_parameter->name}");
}
$instances[] = $_parameter->getDefaultValue();
}
# 这里就需要通过反射来构建对象了
// return new $concrete($parameter);
return $reflector->newInstanceArgs($instances);
};
}
public function bind($abstract , $concrete=null, $shared = false)
{
if (is_null($concrete)) {
$concrete = $abstract;
}
if (!$concrete instanceof Closure) {
$concrete = $this->getClosure($concrete);
}
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
];
}
public function make($abstract ,array $parameters = []) {
if (!isset($this->bindings[$abstract])) {
return false;
}
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
# 先获取到具体的类型
$concrete = $this->bindings[$abstract]['concrete'];
# 这里需要思考一下
# 到目前为止我们的$this->bindings[$abstract]['concrete']里存储的都是通过getClosure方法生成的闭包。
# 那么直接在这里判断类型肯定行不通,所以我们跳到getClosure里面去看看
$object = $concrete($parameters);
if($this->bindings[$abstract]['shared']) {
$this->instances[$abstract] = $object;
}
return $object;
}
}
class Person{
private $name;
private $isProgrammer;
public function __construct($name,$isProgrammer = true) {
$this->name = $name;
$this->isProgrammer = $isProgrammer;
}
public function me() {
$message = $this->isProgrammer ? ',Ta是一个程序员' :'';
return "姓名: {$this->name} $message";
}
}
$container = new Container();
$container->bind('Person');
$p1 = $container->make('Person',[
'name' => 'lilei',
]);
echo $p1->me();
先到这里,休息一下
本系列一律手写,未经允许谢绝转载。