对laravel和yii服务容器的理解

在当今流行的现代化php框架中,容器的概念已经非常普及,比如yii,lavavel的设计都用到了容器。什么是容器呢,它提供了整个框架运行过程中所需要的一系列服务,其实通俗点讲,就是装东西的盒子,框架中常见的类,对象,配置等都装在这个盒子里,在程序运行的过程中,动态的为系统提供所需要的服务,这就是容器的作用。

laravel中的容器

laravel的核心其实就是一个容器,学名称为Ioc容器,官方文档称其为“服务容器”,这里涉及到一个重要的概念:依赖注入。你千万不要被这名字给唬住了,我们直接来一个例子作为说明。

Interface Power
{
    public function fight();
}

class FlyPower implements Power
{
    public function fight()
    {
        echo "我是飞行能力" . PHP_EOL;
    }
}

class FirePower implements Power
{
    public function fight()
    {
        echo "我是开火能力" . PHP_EOL;
    }
}

class XrayPower implements Power
{
    public function fight()
    {
        echo "我是x光线能力" . PHP_EOL;
    }
}



class Superman
{
    private $power;

    public function __construct(Power $power)
    {
        $this->power = $power;
    }

    public function go()
    {
        $this->power->fight();
    }
}

$power1 = new FlyPower();
$power2 = new FirePower();
$power3 = new XrayPower();
$superman1 = new Superman($power1);
$superman1->go();
$superman2 = new Superman($power2);
$superman2->go();
$superman3 = new Superman($power3);
$superman3->go();

在这个例子中,SuperMan的构造方法需要一个参数,该参数必须实现Power接口,而这个参数是我们在类的外部进行实例化后传入的,只要是Power的实现就都有效,所以相当于就把power注入到SuperMan中,这就是依赖注入。

这里我们只是简单了描述了依赖注入的原理,下面我们用代码简单实现一个我们自己的容器:

class Container
{
    protected $binds;

    protected $instances;

    public function bind($abstract, $concrete)
    {
        if ($concrete instanceof Closure) {
            $this->binds[$abstract] = $concrete;
        } else {
            $this->instances[$abstract] = $concrete;
        }
    }

    public function make($abstract, $parameters = [])
    {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        array_unshift($parameters, $this);

        return call_user_func_array($this->binds[$abstract], $parameters);
    }
}


// 创建容器
$container = new Container();
// bind方法:第一个参数可以理解为要绑定的key,别名,第二个参数可以是一个回调函数,也可以是一个实例对象
$container->bind('flypower', function ($container) {
    return new FlyPower();
});
// bind的第二个参数为回调函数的情况
$container->bind('firepower', new FirePower());
// bind的第二个参数为实例对象的情况
$container->bind('superman', function ($container, $power) {
    return new Superman($container->make($power));
});

$superman1 = $container->make('superman', ['flypower']);
$superman2 = $container->make('superman', ['firepower']);
$superman1->go();// 输出:我是飞行能力
$superman2->go();// 输出:我是开火能力

上述例子是我们通过自己的理解实现的一个非常简陋的容器,在laravel中,依赖注入是通过Ioc容器来实现的,而且自动化,更高效,省去了我们手动方式去注入依赖,在Ioc容器中全部自动帮你搞定。我们先来模仿laravel来实现自己的容器,代码如下:

class Container
{
    protected $bindings = [];

    public function bind($abstract, $concrete = null, $shared = false)
    {
        if (!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }
        $this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];
    }


    protected function getClosure($abstract, $concrete)
    {
        return function ($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? "build" : "make";
            return $c->$method($concrete);
        };
    }

    public function make($abstract)
    {
        //这里的注释在调试时,建议打开,可以直观的跟踪程序的执行顺序和流程
        //static $i = 1;
        //echo "make-".$i++.PHP_EOL;

        $concrete = $this->getConcrete($abstract);

        if ($this->isBuildable($concrete, $abstract)) {

            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
        return $object;
    }

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }


    protected function getConcrete($abstract)
    {
        if (!isset($this->bindings[$abstract])) {
            return $abstract;
        }
        return $this->bindings[$abstract]['concrete'];
    }


    public function build($concrete)
    {
        //这里的注释在调试时,建议打开,可以直观的跟踪程序的执行顺序和流程
        //static $i = 1;
        //echo "build-".$i++.PHP_EOL;

        if ($concrete instanceof Closure) {
            return $concrete($this);
        }

        $ref = new ReflectionClass($concrete);

        if (!$ref->isInstantiable()) {
            echo "The $concrete is not instantiable.";
            exit;
        }
        $constructor = $ref->getConstructor();
        if (is_null($constructor)) {
            return new $concrete;
        }
        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);
        return $ref->newInstanceArgs($instances);
    }


    protected function getDependencies($parameters)
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if (is_null($dependency)) {
                $dependencies[] = null;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return (array)$dependencies;

    }

    protected function resolveClass(ReflectionParameter $parameter)
    {
        return $this->make($parameter->getClass()->name);
    }
}

$c = new Container();
$c->bind("Power", "FlyPower");
$c->bind("superman1", "Superman");
$superman1 = $c->make("superman1");
//$c->bind("Superman","Superman");
//$superman1 = $c->make("Superman");
$superman1->go(); //输出:我是飞行能力

调试本代码的时候,建议读者朋友们打开在build()方法和make()方法中的注释部分,可以更加直观的看到make()和build()是如何被调用的,什么时候被调用,一共调用了多少次,窃以为laravel作者在这里的调用有些繁琐,本可以更快捷方便的实现,稍后我会把自己认为比较完美的代码贴出来。

yii中的容器

看完laravel的容器,相信读者们已经都或多或少对容器有了一定的认知,其实目前市面上的php框架都是融入了容器的设计理念的,可以这样说,一个现代化的框架一定是离不开容器的,那么我们不妨来看看php界另一个重量级框架——yii,是如何实现自己的容器的。

在yii的容器中,Container类维护了5个数组,这是功能实现的基础:

private $_singletons = []; //用于保存单例 Singleton 对象,以对象类型为键
private $_definitions = []; //用于保存依赖的定义,以对象类型为键
private $_params = []; //用于保存构造函数的参数,以对象类型为键
private $_reflections = []; //用于缓存 ReflectionClass 对象,以类名或接口名为键
private $_dependencies = []; //用于缓存依赖信息,以类名或接口名为键

在yii的container中,是通过Container->set()方法来注册依赖的,用法简单做一下说明:

// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并供有依赖需要的类使用
// $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 注册一个别名,当调用 $container->get('foo') 时,可以得到一个yii\db\Connection实例
// $_definition['foo', 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');

// 用一个配置数组来注册一个类,需要这个类的实例时,这个配置数组会发生作用
// $_definition['yii\db\Connection'] = [...]
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一个配置数组来注册一个别名,由于别名的类型不详,因此配置数组中需要有class元素
// $_definition['db'] = [...]
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一个 PHP callable 来注册一个别名,每次引用这个别名时,这个 callable 都会被调用
// $_definition['db'] = function(...){...}
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// 用一个对象来注册一个别名,每次引用这个别名时,这个对象都会被引用
// $_definition['pageCache'] = anInstanceOfFileCache
$container->set('pageCache', new FileCache);

容器中对象的获取是通过get()方法:

  • Container->get()用于返回一个对象或一个别名所代表的对象,可以是已经注册好依赖的,也可以是没有注册过依赖的。无论是哪种情况,Yii 均会自动解析将要获取的对象对外部的依赖。

  • get()解析依赖获取对象是一个自动递归的过程,也就是说,当要获取的对象依赖于其他对象时,Yii会自动获取这些对象及其所依赖的下层对象的实例。同时,即使对于未定义的依赖,DI 容器通过PHP 的Reflection机制,也可以自动解析出当前对象的依赖来。

  • get()不直接实例化对象,也不直接解析依赖信息,而是通过build()来实例化对象和解析依赖,这点跟laravel容器的实现机制其实是一模一样的。

我们仍然用最上面Superman的例子,看一下在yii的容器里是如何操作的(该例子只是为了方便理解,没有考虑命名空间的情况):

$container = new Container();               //创建容器
$container->set("Power", "FlyPower");       //在需要使用接口Power时,用FlyPower类来作为实现
$container->set("superman2", "Superman");   //为Superman类定义一个别名
$superman2 = $container->get("superman2");  //获取容器里别名对应的对象

看到了吗,是不是和laravel的容器如出一撤,laravel中容器的bind()方法对应yii中容器的set()方法,laravel中容器的make()方法对应yii中容器的get()方法。

需要特别关注的一点是,yii引入了服务定位器(Service Locator)作为容器的升华(自己的理解),你可千万不要被这个名字吓到了,服务定位器并不是什么高大上的玩意,它背后的实现其实都离不开容器,我对yii的服务定位器作一个我自己认为比较简单的描述:

在网站程序启动时,把所有程序运行必要的组件,配置都放入服务定位器中,注意这里只是放入(其实就是调用了ServiceLocator::set()方法),当程序使用到这个组件的时候,再去取出来(调用ServiceLocator::get()方法),这个取的过程需要容器container的配合,因为要解析依赖,该实例化的要实例化,就是这么简单。

Yii容器和Service Locator的工作流程大致如下:

  1. Yii类提供了一个静态的$container成员变量用于引用 DI 容器。在入口脚本中,会创建一个 DI 容器,并赋值给这个 $container。

  2. Service Locator通过Yii::createObject()来获取实例,而这个Yii::createObject()是调用了DI容器的yii\di\Container::get()来向Yii::$container索要实例的。因此,Service Locator最终是通过DI容器来创建和获取实例的。

关于服务定位器的示例代码:

//创建Service Locator
$serviceLocator = new yii\di\ServiceLocator();

//注册一个服务
$serviceLocator->set('superman3', [
    'class' => 'Superman',
    //'config' => [],//如果需要其他配置,这个数组就用得上
]);

//服务定位器里服务的访问方式有两种:
//1.使用访问属性的方法访问这个服务
$serviceLocator->superman3->go();
//2.使用服务定位器的get()方法访问这个服务
$serviceLocator->get('superman3')->go();

最后,做一个总结

因为这篇文章是专门讲容器的,所以并不会从其他方面分析框架的优劣,只想从容器实现的角度来说一下我对laravel和yii的见解。虽然laravel和yii在容器实现上如出一辙,但是窃以为yii的实现表现得更有条理,也更好理解,而且在注册依赖的时候,可以支持数组,回调函数,类名等,显得更为强大,服务定位器的引入,从操作和表现来看,更为直接和语义化。当然这只是我的理解,因为我对laravel其实也不是非常熟悉,说的不对的地方,大家轻喷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章