PHP依赖注入容器【pimple】

TOC
https://pimple.symfony.com/

安装

通过composer:

$ ./composer.phar require pimple/pimple ~3.0

或者通过PHP的C扩展:

$ git clone https://github.com/silexphp/Pimple
$ cd Pimple/ext/pimple
$ phpize
$ ./configure
$ make
$ make install

使用

创建容器

use Pimple\Container;
$container = new Container();

Pimple管理两种不同的数据:服务 和 变量。

定义服务

在一个大型的系统中,服务是一个可以提供某些功能的对象。例如:数据库连接,模板引擎,邮件收发等。几乎所有的全局对象都可以当做一个服务。
服务可以由匿名函数定义,返回一个对象实例。

// define some services
$container['session_storage'] = function ($c) {
    return new SessionStorage('SESSION_ID');
};


$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

需要注意的是,匿名方法可以带上一个参数,这个参数可以访问当前容器实例,所以也就可以访问容器中其他服务或服务中的变量。
容器中的对象都是访问时才创建的,所以定义对象的顺序无关紧要。
使用以已经定义好的服务非常方便:

// get the session object
$session = $container['session'];


// the above call is roughly equivalent to the following code:
// $storage = new SessionStorage('SESSION_ID');
// $session = new Session($storage);

定义服务工厂

默认情况下,每次你从Pimple获取到的服务都是同一个实例对象,如果你想要每次返回的都是不同的示例对象,那么需要将匿名方法包装在factory方法中:

$container['session'] = $container->factory(function ($c) {
    return new Session($c['session_storage']);
});

现在,每次调用$container['session']都会返回不同的session实例对象了。

定义变量

定义变量可以从外部简化你的容器配置,并能够保存全局变量:

// define some parameters
$container['cookie_name'] = 'SESSION_ID';
$container['session_storage_class'] = 'SessionStorage';

如果你像下面这样调整了session_storage服务的定义:

$container['session_storage'] = function ($c) {
    return new $c['session_storage_class']($c['cookie_name']);
};

现在可以很方便的通过调整变量去动态实例化服务了,免去了从新定义一个新服务的麻烦。

保护变量

因为Pimple会将匿名方法视为服务定义,所以定义实时变量(调用时才返回当时的值)的时候需要用protect()方法包装一下,这样会认为他们是变量,否则会认为是服务,这样会一直返回同一个值。

$container['random_func'] = $container->protect(function () {
    return rand();
});

修改已定义的服务

有时候你需要对已定义的服务进行修改,你可以使用extend()方法来新增代码扩展你的服务。

$container['session_storage'] = function ($c) {
    return new $c['session_storage_class']($c['cookie_name']);
};


$container->extend('session_storage', function ($storage, $c) {
    $storage->...();
    return $storage;
});

第一个参数表示需要扩展的服务,第二个方法可以通过参数访问服务实例变量和容器。

扩展容器

如果你每次都使用一些类库,那么你也许会想在下一个项目也同样使用他们。你可以通过实现Pimple\ServiceProviderInterface接口把你的服务打包成provider:

use Pimple\Container;


class FooProvider implements Pimple\ServiceProviderInterface
{
    public function register(Container $pimple)
    {
        // register some services and parameters
        // on $pimple
    }
}

然后,把provider注册到容器中。

$pimple->register(new FooProvider());

获取服务创建方法

默认情况下,访问容器中的服务会自动为你创建实例,如果你想获取到这个服务创建实例的方法,你可以使用raw()方法:

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};


$sessionFunction = $container->raw('session');

EasyWechat容器模式分析

这个类库采用容器的方式调用,非常便捷,只需要实例化一个容器,内部的服务直接像调用方法一样调用所有的功能。

src
 ├── Kernel 通用的核心类库,包括异常,http客户端,日志,消息体等。
 ├── BasicService 通用基础服务,包括jssdk,二维码生成,媒体上传等。
 ├── MicroMerchant  小微企业服务
 ├── MiniProgram 小程序服务
 ├── OfficialAccount 公众号服务
 ├── OpenPlatform 开放平台服务
 ├── OpenWork 企业微信开放平台服务
 ├── Payment 微信支付服务
 ├── Work 企业微信服务
 └── Factory.php 服务工厂,统一用来实例化容器

这些目录的接口基本都类似【模块名】》【Client】+【Provider】
Client是实际的服务类,Provider是用来注册服务的,我们来看其中的基础模块

BasicService
 ├── Url
 │ ├── ServiceProvider.php
 │ └── Client.php
 ├── QrCode
 │ ├── ServiceProvider.php
 │ └── Client.php
 ├── Media
 │ ├── ServiceProvider.php
 │ └── Client.php
 ├── Jssdk
 │ ├── ServiceProvider.php
 │ └── Client.php
 ├── ContentSecurity
 │ ├── ServiceProvider.php
 │ └── Client.php
 └── Application.php`

其中根目录Application就是这个基础服务的容器,里面包括多个基础服务

获取容器

容器统一通过Factory工厂类创建,这样进一步减少了依赖

$app = Factory::officialAccount($config);
$app = Factory::payment($config);
$app = Factory::miniProgram($config);
$openPlatform = Factory::openPlatform($config);
$app = Factory::work($config);
$app = Factory::openWork($config);
$app = Factory::microMerchant($config);

然后,你就可以通过$app来直接调用了,不需要其他任何实例化。这就是容器的魅力,调用者可以只关心业务,而不用再去为管理实例化对象而烦恼,并且这样极大的降低了耦合度。

Factory做了什么?

我们以$app = Factory::officialAccount($config);为例
1、通过魔法函数,静态调用方法实例化容器

__callStatic()方法,从PHP5.3开始出现此方法,当创建一个静态方法以调用该类中不存在的一个方法时使用此方法。与__call()方法相同,接受方法名和数组作为参数。

2、将officialAccount变成首字母大写,这里这么做是为了在调用的时候符合psr规范。
3、通过目录分析可知,每个服务目录下都有Application容器类,所以我们只需要知道服务目录就可以实例化容器了。

    public static function __callStatic($name, $arguments)
    {
        return self::make($name, ...$arguments);
    }


    public static function make($name, array $config)
    {
        $namespace = Kernel\Support\Str::studly($name); //Convert a value to studly caps case.
        $application = "\\\\EasyWeChat\\{$namespace}\\Application";
        return new $application($config);
    }

Application做了什么?

Application继承自ServiceContainer,Application类只是重写了$providers变量,这个变量保存了这个容器中会用到的服务类

protected $providers = [
        Auth\ServiceProvider::class,
        Server\ServiceProvider::class,
        User\ServiceProvider::class,
        OAuth\ServiceProvider::class,
        ……

]

具体实例化的业务逻辑,还要看父类是如何操作的,我们继续往下看。

ServiceContainer做了什么?

ServiceContainer继承自Pimple的Container,对基础容器类一些针对微信的变量方法扩展。
先看代码上注释。

class ServiceContainer extends Container
{
    use WithAggregator;//代码复用特性


    protected $id;//服务名称,这里没有用到这个变量,Provider内部都已经设置了name。
    protected $providers = [];//服务提供者变量
    protected $defaultConfig = [];//默认配置变量
    protected $userConfig = [];//用户的配置变量


    /**
     * Constructor.
     *
     * @param array $config
     * @param array $prepends
     * @param string|null $id
     */
    public function __construct(array $config = [], array $prepends = [], string $id = null)
    {
        $this->registerProviders($this->getProviders());//注册由Provider提供的服务
        parent::__construct($prepends);//默认情况下,容器可以预先传递一个对象或变量数组
        $this->userConfig = $config;//获取用户传递的配置
        $this->id = $id;//默认为null
        $this->aggregate();//WithAggregator中的方法,设置默认配置
        $this->events->dispatch(new Events\ApplicationInitialized($this));//初始化完成事件分发,这里的events也是服务,在registerProviders完成了注册,所以这里可以直接调用了。
    }


    public function getId()
    {
        return $this->id ?? $this->id = md5(json_encode($this->userConfig));//如果id为null,那么使用用户配置来MD5算出id
    }
    public function getConfig()

    {
        $base = [
            // http://docs.guzzlephp.org/en/stable/request-options.html
            'http' => [
                'timeout' => 30.0,
                'base_uri' => 'https://api.weixin.qq.com/',
            ],
        ];
        return array_replace_recursive($base, $this->defaultConfig, $this->userConfig);//从后往前迭代覆盖前面相同key的数组值
    }


    /**
     * Return all providers.
     *
     * @return array
     */
    public function getProviders()
    {
        return array_merge([
            ConfigServiceProvider::class,
            LogServiceProvider::class,
            RequestServiceProvider::class,
            HttpClientServiceProvider::class,
            ExtensionServiceProvider::class,
            EventDispatcherServiceProvider::class,
        ], $this->providers);//返回合并后的Provider,这里默认有几个核心服务Provider
    }


    /**
     * @param string $id
     * @param mixed $value
     */
    public function rebind($id, $value)
    {
        $this->offsetUnset($id);//重新绑定服务
        $this->offsetSet($id, $value);
    }


    /**
     * Magic get access. 魔法函数,这样就可以以对象的形式去获取数组值了。
     * @param string $id
     * @return mixed
     */
    public function __get($id)
    {
        if ($this->shouldDelegate($id)) {
            return $this->delegateTo($id);//EasyWechat的代理方法,暂时不理。
        }


        return $this->offsetGet($id);
    }


    /**
     * Magic set access.魔法函数,这样就可以通过对象形式设置数组了
     * @param string $id
     * @param mixed $value
     */
    public function __set($id, $value)
    {
        $this->offsetSet($id, $value);
    }


    /**
     * @param array $providers
     */
    public function registerProviders(array $providers)
    {
        foreach ($providers as $provider) {
            parent::register(new $provider());//调用Container的Register方法”注册“服务,
        }
    }
}

从代码可以看出,ServiceContainer主要为我们做了如下几件事:
1、重写__get和__set魔法函数,这样我们就可以通过'$app->menu'来取代$app['menu']的方式,更加符合面向对象开发。另外结合@property注释,编辑器可以实现代码提示。
2、合并了一些基础服务Provider
3、加入了事件通知
4、保存用户配置,为了方便后面的业务直接调用。

最后调用父级的register函数进行服务注册。

Container的register做了什么?

    public function register(ServiceProviderInterface $provider, array $values = array())
    {
        $provider->register($this);//调用Provider的register函数真正的注册服务
        foreach ($values as $key => $value) {
            $this[$key] = $value;
        }
        return $this;

    }

Provider到底做了什么?

我们以ServiceProvider为例,看看provider到底做了什么。
很简单,通过传参,实际上就是调用了Container的offsetSet方法,把实例化服务的方法赋值给一个函数,只有在调用的时候才会真正执行实例化。

class ServiceProvider implements ServiceProviderInterface
{
    public function register(Container $app)
    {
        $app['template_message'] = function ($app) {
            return new Client($app);
        };
    }
}

那我们再看看offsetSet方法做了什么

    public function offsetSet($id, $value)
    {
        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }
        $this->values[$id] = $value;//用来保存实例化匿名函数,结合上文这里$value=匿名实例化函数,$id=template_message
        $this->keys[$id] = true;//用来判断id是否存在
    }

调用时才实例化服务类

了解了上述的过程,那么最后一步就是在需要调用再实例化服务类了,是如何做到的?我们看Container中的offsetGet方法

    public function offsetGet($id)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);//如果id不存在,说明没有赋值,报错
        }


        if (
            isset($this->raw[$id])//如果已经实例化,raw会被赋值原匿名函数
            || !\is_object($this->values[$id])//如果值不是对象
            || isset($this->protected[$this->values[$id]])//如果设置了保护变量
            || !\method_exists($this->values[$id], '__invoke')//如果不是调用的方法
        ) {
            return $this->values[$id];//那么就返回值
        }


        if (isset($this->factories[$this->values[$id]])) {//如果定义了工厂类服务
            return $this->values[$id]($this);//那么每次都返回不一样的对象(这里实时实例化)
        }


        $raw = $this->values[$id];//raw赋值为匿名函数
        $val = $this->values[$id] = $raw($this);//调用匿名函数实例化服务类
        $this->raw[$id] = $raw;//raw数组保存当前匿名函数


        $this->frozen[$id] = true;//实例化完成,冻结服务,禁止再次实例化。


        return $val;
    }

从代码可知,因为provider返回的是一个匿名函数,用来实例化对象,所以这里在用到的时候调用一下匿名函数,然后保存实例化后的对象,下次直接返回即可。
这样整个流程就走完啦。

posted @ 2020-04-09 19:53  leestar54  阅读(1142)  评论(1编辑  收藏  举报