彩云之南土岩 發表於 2020-4-9 19:53:00

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


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

    {
      $base = [
            // http://docs.guzzlephp.org/en/stable/request-options.html
            'http' =&gt; [
                'timeout' =&gt; 30.0,
                'base_uri' =&gt; 'https://api.weixin.qq.com/',
            ],
      ];
      return array_replace_recursive($base, $this-&gt;defaultConfig, $this-&gt;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-&gt;providers);//返回合并后的Provider,这里默认有几个核心服务Provider
    }


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


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


      return $this-&gt;offsetGet($id);
    }


    /**
   * Magic set access.魔法函数,这样就可以通过对象形式设置数组了
   * @param string $id
   * @param mixed $value
   */
    public function __set($id, $value)
    {
      $this-&gt;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-&gt;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-&gt;register($this);//调用Provider的register函数真正的注册服务
      foreach ($values as $key =&gt; $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-&gt;frozen[$id])) {
            throw new FrozenServiceException($id);
      }
      $this-&gt;values[$id] = $value;//用来保存实例化匿名函数,结合上文这里$value=匿名实例化函数,$id=template_message
      $this-&gt;keys[$id] = true;//用来判断id是否存在
    }</code></pre></div>

<h2 id="调用时才实例化服务类">调用时才实例化服务类</h2>

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

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


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


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


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


      $this-&gt;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]
查看完整版本: PHP依赖注入容器【pimple】