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返回的是一個匿名函數,用來實例化對象,所以這里在用到的時候調用一下匿名函數,然后保存實例化后的對象,下次直接返回即可。
這樣整個流程就走完啦。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM