Symfony 服務容器效能優化

Symfony 服務容器效能優化

本文首發於 Symfony 服務容器效能優化,轉載請註明出處。

本文是依賴注入(Depeendency Injection)系列教程的最後一篇文章,本系列教程主要講解如何使用 PHP 實現一個輕量級服務容器,教程包括:

第 1 篇:什麼是依賴注入?
第 2 篇:是否需要使用依賴注入容器?
第 3 篇:Symfony 服務容器入門
第 4 篇:Symfony 服務容器:使用建造者建立服務
第 5 篇:Symfony 服務容器:使用 XML 或 YAML 檔案描述服務
第 6 篇:Symfony 服務容器效能優化


術語

Depeendency Injection 譯作 依賴注入

Depeendency Injection Container 譯作 依賴注入容器

Container 譯作 容器

Service Container 譯作 服務容器

Session 譯作 會話

Object-Oriented 譯作 物件導向

mock 譯作 模擬

anti-patterns 譯作 反模式

hardcoded 譯作 硬編碼

dumper 譯作 轉存器

loader 譯作 載入器


正文

在本系列關於依賴注入的前五篇文章中,我們逐步介紹了這個簡單實用的設計模式背後的主要概念。我們還談到了一個將用於 Symfony 2 的輕量級 PHP 容器的實現。

但隨著 XML 和 YAML 配置檔案的引入,您可能會對容器本身的效能產生懷疑。即使服務是延遲載入,在每個請求中讀取一堆 XML 或 YAML 檔案,並通過使用自省(Introspection)來建立物件在 PHP 中可能效率不高。由於容器幾乎是應用程式的基石,它的速度確實很重要。

一方面,使用 XML 或 YAML 來描述服務及其配置是非常強大和靈活的:

<container xmlns="http://symfony-project.org/2.0/container">
<parameters>
<parameter key="mailer.username">foo</parameter>
<parameter key="mailer.password">bar</parameter>
<parameter key="mailer.class">Zend_Mail</parameter>
</parameters>
<services>
<service id="mail.transport" class="Zend_Mail_Transport_Smtp" shared="false">
<argument>smtp.gmail.com</argument>
<argument type="collection">
<argument key="auth">login</argument>
<argument key="username">%mailer.username%</argument>
<argument key="password">%mailer.password%</argument>
<argument key="ssl">ssl</argument>
<argument key="port"><span class="hljs-keyword">true</span></argument>
</argument>
</service>
<service id="mailer" class="%mailer.class%">
<call method="setDefaultTransport">
<argument type="service" id="mail.transport">
</argument></call>
</service>
</services>
</container>

但是,另一方面,將服務容器定義為普通的 PHP 類會為您提供最好的效能,正如本系列第二篇文章中所見:

<?php
class Container extends sfServiceContainer
{
static protected $shared = array();
protected function getMailTransportService()
{
return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth'     => 'login',
'username' => $this['mailer.username'],
'password' => $this['mailer.password'],
'ssl'      => 'ssl',
'port'     => 465,
));
}
protected function getMailerService()
{
if (isset(self::$shared['mailer']))
{
return self::$shared['mailer'];
}
$class = $this['mailer.class'];
$mailer = new $class();
$mailer->setDefaultTransport($this->getMailTransportService());
return self::$shared['mailer'] = $mailer;
}
}

上面的程式碼儘可能地提供了靈活性,這要歸功於配置變數,並且保證了較好的效能。

有沒有魚和熊掌可兼得的方法呢?很簡單。Symfony 依賴注入元件提供了另一個內建的「轉存器」:一個 PHP 轉存器。這個轉存器可以將任何服務容器轉換為普通的 PHP 程式碼。沒錯,它可以自動生成類似手動編寫的服務容器建立程式碼。

讓我們再次使用我們的 Zend_Mail 例子,為了簡潔起見,讓我們使用前一篇文章中建立的 XML 配置檔案:

$sc = new sfServiceContainerBuilder();
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load('/somewhere/container.xml');
$dumper = new sfServiceContainerDumperPhp($sc);
$code = $dumper->dump(array('class' => 'Container'));
file_put_contents('/somewhere/container.php', $code);

類似其它轉存器一樣,sfServiceContainerDumperPhp 類將容器作為其建構函式的第一個引數。該 dump() 方法接受一組選項,其中一個是要生成的類的名稱。

這裡是生成的程式碼:

class Container extends sfServiceContainer
{
protected $shared = array();
public function __construct()
{
parent::__construct($this->getDefaultParameters());
}
protected function getMailTransportService()
{
$instance = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => $this->getParameter('mailer.username'),
'password' => $this->getParameter('mailer.password'),
'ssl' => 'ssl',
'port' => 465
));
return $instance;
}
protected function getMailerService()
{
if (isset($this->shared['mailer'])) return $this->shared['mailer'];
$class = $this->getParameter('mailer.class');
$instance = new $class();
$instance->setDefaultTransport($this->getMailTransportService());
return $this->shared['mailer'] = $instance;
}
protected function getDefaultParameters()
{
return array (
'mailer.username' => 'foo',
'mailer.password' => 'bar',
'mailer.class' => 'Zend_Mail',
);
}
}

如果仔細檢視「轉存器」生成的程式碼,您會發現程式碼與我們手寫的程式碼非常相似。

生成的程式碼不會使用快捷方式表示法來訪問引數和服務以儘可能快。

通過使用 sfServiceContainerDumperPhp,您可以獲得兩全其美的效果:XML 或 YAML 格式的靈活性來描述和配置您的服務,以及自動生成的效能更優的 PHP 檔案。

當然,由於專案對於不同的環境幾乎總是不同的設定,因此您可以根據環境或除錯設定生成不同的容器類。下面是一小段 PHP 程式碼,演示瞭如何為第一個請求動態構建容器,並在不處於除錯模式時在後續請求中使用快取:

$name = 'Project'.md5($appDir.$isDebug.$environment).'ServiceContainer';
$file = sys_get_temp_dir().'/'.$name.'.php';
if (!$isDebug && file_exists($file))
{
require_once $file;
$sc = new $name();
}
else
{
// build the service container dynamically
$sc = new sfServiceContainerBuilder();
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load('/somewhere/container.xml');
if (!$isDebug)
{
$dumper = new sfServiceContainerDumperPhp($sc);
file_put_contents($file, $dumper->dump(array('class' => $name));
}
}

至此有關 Symfony 2依賴注入容器的介紹就差不多完成了。

在結束本系列之前,我還想向您介紹「轉存器」的另一個重要功能。「轉存器」可以做很多不同的事情,為了演示元件如何完成程式碼解耦,我實現了 「Graphviz 轉存器」。它是做什麼的?幫助您視覺化您的服務及其依賴關係。

首先,讓我們看看如何在我們的示例容器上使用它:

$dumper = new sfServiceContainerDumperGraphviz($sc);
file_put_contents('/somewhere/container.dot', $dumper->dump());

Graphviz 轉存器為容器生成一個dot 檔案:

digraph sc {
ratio="compress"
node [fontsize="11" fontname="Myriad" shape="record"];
edge [fontsize="9" fontname="Myriad" color="grey" arrowhead="open" arrowsize="0.5"];
node_service_container [label="service_container\nsfServiceContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"];
node_mail_transport [label="mail.transport\nZend_Mail_Transport_Smtp\n", shape=record, fillcolor="#eeeeee", style="dotted"];
node_mailer [label="mailer\nZend_Mail\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_mailer -> node_mail_transport [label="setDefaultTransport()" style="dashed"];
}

該檔案可以通過使用 dot 程式 轉換為圖片:

$ dot -Tpng /somewhere/container.dot > /somewhere/container.png

對於這個簡單的例子,視覺化沒有真正的附加價值,但只要你開始有不止一些的服務,就會變得非常有用。

Graphviz 轉存器的 dump() 方法需要很多不同的選項來調整圖形的輸出。檢視原始碼以發現它們中的每一個的預設值:

graph:整個圖形的預設選項
node:節點的預設選項
edge:邊緣的預設選項
node.instance:由物件例項直接定義的服務的預設選項
node.definition:通過服務定義例項定義的服務的預設選項
node.missing:缺失服務的預設選項

下圖是為即將釋出的 Symfony 元件生成的圖片:

這就是依賴注入這個系列的全部內容。我希望您能夠有所收穫。我也希望你能很快嘗試 Symfony 2 服務容器元件並給我反饋你的使用情況。另外,如果您為某些現有的開源庫建立「功能」,請考慮與該社群分享它們。您也可以將您的功能分享給我,我會將它們放在容器元件的以便於重用。

原文: http://fabien.potencier.org/s…