Yii2是一个设计简单,灵活性高,容易上手的MVC框架。它的生态所有欠缺,所以很需要理解框架,然后DIY。很希望Yii3能早日发布。

使用Yii2也有了几年,在理解控制反转/IoC的概念之后,再结合框架yii\base\BaseObjectyii\base\Component的源码解读,对框架又有了更深清晰的认识。

首先,不必太纠结于程序设计的各种概念,比如这里的IoC。因为在编写代码的过程中,可能已经在无意中已经用到了相关思想。这些概念只是对编写的代码做了抽象的总结。但是呢,理解设计概念,更容易理解大块的代码逻辑。

控制反转/Inversion of Control(IoC)

我对控制反转的理解是:一个对象所需的依赖不由自己内部主动创建,而由外部提供。这个由内而外的变化,就是反转。发生发转的是所需依赖的获取过程。用以下简单的代码示例来辅助理解。

interface Hello
{
    public function hello();
}
class A implements Hello
{
    public function hello() {
        echo 'a';
    }
}
class B implements Hello
{
    public function hello()
    {
        echo 'b';
    }
}

// 常规方式(未采用控制反转设计)
class H
{
    protected $obj;
    public function __construct()
    {
        $this->obj = new A(); // 所需依赖由H类自己创建
    }
    public function run() {
        $this->obj->hello();
    }
}
// 控制反转-依赖注入方式设计
class I
{
    protected $obj;
    public function __construct(Hello $obj)
    {
        // 所需依赖由外部提供
        $this->obj = $obj;
    }
    public function run()
    {
        $this->obj->hello();
    }
}

由上可见,不管给I类提供AB类的对象,甚至其他Hello接口的实现,都能正常工作。而H类与A类强耦合,无法直接替换依赖。代入一个简单场景进行理解:HI是用来给用户发通知的,有AB两种通知方式(如:QQ、微信)。H就只能通过A方式来发送,I却可以灵活地使用多种方式。I类的代码逻辑并没有很复杂,却是通过依赖注入方式实现控制反转的典型方式。因此我才觉得无需太纠结于相关概念。

依赖注入的时候需要外部提供依赖,依赖注入容器/Dependency Injection Container往往是提供所需依赖项的角色,它定义了各依赖项的实现。以下是一个简单的依赖注入容器示例。

class DiContainer
{
    protected $definitions = [];

    public function set($id, $def)
    {
        $this->definitions[$id] = $def;
    }

    public function get($id, $params = [])
    {
        if (!isset($this->definitions[$id])) {
            throw new Exception("$id not defined");
        }

        $def = $this->definitions[$id];
        return new $def(...$params);
    }
}

结合上文的示例,使用场景演示代码

// 应用启动时
$container = new DiContainer(); // 往往是个单例
$container->set(Hello::class, A::class);
$container->set('my-id', I::class);

// 应用运行时
$helloObj = $container->get(Hello::class);
$myObj = $container->get('my-id', [$helloObj]);
$myObj->run();

其实,当DiContainer使用PHP的反射特性时,根据I类的构造参数,主动解析出一个Hello实例,进一步实例化出my-id的对象。此时,就不需要我们显式地获取Hello对应实例,然后传递给my-id的构造函数了。

Yii2中的依赖注入容器是yii\di\Container,它的setget方法更加强大,支持字符串、数组、闭包等多种定义方式,并能递归解析各个对象的依赖项。Yii::createObject是对yii\di\Container的封装。

此外,Yii2中使用yii\di\ServiceLocator对依赖注入容器进一步封装,扩展了组件了定义功能。Yii2的yii\base\Application继承自yii\base\Module模块,是yii\di\ServiceLocator的子类。得益于此,通过相关web.php等配置文件,即可定义系统组件。

BaseObject和Component

Yii2框架几乎所有类都是BaseObject的子类,它通过__set__get等魔术方法,扩展了类属性的赋值和获取。同时实现了yii\base\Configurable接口(虽然是空接口),使类获得了强大的配置能力,可以通过Yii::configure填充类属性。

ComponentBaseObject的子类,增加了事件的定义和触发逻辑,并引入了行为功能(是一种动态的混入,扩展了原类的功能)。并扩充了__set__get__call等魔术方法,使其能够把行为所具有的属性、方法作为自身的接口对外服务。

示例代码

class RunComponent extends yii\base\Component {
    const AFTER_RUN = 'afterRun';
    public function run()
    {
        $this->trigger(static::AFTER_RUN);
    }
}
class HiBehavior extends yii\base\Behavior {
    public $message = 'hi';
    public function events()
    {
        return [
            RunComponent::AFTER_RUN => 'bye',
        ];
    }
    public function hi()
    {
        echo $this->message;
    }
    public function bye()
    {
        echo 886;
    }
    public function getMsg()
    {
        return $this->message;
    }
    public function setMsg($value)
    {
        $this->message = $value;
    }
}

$runner = Yii::createObject([
    'class' => RunComponent::class,
    'as hi' => [
        'class' => HiBehavior::class,
        'msg' => 'hello',
    ],
    'on afterRun' => function () {
        echo 'event';
    },
]);
echo $runner->message;  // hello
echo $runner->msg;      // hello
$runner->msg = 'hi';
$runner->hi();          // hi
$runner->run();         // 886 event
$runner->bye();         // 886

理解了以上内容后,回头再看web.phpconsole.php里的内容,茅塞顿开,神清气爽。这个配置文件配置的是Application类的属性,其中components属性又是各个组件的定义和属性配置。从此,新世界的门大开。

参考文章