PHP依赖注入容器【pimple】
<div id="MathJax_Message" style="display: none"></div><p></p><div class="wiz_toc_layer">安装<br>使用<br>定义服务<br>定义服务工厂<br>定义变量<br>保护变量<br>修改已定义的服务<br>扩展容器<br>获取服务创建方法<br>EasyWechat容器模式分析<br>获取容器<br>Factory做了什么?<br>Application做了什么?<br>ServiceContainer做了什么?<br>Container的register做了什么?<br>Provider到底做了什么?<br>调用时才实例化服务类</div>TOC <br>https://pimple.symfony.com/<p></p>
<h1 id="安装">安装</h1>
<p>通过composer:</p>
<div><pre><code>$ ./composer.phar require pimple/pimple ~3.0</code></pre></div>
<p>或者通过PHP的C扩展:</p>
<div><pre><code>$ git clone https://github.com/silexphp/Pimple
$ cd Pimple/ext/pimple
$ phpize
$ ./configure
$ make
$ make install</code></pre></div>
<h1 id="使用">使用</h1>
<p>创建容器</p>
<div><pre><code>use Pimple\Container;
$container = new Container();</code></pre></div>
<p>Pimple管理两种不同的数据:服务 和 变量。</p>
<h2 id="定义服务">定义服务</h2>
<p>在一个大型的系统中,服务是一个可以提供某些功能的对象。例如:数据库连接,模板引擎,邮件收发等。几乎所有的全局对象都可以当做一个服务。 <br>
服务可以由匿名函数定义,返回一个对象实例。</p>
<div><pre><code>// define some services
$container['session_storage'] = function ($c) {
return new SessionStorage('SESSION_ID');
};
$container['session'] = function ($c) {
return new Session($c['session_storage']);
};</code></pre></div>
<p>需要注意的是,匿名方法可以带上一个参数,这个参数可以访问当前容器实例,所以也就可以访问容器中其他服务或服务中的变量。 <br>
容器中的对象都是访问时才创建的,所以定义对象的顺序无关紧要。 <br>
使用以已经定义好的服务非常方便:</p>
<div><pre><code>// 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);</code></pre></div>
<h2 id="定义服务工厂">定义服务工厂</h2>
<p>默认情况下,每次你从Pimple获取到的服务都是同一个实例对象,如果你想要每次返回的都是不同的示例对象,那么需要将匿名方法包装在factory方法中:</p>
<div><pre><code>$container['session'] = $container->factory(function ($c) {
return new Session($c['session_storage']);
});</code></pre></div>
<p>现在,每次调用<code>$container['session']</code>都会返回不同的session实例对象了。</p>
<h2 id="定义变量">定义变量</h2>
<p>定义变量可以从外部简化你的容器配置,并能够保存全局变量:</p>
<div><pre><code>// define some parameters
$container['cookie_name'] = 'SESSION_ID';
$container['session_storage_class'] = 'SessionStorage';</code></pre></div>
<p>如果你像下面这样调整了<code>session_storage</code>服务的定义:</p>
<div><pre><code>$container['session_storage'] = function ($c) {
return new $c['session_storage_class']($c['cookie_name']);
};</code></pre></div>
<p>现在可以很方便的通过调整变量去动态实例化服务了,免去了从新定义一个新服务的麻烦。</p>
<h2 id="保护变量">保护变量</h2>
<p>因为Pimple会将匿名方法视为服务定义,所以定义实时变量(调用时才返回当时的值)的时候需要用protect()方法包装一下,这样会认为他们是变量,否则会认为是服务,这样会一直返回同一个值。</p>
<div><pre><code>$container['random_func'] = $container->protect(function () {
return rand();
});</code></pre></div>
<h2 id="修改已定义的服务">修改已定义的服务</h2>
<p>有时候你需要对已定义的服务进行修改,你可以使用extend()方法来新增代码扩展你的服务。</p>
<div><pre><code>$container['session_storage'] = function ($c) {
return new $c['session_storage_class']($c['cookie_name']);
};
$container->extend('session_storage', function ($storage, $c) {
$storage->...();
return $storage;
});</code></pre></div>
<p>第一个参数表示需要扩展的服务,第二个方法可以通过参数访问服务实例变量和容器。</p>
<h2 id="扩展容器">扩展容器</h2>
<p>如果你每次都使用一些类库,那么你也许会想在下一个项目也同样使用他们。你可以通过实现<code>Pimple\ServiceProviderInterface</code>接口把你的服务打包成provider:</p>
<div><pre><code>use Pimple\Container;
class FooProvider implements Pimple\ServiceProviderInterface
{
public function register(Container $pimple)
{
// register some services and parameters
// on $pimple
}
}</code></pre></div>
<p>然后,把provider注册到容器中。</p>
<div><pre><code>$pimple->register(new FooProvider());</code></pre></div>
<h2 id="获取服务创建方法">获取服务创建方法</h2>
<p>默认情况下,访问容器中的服务会自动为你创建实例,如果你想获取到这个服务创建实例的方法,你可以使用raw()方法:</p>
<div><pre><code>$container['session'] = function ($c) {
return new Session($c['session_storage']);
};
$sessionFunction = $container->raw('session');</code></pre></div>
<h1 id="EasyWechat容器模式分析">EasyWechat容器模式分析</h1>
<p>这个类库采用容器的方式调用,非常便捷,只需要实例化一个容器,内部的服务直接像调用方法一样调用所有的功能。</p>
<div><pre><code>src
├── Kernel 通用的核心类库,包括异常,http客户端,日志,消息体等。
├── BasicService 通用基础服务,包括jssdk,二维码生成,媒体上传等。
├── MicroMerchant小微企业服务
├── MiniProgram 小程序服务
├── OfficialAccount 公众号服务
├── OpenPlatform 开放平台服务
├── OpenWork 企业微信开放平台服务
├── Payment 微信支付服务
├── Work 企业微信服务
└── Factory.php 服务工厂,统一用来实例化容器</code></pre></div>
<p>这些目录的接口基本都类似【模块名】》【Client】+【Provider】 <br>
Client是实际的服务类,Provider是用来注册服务的,我们来看其中的基础模块</p>
<div><pre><code>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`</code></pre></div>
<p>其中根目录Application就是这个基础服务的容器,里面包括多个基础服务</p>
<h2 id="获取容器">获取容器</h2>
<p>容器统一通过Factory工厂类创建,这样进一步减少了依赖</p>
<div><pre><code>$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);</code></pre></div>
<p>然后,你就可以通过$app来直接调用了,不需要其他任何实例化。这就是容器的魅力,调用者可以只关心业务,而不用再去为管理实例化对象而烦恼,并且这样极大的降低了耦合度。</p>
<h2 id="Factory做了什么?">Factory做了什么?</h2>
<p>我们以<code>$app = Factory::officialAccount($config);</code>为例 <br>
1、通过魔法函数,静态调用方法实例化容器</p>
<blockquote>
<p>__callStatic()方法,从PHP5.3开始出现此方法,当创建一个静态方法以调用该类中不存在的一个方法时使用此方法。与__call()方法相同,接受方法名和数组作为参数。</p>
</blockquote>
<p>2、将officialAccount变成首字母大写,这里这么做是为了在调用的时候符合psr规范。 <br>
3、通过目录分析可知,每个服务目录下都有Application容器类,所以我们只需要知道服务目录就可以实例化容器了。</p>
<div><pre><code> 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);
}</code></pre></div>
<h2 id="Application做了什么?">Application做了什么?</h2>
<p>Application继承自ServiceContainer,Application类只是重写了$providers变量,这个变量保存了这个容器中会用到的服务类</p>
<div><pre><code>protected $providers = [
Auth\ServiceProvider::class,
Server\ServiceProvider::class,
User\ServiceProvider::class,
OAuth\ServiceProvider::class,
……
]</code></pre></div>
<p>具体实例化的业务逻辑,还要看父类是如何操作的,我们继续往下看。</p>
<h2 id="ServiceContainer做了什么?">ServiceContainer做了什么?</h2>
<p>ServiceContainer继承自Pimple的Container,对基础容器类一些针对微信的变量方法扩展。 <br>
先看代码上注释。</p>
<div><pre><code>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方法”注册“服务,
}
}
}</code></pre></div>
<p>从代码可以看出,ServiceContainer主要为我们做了如下几件事: <br>
1、重写__get和__set魔法函数,这样我们就可以通过'$app->menu'来取代<code>$app['menu']</code>的方式,更加符合面向对象开发。另外结合@property注释,编辑器可以实现代码提示。 <br>
2、合并了一些基础服务Provider <br>
3、加入了事件通知 <br>
4、保存用户配置,为了方便后面的业务直接调用。</p>
<p>最后调用父级的register函数进行服务注册。</p>
<h2 id="Container的register做了什么?">Container的register做了什么?</h2>
<div><pre><code> public function register(ServiceProviderInterface $provider, array $values = array())
{
$provider->register($this);//调用Provider的register函数真正的注册服务
foreach ($values as $key => $value) {
$this[$key] = $value;
}
return $this;
}</code></pre></div>
<h2 id="Provider到底做了什么?">Provider到底做了什么?</h2>
<p>我们以ServiceProvider为例,看看provider到底做了什么。 <br>
很简单,通过传参,实际上就是调用了Container的offsetSet方法,把实例化服务的方法赋值给一个函数,只有在调用的时候才会真正执行实例化。</p>
<div><pre><code>class ServiceProvider implements ServiceProviderInterface
{
public function register(Container $app)
{
$app['template_message'] = function ($app) {
return new Client($app);
};
}
}</code></pre></div>
<p>那我们再看看offsetSet方法做了什么</p>
<div><pre><code> 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是否存在
}</code></pre></div>
<h2 id="调用时才实例化服务类">调用时才实例化服务类</h2>
<p>了解了上述的过程,那么最后一步就是在需要调用再实例化服务类了,是如何做到的?我们看Container中的offsetGet方法</p>
<div><pre><code> 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;
}</code></pre></div>
<p>从代码可知,因为provider返回的是一个匿名函数,用来实例化对象,所以这里在用到的时候调用一下匿名函数,然后保存实例化后的对象,下次直接返回即可。 <br>
这样整个流程就走完啦。</p><wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none"><div id="wiz-table-col-line" style="display: none"></div><div id="wiz-table-row-line" style="display: none"></div><div id="wiz-table-range-border_start" style="display: none"><div id="wiz-table-range-border_start_top"></div><div id="wiz-table-range-border_start_right"></div><div id="wiz-table-range-border_start_bottom"></div><div id="wiz-table-range-border_start_left"></div><div id="wiz-table-range-border_start_dot"></div></div><div id="wiz-table-range-border_range" style="display: none"><div id="wiz-table-range-border_range_top"></div><div id="wiz-table-range-border_range_right"></div><div id="wiz-table-range-border_range_bottom"></div><div id="wiz-table-range-border_range_left"></div><div id="wiz-table-range-border_range_dot"></div></div></wiz_tmp_tag>
</div>
<div id="MySignature" role="contentinfo">
我不怕千万人阻挡,只怕自己投降。<br><br>
来源:https://www.cnblogs.com/leestar54/p/12669123.html
頁:
[1]