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()[/]
再细看Container::get($class, $params = , $config = )如何实现服务容器的。三个参数里config其实对于BaseYii::createObject()是暂时没用的,先关注前面两个参数。
# 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

0 个评论

要回复文章请先登录注册