本文首發於 Symfony 服務容器性能優化,轉載請註明出處。
本文是依賴注入(Depeendency Injection)系列教程的最後一篇文章,本系列教程主要講解如何使用 PHP 實現一個輕量級服務容器,教程包括:php
在本系列關於依賴注入的前五篇文章中,咱們逐步介紹了這個簡單實用的設計模式背後的主要概念。咱們還談到了一個將用於 Symfony 2 的輕量級 PHP 容器的實現。html
但隨着 XML 和 YAML 配置文件的引入,您可能會對容器自己的性能產生懷疑。即便服務是延遲加載,在每一個請求中讀取一堆 XML 或 YAML 文件,並經過使用自省(Introspection)來建立對象在 PHP 中可能效率不高。因爲容器幾乎是應用程序的基石,它的速度確實很重要。node
一方面,使用 XML 或 YAML 來描述服務及其配置是很是強大和靈活的:shell
<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 配置文件:app
$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 服務容器組件並給我反饋你的使用狀況。另外,若是您爲某些現有的開源庫建立「功能」,請考慮與該社區分享它們。您也能夠將您的功能分享給我,我會將它們放在容器組件的以便於重用。