本文討論一種稱為 Butterfly 的框架的創建過程,它運行在 PHP 5 中,而且有助於將一系列 XSLT 樣式表應用到 XML 源文檔。它提供轉換結果的透明緩存。受到 Java™-based Apache Cocoon 項目的啟發,之所以這樣命名是因為它可以存儲和管理數據在不同形式之間的轉換(從毛毛蟲蛻變為蝴蝶),因此這種更加輕量級的框架被稱為 Butterfly。利用 Butterfly 框架,可以創建一個定義一系列樣式錶轉換的 XML 配置文件,然後實例化 Butterfly 對象以生成一個 XSLT 轉換鏈的結果。本文也討論了一般的框架設計特性,並重點介紹了 Butterfly 框架。
簡介
在 PHP 5 中使用 XSL 模塊,您可以對 XML 文檔應用 XSLT 樣式表,將 XML 數據轉換為其他類型的文本文檔。轉換后的文檔可以是其他 XML 結構、HTML 或其他結構(純文本甚至是 Java 以及其他編程語言)。無論源文本或目標文檔結構是什麼,您都可以特定於 XSLT 中的問題進行編程。本文使用的 PHP 代碼只用來創建 XSLT 處理器對象和應用轉換。
因為在 PHP 中對 XML 應用 XSLT 樣式表的方法通常是相同的,因此,可以將特定於業務的代碼提取出來,從而實現更易重用的過程。本文概述了一個稱為 Butterfly 的輕量級、可重用的 PHP 框架,該框架處理一系列 XSLT 文檔(從 參考資料 下載 Butterfly)。注意:這裡介紹的 Butterfly 框架指的是 http://jakemiles.com/butterfly 中的項目,與其他具有相同名稱的基於 Java 的 Web 應用程序框架無關。該系列以一個 XML 源文檔開頭(但是,未必是一個文件),對其應用一系列 XSLT 樣式表直到它生成最終文檔。該功能是 Apache Cocoon 項目提供的功能的一個子集,Apache Cocoon 項目處理 XSLT 樣式表管道來生成最終的文檔。
如果最終文檔是一個 Web 頁面,則處理 XSLT 樣式表要考慮的一個問題是性能。對於小型數據文檔和簡單的樣式表,這並不是難題。但是,對於包含成千上萬個元素的大型數據集,對每個載入的頁面都應用一系列樣式表,不僅會減慢頁面速度,而且會消耗伺服器的大量內存和處理資源。
性能問題的解決方案非常簡單 — 將 XSLT 轉換的結果作為一個 Web 伺服器可以立即訪問的靜態 HTML 頁面存儲在緩存中,只有源文檔或某個樣式表改變時,才執行全部的 XSLT 轉換鏈。這種緩存機制不限於特定的 XML 或 XSLT 內容,因此,框架通常可以處理它。
在處理 XSLT 樣式表時,另外一個潛在的問題是 XML 源,它可以是一個文件或一個 SQL 資料庫。框架必須具有足夠的靈活性來處理多個數據源。
框架的開端
當設計一個面向對象的框架時,實際上可以使用兩種抽象工具:方法和類。兩種工具必須都是可擴展的,即允許將來生成新的行為,必須抽象為一個方法調用或類。它允許利用多態性在運行時換出一個不同的類來公開相同的方法。如果框架與類混雜,可以添加一個工廠類,將它們裝配到通用組合中,並為通用情況提供一個更簡單的 API。
對框架來說,一個實用方法是編寫核心功能,將更多結構應用到其中,使其具有可擴展性並簡化介面。要想進一步解釋本文介紹的代碼,首先查看貫穿代碼的兩個核心介面,ButterflyDocument 和 ButterflyXmlDocument,如清單 1 所示。
interface ButterflyDocument { function getContents(); function writeContents(); } interface ButterflyXmlDocument extends ButterflyDocument { function getDom(); } |
ButterflyDocument 表示一個內容可讀寫的文檔。在 清單 1 中,getContents() 將它的內容作為字元串返回,writeContents() 將文檔的內容寫到標準輸出。ButterflyXmlDocument 表示一個 XML 文檔,可以通過 getDom() 方法擴展 ButterflyDocument,並返回表示它的 XML 內容的 DOMDocument 對象。
了解了這兩個核心介面后,查看框架的核心功能,ButterflyTransformer 類。該類實現對一個 XML 文檔應用 XSLT 樣式表的基本功能(參見清單 2)。
class ButterflyTransformer { private $processor; public function __construct ($xslSource) { $this->processor = new XSLTProcessor(); $this->processor->importStylesheet ($xslSource->getDom()); } public function transformToFile ($xmlSource, $filepath) { $file = fopen ($filepath, "w"); fwrite ($file, $this->processor->transformToXml ($xmlSource->getDom())); fclose($file); } public function transformToString ($xmlSource, $filepath) { return $this->processor->transformToXml ($xmlSource->getDom()); } } |
在 清單 2中,ButterflyTransformer 接受一個表示 XSLT 樣式表的 ButterflyXmlDocument 對象,並使用 PHP 5 的 XSL 模塊將其應用到 XML 數據文檔中。構造函數創建一個 XSLTProcessor 對象,並將給定的 XSLT 樣式表導入到對象中。transformToFile() 對另外一個 ButterflyXmlDocument 對象表示的 XML 文檔應用樣式表,並將轉換后的內容寫到指定的文件路徑。
添加轉換鏈和緩存
比 ButterflyTransformer 更高一級的是 Butterfly 類。該類表示樣式錶轉換系列中的一條鏈(或採用 Apache Cocoon 用語,管道),另外也處理核心緩存邏輯(參見 清單 3)。
class Butterfly implements ButterflyXmlDocument { private $transformer; private $xmlDoc; private $cache; public function __construct ($transformer, $xmlDoc, $cache=null) { $this->transformer = $transformer; $this->xmlSource = $xmlDoc; $this->cache = $cache; } public function getDom() { return $this->getTransformedDoc()->getDom(); } public function getContents() { return $this->getTransformedDoc()->getContents(); } public function writeContents() { $this->getTransformedDoc()->writeContents(); } protected function getTransformedDoc() { return ($this->cache != null ? $this->getTransformedCache () : $this->getTransformedString ()); } protected function getTransformedCache () { if (! $this->cache->isPresent()) { $this->transformer->transformToFile ($this->xmlSource, $this->cache->getFilepath()); } return $this->cache; } protected function getTransformedString () { return new ButterflyXmlString ($this->transformer->transformToString ($xmlDoc)); } } |
Butterfly 中的構造函數接受一個 ButterflyTransformer 對象來處理核心 XSLT 轉換邏輯,ButterflyXmlDocument 表示源 XML,一個可選的 ButterflyCache 對象存儲和生成轉換后的文檔的緩存形式。注意,Butterfly 本身實現了 ButterflyXmlDocument,意味著它公開了 getContents()、writeContents() 和 getDom() 方法。因此,您可以像實現一個 Butterfly 對象鏈一樣實現一個 XSLT 轉換鏈,每個轉換鏈充當下一個對象的源對象。
要使用 Butterfly,調用程序使用這些對象調用構造函數,然後調用 getContents() 或 writeContents() 方法,獲得轉換后的結果字元串或直接將轉換后的結果寫到標準輸出。
getTransformedDoc() 和 getTransformedCache() 方法處理緩存邏輯。回想一下,構造函數以可選參數的方式接受緩存對象。這樣緩存變為可選,因此,調用程序可以只創建一個 Butterfly 對象來應用 XSLT 轉換,而不必考慮緩存或框架配置。getTransformedDoc() 查看是否為 Butterfly 提供了緩存對象,如果是,則調用 getTransformedCache() 處理緩存,否則調用 getTransformedString()。注意,每個方法都會返回一個 ButterflyDocument 對象。
源文檔
ButterflyTransformer 和 Butterfly 類包含框架的核心功能。下一步實現一個本身不是 Butterfly 的 ButterflyXmlDocument,因此,當調用 Butterfly 的 writeContents() 或 getContents() 方法時,委託鏈會在某處終止。最常見的例子是對 XML 文件應用 XSLT 樣式表。為了表示這種類型的文檔,要創建 ButterflyFile 類(參見 清單 4)。
class ButterflyFile implements ButterflyDocument { private $filepath; public function __construct($filepath) { $this->filepath = $filepath; } public function getFilepath() { return $this->filepath; } public function isPresent() { return file_exists ($this->filepath); } public function getContents() { return file_get_contents($this->filepath); } public function writeContents() { $file = fopen($this->filepath, "r"); fpassthru($file); fclose($file); } public function delete() { unlink ($this->filepath); } } |
這是 ButterflyXmlDocument 具體實現的第一步 — 該類只實現 ButterflyDocument,它是 ButterflyXmlDocument 的父介面。轉換的結果可能不是 XML,框架不僅要表示 XML 源文檔,還要表示純文本文檔。因此,ButterflyFile 提供了一個針對文件的標準封裝類。可以通過文件的完整路徑構造它,並且提供可以通過 Butterfly 類調用的 isPresent()、getContents() 和 writeContents() 方法。如果路徑指定的文件在磁碟中存在,isPresent() 返回真。getContents() 返迴文件的內容。調用 writeContents() 以將文件內容寫到標準輸出,在 PHP 中通過調用 fpassthru() 函數實現該功能。
對 fpassthru 的調用是系統的一個重要元素。當得到 XSLT 管道的最終結果后,框架存儲結果的緩存形式用於快速交付。此處對 fpassthru() 的調用是提供快速交付的一種方法。Web 伺服器直接將緩存文件的內容寫到標準輸出,顯示在用戶的瀏覽器中。
表示緩存文件
因為緩存行為是系統中一個重要的概念,所以有必要創建一個 ButterflyCache 類表示緩存后的轉換(參見 清單 5)。
class ButterflyCache extends ButterflyFile { public function __construct($filepath) { parent::__construct ($filepath); } } |
ButterflyCache 只用來擴展 ButterflyFile,因此自然有人會問它的作用是什麼?它的作用是表示 Butterfly 系統中的一個特殊的文件功能。這樣可為緩存后的文件提供一個抽象層。如果 Butterfly 的緩存后的文件需要其他行為,或稍後出現其他類型的緩存(例如,將結果緩存到資料庫中),那麼不會影響其他的框架類。只有創建緩存對象的代碼需要了解新的緩存類型。這種類型的抽象在 Java 或其他強類型的語言中更有用,因為調用代碼實際上依賴編譯時的緩存對象的類型。在 PHP 這類語言中,包含這種類可以使以後將對框架進行擴展的開發人員明確目的,以便將來擴展框架。創建 ButterflyCache(而不只是 ButterflyFile )的代碼明確地創建了用於緩存轉換內容的文件。
ButterflyXmlDocument 的具體實現
相比之下,ButterflyXmlFile 類將概念和一些功能添加到純文本文件的概念和功能中(參見 清單 6)。
class ButterflyXmlFile extends ButterflyFile implements ButterflyXmlDocument { public static function create ($filepath) { return new ButterflyXmlFile ($filepath); } public function __construct($filepath) { parent::__construct ($filepath); } public function getDom() { $dom = DOMDocument::load($this->getFilepath()); if (! $dom) { throw new Exception ("Couldn't load DOM object from filepath " . $this->getFilepath()); } return $dom; } } |
與 ButterflyCache 類似,ButterflyXmlFile 擴展 ButterflyFile,因為它們都表示磁碟上的文件。但是,ButterflyXmlFile 也實現了 ButterflyXmlDocument 介面,因此,可以實現 getDom() 方法以 DOMDocument 的形式返回內容。這非常關鍵,因為 ButterflyTransformer 類中的 PHP 的 XSLTProcessor 對象只處理 DOMDocument 對象表示的 XML。另外,它能隱式實現 ButterflyXmlDocument's getContents() 和 writeContents() 方法,因為 ButterflyFile 基類中定義了兩種方法。
系統中源 XML 的另外一種類型是純 XML 字元串。因為系統中這種特殊類型的字元串具備特殊的含義和行為,因此,它被封裝在 ButterflyXmlString 類中(參見 清單 7)。
class ButterflyXmlString implements ButterflyXmlDocument { protected $xml; public function __construct($xmlString) { $this->xml = $xmlString; } public function getDom() { return DOMDocument::load($this->xml); } public function getContents() { return $this->xml; } public function writeContents() { echo ($this->xml); } } |
在 清單 7 中,ButterflyXmlString 封裝了一個 XML 字元串,並提供一個 ButterflyXmlDocument 介面。getContents() 返回字元串,writeContents() 將字元串回傳到標準輸出(即瀏覽器)。getDom() 返回表示字元串的 XML 內容的 DOMDocument,接下來,XSLTProcessor 可以將其作為 XSLT 樣式表或要轉換的 XML 源文檔進行處理。
使用類抽象實現未來擴展
作為抽象的主要方法,類的使用在面向對象框架設計中是很關鍵的。Butterfly 類包含大量 if 語句,用以確定源 XML 是一個字元串還是一個文件,從而採取相應的動作,但是,這樣代碼是完全固定的,不允許擴展。ButterflyXmlDocument 介面抽象不需要改變現有代碼,就可實現未來擴展。例如,如果想編寫一個 ButterflyXmlSqlSource 類,它接收 SQL 語句,並將結果轉換為 XML,這時,就可以將該類添加到系統中,將其作為 XML 源對象(而不是 ButterflyXmlFile 或 ButterflyXmlString)傳遞給 Butterfly。
使用一個工廠類簡化對象構建
現在,Butterfly 系統將應用 XSLT 樣式錶轉換鏈,並為轉換鏈中的每個點提供緩存機制 — 每個表示 XSLT 轉換的 Butterfly 對象。但是,問題是這樣不僅會使終端開發人員無法了解如何構建 Butterfly 對象(因為開發人員需要有關的各種類,以及如何實例化每個類),而且這樣做是很麻煩的,需要開發人員為每個 XSLT 轉換鏈編寫特殊代碼,或編寫裝配 Butterfly 鏈的代碼。
針對這類問題的一種面向對象式解決方案是使用工廠類。該類可以根據輸入條件創建其他對象。Butterfly 的工廠類 ButterflyFactory 從 清單 8 所示的配置文件中裝配 Butterfly 鏈。
<butterfly-config> <chain> <name>resume</name> <source type="ButterflyXmlFile"> <arg>resume.xml</arg> </source> <xslt file="resume-restructured.xsl"/> <xslt file="resume-restructured-to-view.xsl"/> <xslt file="resume-view-to-html.xsl"/> </chain> </butterfly-config> |
該配置文件定義了 Butterfly 轉換鏈。每個鏈都以一個 source 開頭,並包含任何數量的 XSLT 樣式表,可以依次應用。本例創建了一個呈現簡歷 XML 文檔 resume.xml 的單個鏈,依次應用三個樣式錶轉換,將 resume.xml 轉換為一個 HTML 頁面。配置應儘可能地少,因為框架的目的是簡化任務,複雜的配置將無法達到此目的。
ButterflyFactory 類讀取該配置文件的格式,並充當 Butterfly 對象的工廠類(參見 清單 9)。
class ButterflyFactory { private $cacheDir; private $chains; public function __construct ($configFile, $cacheDir) { $this->cacheDir = $cacheDir; $config = simplexml_load_file($configFile); $this->chains = $this->mapChainsByName (is_array($config->chain) ? $config->chain : array($config->chain)); } protected function mapChainsByName ($chains) { $byName = array(); foreach ($chains as $chain) { $name = (string) $chain->name; if (isset($byName[$name])) { throw new Exception ("Two Butterfly chains defined with the same name: $name"); } $byName[$name] = $chain; } return $byName; } protected function getChainByName ($name) { if (! isset($this->chains[$name])) { throw new Exception ("ButterflyFactory: no chain with specified name: $name"); } return $this->chains[$name]; } protected function createCacheFactory ($cacheDir) { return new ButterflyCacheFactory ($cacheDir); } protected function createButterflyObject ($transformer, $xmlDoc, $cache) { return new Butterfly ($transformer, $xmlDoc, $cache); } public function createButterfly ($xsltFilepath, $xmlDoc, $cache) { return $this->createButterflyObject ($this->createTransformerFromFilepath ($xsltFilepath), $xmlDoc, $cache); } protected function createTransformerFromFilepath ($xsltFilepath) { return new ButterflyTransformer (new ButterflyXmlFile($xsltFilepath)); } public function getButterfly ($chainName) { $chain = $this->getChainByName ($chainName); $source = $this->createChainSource ($chain); $invalidateCache = false; for ($i = 0; $i < count($chain->xslt); $i++) { $xslt = $chain->xslt[$i]; $cache = $this->createButterflyCache ($this->createCacheFilename ((string) $chain->name, $i)); if ($invalidateCache && $cache->isPresent()) { $cache->delete(); } $source = $this->createButterfly ((string) $xslt['file'], $source, $cache); $invalidateCache = $invalidateCache || ! $cache->isPresent(); } return $source; } protected function createChainSource ($chain) { if (! isset($chain->source)) { throw new Exception ("Butterfly chain has no source element: $chainName"); } $args = is_array($chain->source->arg) ? $chain->source->arg : array($chain->source->arg); $argsAsStrings = array(); foreach ($args as $arg) { $argsAsStrings[] = (string) $arg; } return $this->createSourceFromType ((string) $chain->source['type'], $argsAsStrings); } protected function createCacheFilename ($chainName, $xsltNumber) { return $this->cacheDir . '/' . $chainName . '_' . $xsltNumber . '.cache'; } protected function createSourceFromType ($type, $args) { return call_user_func_array (array($type, 'create'), $args); } protected function createButterflyCache ($cacheFilePath) { return new ButterflyCache ($cacheFilePath); } } |
ButterflyFactory 在配置文件的路徑和緩存文件夾的路徑(保存全部緩存后的 XSLT 轉換的目錄)中創建。它使用 SimpleXML 模塊讀取 XML 配置文件,並將其解析為 PHP 對象,然後,創建一個關聯數組,將每個定義的鏈映射到其名稱,以便於查找。
ButterflyFactory 的核心是 getButterfly() 方法,它接受定義的鏈的名稱,並返回 ButterflyXmlDocument,由於類型定義的特性,ButterflyXmlDocument 將是一個返迴轉換結果的 Butterfly 對象。getButterfly() 首先根據提供的名稱查找鏈(例如,在示例配置中,使用 “resume” 查找 resume 鏈),然後確認它包含定義源 XML 文檔對象的 <source> 元素,該元素可以是一個 ButterflyXmlFile 或其他 ButterflyXmlDocument 實現。<source> 元素的類型屬性指定作為源文檔對象要實例化的類。對於 “resume” 鏈,它的 source 是一個 ButterflyXmlFile。
一旦 getButterfly() 獲得源對象,接下來,它將遍歷 <chain> 中定義的 <xslt> 元素,並創建每個元素的 Butterfly 對象。因為 Butterfly 接受源對象作為參數,第一個 Butterfly 對象是鏈的 <source> 元素中創建的源對象,但是,接下來,Butterfly 對象本身分配給 $source 變數,成為循環中下一個要創建的 Butterfly 的源對象。不斷重複此過程,直到返回最後一個要調用的 Butterfly。這樣就創建了 Butterfly 鏈,因此,調用返回的 Butterfly 對象促使它調用它的源對象(可能是一個 Butterfly),源對象再調用它的源對象,直到真正的源對象被調用,該對象將它的內容返回給調用的 Butterfly。然後,調用的 Butterfly 應用它的 XSLT 樣式表,並將結果返回給它的調用者,後者執行同樣的操作,直到鏈的頂層對象將轉換的結果返回或寫到標準輸出。
該循環也使 $invalidateCache 標誌保持最新狀態,因此,如果刪除了 Butterfly 的緩存,它便刪除鏈中在它之上的其他 Butterfly 對象的緩存。這樣,如果在轉換鏈的任何階段刪除了緩存文件,將使所有的相關 Butterfly 緩存文件無效,必須重新創建這些文件。
後續步驟
這時,框架將處理 XSLT 樣式錶轉換的各個部分,包括創建 XSLT 鏈,在呈現 HTML 頁面時使用緩存文件消除 XSLT 轉換的性能損失。框架設計的下一步是提供更優雅的緩存管理介面(目前需要用戶刪除相應的緩存文件),提供其他的 XML 源,如從 SQL 查詢獲取內容的 XML 源。類(如 ButterflyXmlSqlSource)本身可以成為一個輕量級的框架,因為配置需要指定 OR 映射,並將關係型 SQL 結果轉換為具有層次結構的 XML 文檔。另外也可以使用 SQL 資料庫存放緩存文件,但是,使用磁碟訪問和 fpassthru() 無疑是提高性能的最佳解決方案。
結束語
Butterfly 框架是一種在 PHP 5 中簡化 XSLT 的使用的輕量級方法,通過應用樣式錶鏈和緩存來提高性能。框架非常簡單和直觀,但是它從 PHP 代碼中去除了 XSLT 樣式表應用,使開發人員能夠關注工作的核心內容,即 XSLT 本身。要進一步研究 Butterfly,請參見 參考資料。(責任編輯:A6)
[火星人 ] 創建支持 XSLT 轉換管道的框架已經有624次圍觀