PHP依赖注入容器【pimple】
使用
定义服务
定义服务工厂
定义变量
保护变量
修改已定义的服务
扩展容器
获取服务创建方法
EasyWechat容器模式分析
获取容器
Factory做了什么?
Application做了什么?
ServiceContainer做了什么?
Container的register做了什么?
Provider到底做了什么?
调用时才实例化服务类
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返回的是一个匿名函数,用来实例化对象,所以这里在用到的时候调用一下匿名函数,然后保存实例化后的对象,下次直接返回即可。
这样整个流程就走完啦。