本文讨论一种称为 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 所示。
清单 1. 核心 ButterflyDocument 和 ButterflyXmlDocument 接口
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)。
清单 2. ButterflyTransformer
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)。
清单 3. Butterfly 类
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)。
清单 4. 创建 ButterflyFile 类
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)。
清单 5. 创建 ButterflyCache 类
class ButterflyCache extends ButterflyFile { public function __construct($filepath) { parent::__construct ($filepath); } } |
ButterflyCache 只用来扩展 ButterflyFile,因此自然有人会问它的作用是什么?它的作用是表示 Butterfly 系统中的一个特殊的文件功能。这样可为缓存后的文件提供一个抽象层。如果 Butterfly 的缓存后的文件需要其他行为,或稍后出现其他类型的缓存(例如,将结果缓存到数据库中),那么不会影响其他的框架类。只有创建缓存对象的代码需要了解新的缓存类型。这种类型的抽象在 Java 或其他强类型的语言中更有用,因为调用代码实际上依赖编译时的缓存对象的类型。在 PHP 这类语言中,包含这种类可以使以后将对框架进行扩展的开发人员明确目的,以便将来扩展框架。创建 ButterflyCache(而不只是 ButterflyFile )的代码明确地创建了用于缓存转换内容的文件。
ButterflyXmlDocument 的具体实现
相比之下,ButterflyXmlFile 类将概念和一些功能添加到纯文本文件的概念和功能中(参见 清单 6)。
清单 6. ButterflyXmlFile 类
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)。
清单 7. ButterflyXmlString 类
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 链。
清单 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)。
清单 9. ButterflyFactory
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)