• 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 PimpleContainer;
    $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;
    });

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

    扩展容器

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

    use PimpleContainer;
    
    
    class FooProvider implements PimpleServiceProviderInterface
    {
        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 = KernelSupportStr::studly($name); //Convert a value to studly caps case.
            $application = "\\EasyWeChat\{$namespace}\Application";
            return new $application($config);
        }

    Application做了什么?

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

    protected $providers = [
            AuthServiceProvider::class,
            ServerServiceProvider::class,
            UserServiceProvider::class,
            OAuthServiceProvider::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 EventsApplicationInitialized($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返回的是一个匿名函数,用来实例化对象,所以这里在用到的时候调用一下匿名函数,然后保存实例化后的对象,下次直接返回即可。
    这样整个流程就走完啦。

  • 相关阅读:
    EXP8
    EXP7
    数据库作业
    windows下如何编译运行龙脉代码
    CVE-2019-6340 Drupal8's REST RCE 漏洞复现
    小黄衫获奖感言
    Exp6 MSF应用基础
    Exp5
    实验一 密码引擎-4-国䀄算法交叉测试(选做)
    2020-2021-2 20175335 丹增罗布 《网络对抗技术》Exp1 PC平台逆向破解
  • 原文地址:https://www.cnblogs.com/leestar54/p/12669123.html
Copyright © 2020-2023  润新知