Container引发的一场变革
Yii 1.x、thinkPHP、CodeIgniter在PHP 5.3之前,MVC的实现算是比较的足规中矩,大体解决办法是从REQUEST_URI中提取uri,根据uri规则分解出contraller、action、params或者还有app(Yaf)。逻辑也比较清晰,用notepad++就可以了解MVC的结构和主体思想。
现在的MVC框架随着PHP版本的升级,支持的特性越来越多,尤其匿名函数这概念的引入,使得服务容器Container在众多一流MVC新版本中极为受宠。laravel的Illuminate/Container使得laravel 5可以让开发者非常灵活组合地使用composer组件。其他如Symfony 2的Component/DependencyInjection/ContainerBuilder和Yii 2的di/Container各家都做了自己的实现。
服务容器也叫IoC 容器,或者另外一些说法叫控制反转、依赖注入。暂时叫依赖注入,这名字更贴切的表达服务容器的使命:为解决依赖而生。
先来简释Yii2的Container,事实上Yii2的容器实现非常复杂。以Controller::behaviors()这方法说起,先看下:
public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'actions' => ['login', 'error'], 'allow' => true, ], [ 'actions' => ['logout', 'index'], 'allow' => true, 'roles' => ['@'], ], ], ], 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'logout' => ['post'], ], ], ]; }这就得一路追起来
- []yii\web\Application[/][]yii\base\Application::run()[/][]yii\base\Component::trigger()[/][]yii\base\Component::ensureBehaviors()[/][]yii\base\Component::attachBehaviorInternal()(到现在终于才看到controller::behavior()的影子)[/][]yii\BaseYii::createObject()(终于是服务容器登场了)[/][]yii\di\Container::get()[/]
# 1、对于单例(对象)来说,无须检查依赖和参数传递 if (isset($this->_singletons[$class])) { return $this->_singletons[$class]; # 2、好吧,首次创建这个对象 } elseif (!isset($this->_definitions[$class])) { return $this->build($class, $params, $config); }Container::build()这个对象得需要知道这个对象的依赖关系:Container::getDependencies($class)
$dependencies = ; # 先建立对象反射 $reflection = new ReflectionClass($class); # 获取这个对象的初始化__construct(Foo $foo, $level = 0)依赖条件 $constructor = $reflection->getConstructor(); if ($constructor !== null) { foreach ($constructor->getParameters() as $param) { # 有默认值的好说,如level = 0 if ($param->isDefaultValueAvailable()) { $dependencies = $param->getDefaultValue(); # 否则,我们得知道这个依赖的类是什么,如:类Far,并且创建这个对象Instance::of('Foo') } else { $c = $param->getClass(); $dependencies = Instance::of($c === null ? null : $c->getName()); } } } # 记录$_reflections、$_dependencies并返回 $this->_reflections[$class] = $reflection; $this->_dependencies[$class] = $dependencies;此时已经得到一个AccessControl的反射类和相关依赖,好吧,回头一看Controller::behaviors()应该可以由服务容器提供一个AccessControl了吧。细心的同学在上面获取依赖类的过程,有一个细节:创建类Far是用了Instance::of('Far'),只是一个$id = 'Far'的Instance。并不是真正的Far类,而且能想到这个Far实例会不会也像AccessControl一样也有依赖呢?啊,这样下去还有完没完了! 好吧,那所有的都走一次Container::get($class, $params),满足了吧。所以也就有了Container::build($class, $params)方法里先解决一层AccessControl依赖,接着再来一次解决依赖Container::resolveDependencies()
list ($reflection, $dependencies) = $this->getDependencies($class); ... $dependencies = $this->resolveDependencies($dependencies, $reflection);具体看Container::resolveDependencies()的实现
/** * 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) { # 取回$id = 'Far'值,重新Container::get('Far')得到真正的实例,新的一轮Container::get()又开始了,直到所有依赖的依赖的依赖...都被解决 $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; }Container::build($class, $params)已经被打断两次了,好不容易把依赖都解决完了,终于可以创建最开始的实例AccessControl了。
# 打断:解决依赖
list ($reflection, $dependencies) = $this->getDependencies($class);
...
# 打断:解决依赖的依赖...
$dependencies = $this->resolveDependencies($dependencies, $reflection);
# 利用反射类实例对象,顺手把$config数据元素赋到对象的属性
if (!empty($config) && !empty($dependencies) && is_a($class, 'yii\base\Object', true)) {
// 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;
}
纵观上面服务容器可以支持实例singleton、类名string,但还不能支持闭包clourse,那Yii 2怎么好意思呢?刚才追到了Container::build($class, $params, $config),现在稍微回溯一级到Container::get($class, $params, $config)
在我们看代码之前,试想下,如果自己要实现一个服务容器,分别支持这三种类型该如何设计?实例不须做工作,先记录保存;字符串类名需要特别细心,通过上述层层依赖的反射最终可以解决;剩下的闭包可以通过call_user_func处理匿名函数就可以得到最终的实例。现在验证下Yii 2是不是也这样的策略。
在我们马上要彻底分析Container::get之前,还有一些工作需要我们理清楚的。get相当于依赖解析和实例化对象,而之前还有一个工作就是注入。到现在我们也还没有对注入进行分析,而在PHP中设计一个服务容器支持上面提到的三种类型,注入是重要的入口,只有注入优雅了,依赖解析才会优雅。
Yii 2的注入是在Container::set中实现,Container::set($class, $params)都支持哪些类注册方式?以达到我们可以随意的Container::get呢?我简单分类说明,代码可以忽略,以下注释就是对Container::set中唯一一个方法normalizeDefinition($class, $definition)对定义进行规范化处理的实例版本。#A:初级版本的注册,直接一个命名空间的类名,毫无挑战性,甚至都没有注册的必要 $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 #B:对于用接口作为类型约束,那实例化时可不能对接口进行实例化,需要根据实际的继承类来实例化。 # 如:__construct(yii\mail\MailInterface $mailer),而最终实例化的是yii\swiftmailer\Mailer $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); // register an alias name. You can use $container->get('db') // to create an instance of Connection #C:如果你觉得yii\db\Connection这货名字太长,可以别名为db,这样在model层就可以随意的$container->get('db') $container->set('db', 'yii\db\Connection'); // register a class with configuration. The configuration // will be applied when the class is instantiated by get() #D:如果对于A版本,没法满足你了,需要在注入时就初始化该的一些属性,使用$params数组即可 $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 #E:如果你想要C+D这种结合体,当然也可以,请在$params的key为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 #F:总会有一些任性的同学,我想要自定义类,那总得支持吧,请使用闭包函数吧 # 虽然它不会像js那些做到真正的回调,但变量的作用域的思想是一致的 $container->set('db', function ($container, $params, $config) { return new \yii\db\Connection($config); });既然注入是这么简单的规则,学习成本小,接下来的最后依赖解析Container::get($class, $params, $config)的完整代码。
/** * Returns an instance of the requested class. * 所谓好的IoC就是能static::get('啥都有')都能return正确的对象 */ public function get($class, $params = , $config = ) { # 1、对于单例(对象)来说,无须检查依赖和参数传递 if (isset($this->_singletons[$class])) { return $this->_singletons[$class]; # 2、好吧,首次创建这个对象 } elseif (!isset($this->_definitions[$class])) { return $this->build($class, $params, $config); } # 3、为什么已定义的对象不直接给返回? # 此定义非已经创建过对象这种定义,而是Container::set($class, $definition, $params)注册了一个$class而已,跟laravel的bind相像。 $definition = $this->_definitions[$class]; # 4、$definition为闭包函数: if (is_callable($definition, true)) { $params = $this->resolveDependencies($this->mergeParams($class, $params)); $object = call_user_func($definition, $this, $params, $config); # 5、$definition为数组: } elseif (is_array($definition)) { # 数组中必须要给出一个key为class的类名 $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); # 别名为什么要递归?而不是$this->build(concrete, $params, $config) # } else { $object = $this->get($concrete, $params, $config); } # 6、$definition为对象: } elseif (is_object($definition)) { return $this->_singletons[$class] = $definition; } else { throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition)); } # 更新单例记录的对象,取最后更新值。假设Foo::__construct($level = 0) {} # 当同一进程中有$this->_singletons['Foo'] = new Foo(1); # 现在Container::get('Foo') # 此时$this->_singletons[$class] = new Foo(0); if (array_key_exists($class, $this->_singletons)) { // singleton $this->_singletons[$class] = $object; } return $object; }
原文作者:花满树 分享原文链接:http://blog.huamanshu.com/?date=2015-06-26