内容


使用服务数据对象简化 PHP 中的 XML 处理

通过构建简单的 blog 和 RSS 提要探索 SDO

Comments

观察 SDO 及其相关接口,即可对 SDO 扩展提供的 API 有清晰的认识。本文稍后会展示一个在分为两部分的应用程序中使用 SDO 的示例,一部分为实现简单 blog(Web log)的小型 PHP 应用程序,另一部分则将此 blog 显示为 RSS 提要。两部分均以 SDO 作为处理 XML 的手段。我们希望您能认同,SDO 是在 PHP 中处理 XML 数据的一种吸引人的选择。

SDO 简介

让我们首先来剖析一下服务数据对象(SDO) 这个术语,看看其来源。

SDO 是 PHP V5 对象。但与普通 PHP V5 对象不同,SDO 仅用于传送数据,而没有定义于其上的应用方法或功能。因此,它们是数据对象。SDO 是使数据可为应用程序所用的一种方式,同时保持与原始数据源的格式无关,因此可通过同一种方式结构化和操纵数据 —— 无论数据是来自关系数据库还是 XML。不太精确地讲,这使其成为服务 数据对象。目前,我们认为 SDO 在面向服务的应用程序中非常有用:有着复杂结构的数据需要在面向服务的应用程序的两个组件之间进行交换时,SDO 是一种很好的实现方式。

SDO 是数据传输对象(Data Transfer Object) 模式的泛化。在线搜索 “Data Transfer Object”,很快就能找到 Martin Fowler 的著作 Patterns of Enterprise Application Architecture 中给出的定义:“为减少方法调用次数而在流程间传送数据的一种对象。” 数据传输对象是将一组值包装在一个对象中的方法,目的在于经济地传送这些值。

SDO 从几个方面扩展了数据传输对象模式,实现了一些比简单模式更强大的功能。但 SDO 与数据传输对象依然存在共性:仅传送数据,没有为之定义的应用方法或业务逻辑。

SDO 在以下三个重要的方面增强了基本数据传输对象模式:

  • SDO 不仅仅是单独的数据对象,还是可彼此引用的对象集合。因而在表示那些通常存储在 XML 文件中的结构化数据时尤为有用。
  • 一组经过变更的 SDO 还会维护一条其原始值的记录,从而支持某种类型的乐观性锁定算法。
  • 仅当 SDO 位于内存中或被串行化并在传输中时,这种对象才是有意义的,但仅保持在内存中或传输中且不会传送到后端存储的数据并不总是有用的。SDO 的 PHP 实现包含两个所谓的 Data Access Service(DAS),其任务就是从一些后端存储中获取数据,将之转换为 SDO 图或将 SDO 图中的数据重新放回存储。一个 DAS 用于处理关系数据库中的数据,另外一个用于处理 XML 文件中的数据。

顺便提一下,SDO for PHP 实现只是家族产品之一,此家族中还有面向 C++ 和 Java™ 技术的 SDO 实现。

SDO 剖析

一个 SDO 就是一个 PHP V5 对象。与其他任何 PHP V5 对象相似,SDO 也拥有一组属性,但与普通 PHP V5 对象的不同之处在于,SDO 只是数据值容器,不能有为之定义的应用方法。不能通过代码为 SDO 编写一个类定义,也不能调用构造函数来创建它们。

var_dump()

暂时把创建方法的问题放到一边,假如我们在 SDO 上使用 var_dump() 函数,将看到这是 SDO_DataObjectImpl 的一个实例,还会看到属性名称及其值。假设有一个名为 $author 的 SDO,并在此 SDO 上使用了 var_dump 函数,则可看到:

清单 1. SDO 的 var_dump()
object(SDO_DataObjectImpl)#2 (3) {
  ["name"]=>
  string(19) "William Shakespeare"
  ["dob"]=>
  string(28) "April 1564, most likely 23rd"
  ["pob"]=>
  string(33) "Stratford-upon-Avon, Warwickshire"
}

该数据对象有三个属性,均为 PHP 字符串:namedob(出生日期)和 pob(出生地)。SDO 的数据对象属性可为任意简单 PHP 标量类型:字符串、整型、浮点、布尔,还可以是 NULL 或另一个 SDO 的引用。任何给定属性的类型都是固定的,若指派的值类型不同,将予以转换。例如,若为字符串属性指派一个整型值,整型值将被转换为字符串。

所有 SDO 都有 SDO_DataObjectImpl PHP 类型,而 SDO 还有一个 SDO 类型名。var_dump() 函数展示了数据对象的内容,但未显示其 SDO 类型名。为查看此信息,在对象上使用 getTypeName() 方法,本例的输出结果为:Author

本文稍后将介绍在哪里为 SDO 定义该类型名,以及如何指定属性集。

Print

顺便说一下,在此对象上使用 PHP print 指令将获得与 var_dump() 相同的信息,且格式更为简洁,仅有一行(为可读性起见,本文将其拆分为五行书写)。

清单 2. SDO 的 print()
object(SDO_DataObject)#2 (3) {
  name=>"William Shakespeare"; 
  dob=>"April 1564, most likely 23rd"; 
  pob=>"Stratford-upon-Avon, Warwickshire"
}

设置及获取属性

SDO 支持对象语法和关联数组语法,因此属性可以通过对象语法设置:

$author->name = 'William Shakespeare';
$author->dob  = 'April 1564, most likely 23rd';
$author->pob  = 'Stratford-upon-Avon, Warwickshire';

或通过关联数组语法设置:

$author['name'] = 'William Shakespeare';
$author['dob']  = 'April 1564, most likely 23rd';
$author['pob']  = 'Stratford-upon-Avon, Warwickshire';

当然,可从数据对象中获取值,也可同样使用这两种形式:

$name_of_the_author = $author->name;

或:

$name_of_the_author = $author['name'];

多值和单值属性

属性可为多值的,也可为单值的。若一个属性为多值的,则 var_dump() 将显示它指向一个 SDO_DataObjectList 对象,此对象为单值列表。清单 3 给出了一个示例,其中 works 属性已添加到 Author 类型,并定义为包含字符串列表:

清单 3. 带有多值属性的 SDO 的 var_dump()
object(SDO_DataObjectImpl)#2 (4) {
  ["name"]=>
  string(19) "William Shakespeare"
  ["dob"]=>
  string(28) "April 1564, most likely 23rd"
  ["pob"]=>
  string(33) "Stratford-upon-Avon, Warwickshire"
  ["works"]=>
  object(SDO_DataObjectList)#4 (2) {
    [0]=>
    string(17) "The Winter's Tale"
    [1]=>
    string(9) "King Lear"
  }
}

在本例中,print 指令更为简洁,仅指出存在一个名为 works 的多值属性,而未展开此属性。

清单 4. 带有多值属性的 SDO 的 Print
object(SDO_DataObject)#2 (4) {
  name=>"William Shakespeare"; 
  dob=>"April 1564, most likely 23rd"; 
  pob=>"Stratford-upon-Avon, Warwickshire"; 
  works[2]
}

要查看 works 属性的内容,需要分别执行 print

由于 SDO_DataObjectList 实现 PHP ArrayAccess 接口,多值属性的行为与 PHP 数组极为类似。例如,假设已经将这两本著作添加到列表中:

$author->works[] = "The Winter's Tale";
$author->works[] = "King Lear";

假如我们想遍历并输出这些值,需进行如下操作:

foreach ($author->works as $work) {
  print $work . "\n";
}

务必注意:SDO_DataObjectList 的行为与 PHP 数组并非完全相同。例如,在列表中的一个项目上使用 unset 将打乱所有后续项目的索引,索引集中不允许存在任何中断。

反射 API

我们已看到,var_dump()print 可显示任何给定 SDO 的当前属性集。为充分完整地获得 SDO 的信息,例如要查看尚未设置的属性的名称与类型,可参阅 PHP 手册,其中的 SDO 部分介绍了一种反射 API(参见 参考资料 一节)。

连接 SDO:更复杂的结构

SDO 用于表示结构化数据,单独的 SDO 能表示的只有这么多。若将 SDO 彼此连接在一起构成对象图,它们将拥有更为强大的能力。

SDO 通过引用彼此相连。从一个 SDO 到另一个 SDO 的引用也是前者的一项属性。在上面的示例中,属性均为原生数据类型或其列表,但属性也可以是对另一 SDO 的一个引用或引用列表。

下面给出一个简单的示例:我们再次使用 var_dump() 转储名为 $author 的 SDO,但这一次,name 属性不再定义为简单的字符串,而是对另一个 SDO 的引用。第二个 SDO 通过自身的两个属性表示作者的姓名:firstlast。转储 author 对象时将同时看到这两个对象,这是因为 var_dump() 跟踪了从 author 对象到 name 对象的引用:

清单 5. 一对连接的 SDO 的 var_dump()
object(SDO_DataObjectImpl)#2 (3) {
  ["name"]=>
  object(SDO_DataObjectImpl)#3 (2) {
    ["first"]=>
    string(7) "William"
    ["last"]=>
    string(11) "Shakespeare"
  }
  ["dob"]=>
  string(28) "April 1564, most likely 23rd"
  ["pob"]=>
  string(33) "Stratford-upon-Avon, Warwickshire"
}

暂不考虑两个 SDO 的创建方法,第二个 SDO 应已通过之前介绍的方法指派为 author SDO 的 name 属性。整个代码序列应如清单 6 和清单 7 所示:

清单 6. 使用对象语法指派引用
$name->first  = 'William';
$name->last   = 'Shakespeare';

$author->name = $name;	// assign an SDO to the name property of $author
$author->dob  = 'April 1564, most likely 23rd';
$author->pob  = 'Stratford-upon-Avon, Warwickshire';

或:

清单 7. 使用关联数组语法指派引用
$name['first']  = 'William';
$name['last']   = 'Shakespeare';

$author['name'] = $name;	// assign an SDO to the name property of $author
$author['dob']  = 'April 1564, most likely 23rd';
$author['pob']  = 'Stratford-upon-Avon, Warwickshire';

其中的 $author$name 均为 SDO。

清晰识别此处 name 的不同用法非常有用,应该清楚地识别出哪些是 SDO 类型名,哪些是属性名。$author$name 是作为特定 SDO 类型的对象创建的,其创建方式尚未介绍。若在这些对象上调用 getTypeName(),将看到类型名。假设为 AuthorName。现在,$author 对象有一个名为 name 的属性。这样的定义使您仅能为其指派 SDO 类型为 Name 的对象。若试图指派一个不同 SDO 类型的 SDO(比如说,另外一名作者)或原生数据类型,SDO 将抛出异常。SDO 引用属性没有类型转换。但存在继承性:SDO 理解类型间的继承层次结构,给定属性要求给定类型,可指派子类型。

总之,有一个名为 $name 的对象,其 SDO 类型为 Name,此对象被指派给名为 name$author 属性。

至此为止,一切顺利,但关于 SDO 引用属性,还有两个重要的方面必须加以说明。

多值和单值引用属性

在查看包含原生数据类型的属性时,我们已经看到了一个方面:引用属性可为多值或单值的。多值引用属性指向 SDO_DataObjectList 对象,此对象为数据对象列表。这些数据对象类型相同。为详细说明,假设在类型为 Work 的 SDO 多值引用中使用作者的著作列表,且类型为 Work 的 SDO 包含标题和大致写作日期。再假设这些 SDO 还保有类型为 Name 的 SDO 的单值引用。本例的结构图如下:

图 1. 多值和单值引用属性 SDO 图解
多值和单值引用属性 SDO 图解
多值和单值引用属性 SDO 图解

包容和非包容引用属性

引用属性的第二个重要方面就是它们可为包容或非包容引用。

包容与非包容的概念是引用属性特有的,需要简单解释一下。前面已经提到,SDO 的目标之一就是可表示通常存储为 XML 的结构化数据。格式良好的 XML 文档的基本结构就是层次结构:一个元素位于层次结构的顶端,通常称为根元素或文档元素,它往往包含其他元素,这些元素又依次包含另外一些元素。由于任何给定元素都包含在包容它的元素的开始与结束标记之内,因此该结构必然为树型。让我们用作者与姓名的示例加以说明。这是上文中应用 var_dump 的作者与姓名对象在 XML 中的一种可能表达形式。

清单 8. 解释包容的 XML 示例文档
<author>
  <name>
    <first>William</first>
    <last>Shakespeare</last>
  </name>
  <dob>April 1564, most likely 23rd</dob>
  <pob>Stratford-upon-Avon, Warwickshire</pob>
</author>

简单的 <first><last> 元素包含在 <name> 元素中,该元素又包含在 <author> 元素之中。<first><last> 这两个简单的元素作为 name SDO 的原生数据类型(字符串)属性建模。<name> 元素包含于 <author> 元素之中,这一事实建模为从 author SDO 的姓名属性到 name SDO 的包容引用属性。

那么,若 SDO 将这种包容关系称为包容引用,非包容引用又是什么呢?尽管并非所有应用程序都会用到,但 XML 也允许表示不依赖包容层次结构的元素间的链接,方法是使用 XML ID 和 IDREF。SDO 将这些额外的关系建模为非包容引用

我们需要一个新例子来解释相关内容,这是一个在其他许多 SDO 文章中都提到的示例:一家公司包含多个部门,部门下又包含员工。示例的 XML 文档表示如下:

清单 9. 阐明 ID/IDREF 关系的 XML 示例文档(非包容)
<?xml version="1.0" encoding="UTF-8"?>
<company xmlns="companyNS" 
         xsi:type="CompanyType" 
         xmlns:tns="companyNS" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         name="MegaCorp" 
         employeeOfTheMonth="E0003">
  <departments name="Advanced Technologies" location="NY" number="123">
    <employees name="John Jones" SN="E0001"/>
    <employees name="Jane Doe" SN="E0003"/>
    <employees name="Al Smith" SN="E0004" manager="true"/>
  </departments>
</company>

您将看到 XML 建模简单包容层次结构的方法,在此结构中,一个 company 元素包含一个 departments 元素,departments 元素又包含 3 个 employees 元素。departments 和 employees 元素的名称可能选择得不太好,单数或许要比复数更好些。此文档作为 SDO 图载入内存时,图中将包含 5 个数据对象和 2 个数据对象列表。其中包括 1 个 company 数据对象、1 个 department 数据对象和 3 个 employee 数据对象。还有两个列表对象,一为部门集合(尽管只有一个部门,但属性为多值),一为员工集合。company 对象有一个指向 department 对象列表(本例中仅包含一个)的 departments 属性。department 对象有一个指向 employee 对象列表的 employees 属性。

各数据对象还有一个或多个原生数据类型属性,company 对象有一个继承自 name 属性(attribute)的 name 属性(property),值为 “MegaCorp”,department 和 employee 数据对象也有 name 属性等。

请注意 company 元素的 employeeOfTheMonth 属性。您会看到,它包含一位员工的序号。但未展示此 XML 文档将序号属性与 employeeOfTheMonth 属性相连接的 XML 模式,这是 XML ID 和 IDREF 的用法。SN(序号)定义为 ID 字段,employeeOfTheMonth 定义为 IDREF。这表示这两个与主层次结构无关的元素之间的关系。

我们使用非包容引用建模 SDO 中的 ID 和 IDREF 属性。与 XML 中的 ID 和 IDREF 属性类似,非包容关系就像是包容层次结构上额外的一层。根据 SDO 的规则,IDREF 不能引用文档中不存在的 XML 元素,任何非包容引用可获得的图内的 SDO 必须也可通过包容引用从根处获取。不能仅通过非包容引用指向一个 SDO。SDO 图的这一方面称为闭包(closure),任何 DAS 将 SDO 写出到存储时,都会就此进行检查。

下图描述了相关原理。注意非包容引用是怎样独立于包容层次结构的。还应注意,图中非包容关系可获得的员工同样可通过包容引用从根元素处获取。

图 2. 包容与非包容引用属性的 SDO 图解
包容与非包容引用
包容与非包容引用

类似于包容引用,非包容引用也可为多值或单值的。

其应用情况与 XML 的 ID 和 IDREF 相似,也就是说,有些应用程序会大量使用,而有些应用程序则根本不使用它们。

创建数据对象

我们已介绍了如何获取和设置数据对象的属性及其连接方法,接下来需要说明如何创建数据对象。

SDO 是通过调用数据工厂对象创建的。在数据工厂创建对象之前,需要为其定义模型,即一组类型名和各类型可拥有的属性。此模型约束可接受的属性类型。因此,举例来说,若模型里没有某项属性,则无法为数据对象添加该属性,不能将一种类型的数据对象指派给指定接受不同类型的属性。

在 SDO 的 PHP 实现中,可以通过三种方法初始化数据工厂,以开始创建数据对象。前两种方法使用现成的 DAS,以可能要读取或写入数据库或 XML 文件为前提。在这些环境中,DAS 创建并初始化数据工厂,但使之保持隐藏状态,自行提供一个接口来创建数据对象。得到数据工厂的另外一种方法是自行创建,使用 AddType()AddPropertyToType() 方法来定义模型 —— 就像 DAS 做的那样。尽管这很不方便,但我们会展示其简要形式,以使您清楚了解现成的 DAS 幕后的一切。

上文示例中所用到的两种类型定义如下:

$data_factory = SDO_DAS_DataFactory::getDataFactory();
$data_factory->addType('NAMESPACE', 'Author');
$data_factory->addType('NAMESPACE', 'Name');

您该记得,我们在 $author 对象上调用了 getTypeName(),结果为显示出名称 Author。此名称正是通过以上述方式调用 addType() 在 SDO 模型中设置的。

所有类型都位于一个名称空间中。此处名称空间设置为 NAMESPACE,但若不需要名称空间,直接将此处留空即可。

以下代码展示了如何向 Name 类型添加简单字符串属性 first

清单 10. 为 SDO 类型添加原生数据类型属性
$data_factory->addPropertyToType (
  'NAMESPACE' , 'Name',              	// adding to NAMESPACE:Name ...
  'first',                           	// ... a property called first ...
  SDO_TYPE_NAMESPACE_URI, 'String',  	// ... to take string values ...
  array('many' => false));           	// ... which is single-valued.

该调用看上去相当复杂,前两个参数指定了我们将为之添加属性的类型的名称空间和名称 —— 在本例中为 NAMESPACE:Name。第三个参数是所添加的属性的名称 —— 本例中为 first。第四个和第五个参数为新属性可接受的类型的名称空间和名称 —— 在本例中为 commonj.sdo:String(常量 SDO_TYPE_NAMESPACE_URI 的值为 commonj.sdo:,这是 SDO 模型中原生数据类型所在的名称空间)。最后一个参数是指定性质的关联数组,比如单值还是多值,并指定对象引用为包容还是非包容(本例中无此项)。

至此,进行这些调用后,将能够调用此数据工厂来创建类型为 Name 的数据对象,此对象有一个名为 first 的属性,要求被指派以字符串类型的对象。下面我们将简要示范。

清单 11 展示了从 AuthorName 的包容引用的指定方式:

清单 11. 为 SDO 类型添加一个包容引用属性
$data_factory->addPropertyToType (
  'NAMESPACE' , 'Author',           	// adding to NAMESPACE:Author ...
  'name',                           	// ... a property called name ...
  'NAMESPACE', 'Name',              	// to take objects of type NAMESPACE:Name ...
  array('many' => false, 'containment' => true));	// ... single-valued \
  and containment.

此处,为 Author 类型添加了一个名为 name 的属性,并将其约束为引用 Name 类型的对象。name 属性是一个单值包容引用。

必须再次强调,像这样自行定义数据工厂的类型和属性可能非常不方便。DAS 通常会替您完成这些任务,我们将在下一节介绍相关内容。

我们已看到的其他属性应以相同的方式添加。若添加之前的示例中所使用的 works 属性,则该属性应为多值的。

一旦将模型指定给数据工厂,就可以创建数据对象了。第一个对象仅能通过调用数据工厂或在使用的任一 DAS 上调用恰当的方法创建。此处,调用数据工厂来创建顶级对象,并传递类型名称和名称空间:

$author = $data_factory->create('NAMESPACE', 'Author');

有了一个对象之后,即可通过两种方法来创建子数据对象。直接的方法为再次调用数据工厂来创建恰当类型的对象,随后将其指派给其父对象的属性。

清单 12. 通过数据工厂调用创建并指派第二个数据对象
$name = $data_factory->create('NAMESPACE', 'Name');
$author->name = $name;

还有一种更通用的捷径。在现有对象上调用 createDataObject(),传递属性名称以包含新对象。

$name = $author->createDataObject('name');

此调用在模型中执行查找,查看 name 属性可指向何种类型的数据对象,为给定属性指派数据对象,随后返回创建好的数据对象。我们实际上将一个数据对象作为其引用的对象的数据工厂。一旦有了一个数据对象,则这是创建其他对象的一种更为通用的方法。

数据访问服务

现在让我们来看一下,如何使用 DAS 通过更为方便的方法完成上述任务。首先介绍一些背景知识。

SDO DAS 的任务是从存储器中载入数据,并将其转换为数据图,供应用程序处理,随后再将数据图写回存储。为 SDO 的 PHP 实现提供了两个 DAS:一个用于处理 XML 数据,另一个用于处理关系数据库。PHP 文档的相关章节分别详细讨论了这两个 DAS,本文只给出简要介绍。在本文的第二部分中,您还会看到一些使用 XML DAS 的示例。

这两个 DAS 都需要从初始化 SDO 模型入手,可通过调用前文介绍的 addType()addPropertyToType() 方法完成。模型需要匹配源 XML 或关系数据库中存在的类型、属性和关系,需要将此信息指定给 DAS。为两种 DAS 指定此信息的方法大为不同。

XML DAS 初始化其模型的方法是:读取和解析与准备载入的 XML 文档对应的 XML 模式定义文件(XSD 文件)。SDO V2.0 规范文档给出了从 XML 模式到 SDO 模型的映射规则,实质上就是任何复杂类型的元素都将成为模型中的一个 SDO 类型。元素之间的包容关系将由 SDO 之间的包容引用表示。字符串等简单类型将成为 SDO 上的原生数据类型属性。XML 属性(Attribute)也会成为 SDO 属性(property)。

模式定义文件通常在初始化时传递给 XML DAS,这是通过使用类上的静态 create() 方法实现的,如下所示:

$xmldas = SDO_DAS_XML::create('author.xsd');

为 XML DAS 定义好模型后,就可以使用 loadFile()saveFile() 方法分别从文件载入 XML 数据以及将 XML 数据保存到文件中;使用 loadString()saveString() 方法分别从字符串载入 XML 数据及将 XML 数据保存到字符串。还有一个 createDocument() 方法,可从头开始创建文档,无需从载入的文件入手。

Relational DAS 的初始化方式大为不同。它使用应用程序提供的数据,创建时这些数据将以关联数组的形式传递给 DAS。一个参数是描述数据库的关联数组集合:表名、列名、主键和外键。这种信息原则上应自动从数据库获取。尽管此功能尚未实现,但 Relational DAS 的未来版本或许将提供这一功能。应用程序必须提供的其他重要信息定义了数据库中的数据如何映射到数据图中,其类型应视为图的顶端,其外键应解释为包容类型等。

Relational DAS 掌握了模型后,即可用于将数据库中的数据作为 SDO 载入内存。给 Relational DAS 一个 SQL 查询供其执行,它将处理查询并将结果集置入 SDO 图。此后若更新对象(例如添加到图中或从图中删除),并在 Relational DAS 上调用 applyChanges,它会生成将更改应用回数据库所必需的 SQL 语句。关于 Relational DAS 的更多信息,请参阅 developerWorks 的其他文章和在线文档(参见 参考资料 一节)。

RSS 提要半成品

为阐明 SDO 的用法和 XML DAS,我们将开发一个示例应用程序,使用 SDO 来完成所有 XML 处理任务。应用程序分为两部分:一部分是一个简单的 blog 应用程序,它将 blog 的内容保存为 XML 文件;另一部分读取 XML 文件,并将其重新发布为 RSS 提要。您应发现,SDO 使 PHP 中的 XML 处理变得极其简单。相关代码请参见 下载 一节。

blog 应用程序

从结构上来看,我们的 blog 和 blog 应用程序都非常简单。第一个脚本显示一个 HTML 窗体,用户可在此窗体中以标题和描述输入新项目。图 3 展示了添加第一个项目的屏幕:

图 3. 向 blog 添加项目
向 blog 添加项目
向 blog 添加项目

提交此项目时,第二个脚本 —— 也就是第一个脚本所显示的窗体的目的地 —— 将数据写入 blog,并显示确认项目添加完毕的屏幕。

图 4. 确认
确认
确认

后面我们会介绍脚本,但首先来看一下只包含上面添加的这一个项目的 blog。

清单 13. 仅有一个项目的 blog
<?xml version="1.0" encoding="UTF-8"?>
<blog xsi:type="blog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <item>
    <title>Hello World</title>
    <description>The traditional opening in all fields of computing</description>
    <date>Thu, 6 Apr 2006 15:25:58 BST</date>
    <guid>b787c22d15b34b0eb29305e9ea17d8e9</guid>
    <from_ip>127.0.0.1</from_ip>
  </item>
</blog>

只有一个顶级元素 <blog>,其中包含许多 <item>。各元素都有标题、正文、日期和一个名为 guid(“globally unique ID” 的缩写,表示全局惟一 ID)的惟一键字段,这个字段是在创建项目时指派的。尽管 blog 本身并不需要此字段,但后面需要将其用作 RSS 提要的一部分。guid 的指派方法为:将其创建为输入项的日期和时间的散列。为了解谁执行过 blog 写入操作,我们还会捕获发信站点的 IP 地址。

由于 blog 为累积式的,我们希望读入一个与上例类似的 blog、添加项目并将其写出,因此使用 SDO XML DAS。回忆一下,XML DAS 通过读取 XML 模式文件获取 SDO 必须遵循的模型。与之相对应,必须提供一个 XML 模式文件。

清单 14. blog 的 XML 模式
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="blog">
     <xs:complexType>
       <xs:sequence>
         <xs:element name="item" maxOccurs="unbounded">
           <xs:complexType>
             <xs:sequence>
               <xs:element name="title" type="xs:string"/>
               <xs:element name="description" type="xs:string"/>
               <xs:element name="date" type="xs:string"/>
               <xs:element name="guid" type="xs:string"/>
               <xs:element name="from_ip" type="xs:string"/>
             </xs:sequence>
           </xs:complexType>
         </xs:element>
       </xs:sequence>
     </xs:complexType>
   </xs:element>
 </xs:schema>

您应能看出,此模式文件确实与上文中的示例文档相对应。我们的 blog 有一个名为 <blog> 的文档元素,包含无限制的项目序列,每个项目都有标题、描述等。

应用程序代码

如下 HTML 脚本将显示一个窗体,接受标题和描述(本例不需要 PHP 或 SDO):

清单 15. 为 blog 捕获项目的 HTML 页面
<html>
<head>
<title>Add an item to my half-baked blog</title>
</head>

<body>
<p>
<strong>Add an item to my half-baked blog</strong>
<br/>
<br/>

<form method="post" action="additem.php">
  Title:
  <br/>
  <input type="text" size="50" name="title"/>
  <br/>
  Description:
  <br/>
  <textarea rows="5" cols="50" name="description"></textarea>
  <br/>
  <input value="Submit" type="submit"/>
</form>
</p>
</body>
</html>

此脚本链接到第二个脚本 additem.php:

清单 16. 为 blog 添加项目的 PHP 脚本
<html>

<head>
<title>An item has been added to my half-baked blog</title>
</head>

<body>

<p><strong>An item has been added to my half-baked blog</strong><br />

<?php
/* initialize the XML DAS and read in the blog */
$xmldas                = SDO_DAS_XML::create('./blog.xsd');

/* read in and parse the XML instance document */
$xmldoc                = $xmldas->loadFile('./blog.xml');
$blog                  = $xmldoc->getRootDataObject();

/* create a new item and copy info from the html form */
$new_item              = $blog->createDataObject('item');
$new_item->title       = $_POST['title'];
$new_item->description = $_POST['description'];
$new_item->date        = date("D\, j M Y G:i:s T");
$new_item->guid        = md5($new_item->date);
$new_item->from_ip     = $_SERVER['REMOTE_ADDR'];
 
/* write the blog back to the file from whence it came */
$xmldas->saveFile($xmldoc,'./blog.xml',2);

echo "Title: "         . $new_item->title;
echo "<br/>";
echo "Description: "   . $new_item->description;
echo "<br/>";
echo "Date: "          . $new_item->date;
?>

</p>
</body>
</html>

我们在第二个脚本中初次使用了 SDO 和 XML DAS,所以尽管出人意料之处不多,但此脚本依然值得分析。

我们所做的第一件事就是使用 SDO_DAS_XML::create() 静态方法,该方法用包含 blog 描述的模式文件初始化 XML DAS。XML DAS 将解析模式文件,确定 SDO 的类型与属性模型形式如何,并在数据工厂上调用 addType()addPropertyToType(),以使用 SDO 模型进行初始化。

后续的 loadFile() 调用将读入并解析 XML 示例文档,返回一个表示文档的对象。假设我们正添加第二个项目,之前添加的项目已保存,其标题为 Hello World。背后的 loadFile() 重复调用 createDataObject() 以构建 SDO。假设正在载入的 blog 中已有一个项目,所以 XML DAS 将创建一个 blog 类型的 SDO 和一个 item 类型的 SDO。blog 数据对象将包含一个多值包容引用属性,名为 item,它指向包含一个项目的列表。我们在文档对象上调用 getRootDataObject() 时,将得到表示文档元素的 SDO —— <blog>。若在其上使用 var_dump(),将看到:

清单 17. var_dump() 所显示的 blog 信息
object(SDO_DataObjectImpl)#9 (1) {
  ["item"]=>
  object(SDO_DataObjectList)#10 (1) {
    [0]=>
    object(SDO_DataObjectImpl)#11 (5) {
      ["title"]=>
      string(11) "Hello World"
      ["description"]=>
      string(50) "The traditional opening in all fields of computing"
      ["date"]=>
      string(28) "Thu, 6 Apr 2006 15:25:58 BST"
      ["guid"]=>
      string(32) "b787c22d15b34b0eb29305e9ea17d8e9"
      ["from_ip"]=>
      string(9) "127.0.0.1"
    }
  }
}

您应可看出示例文档与模式之间的对应关系,SDO 数据图非常清晰。

接着看下面的代码行,我们创建了一个新项目,并复制通过 HTML 窗体输入的标题与描述。这一次,还获取当前日期与时间,生成 guid,并保存输入项目的 IP 地址。注意,应用程序通过在 blog SDO 上调用 createDataObject() 创建新项目,它传递属性名项目。这里要解释一下,这意味着在幕后,模型被监视着,发现 item 属性期望接受 item 类型的 SDO(第一个为属性名,第二个为类型名),随后创建一个 item 类型的数据对象,并将其指派给 SDO $blog 中的多值包容引用属性 item

最后,调用了 saveFile() 将 blog 写回其源 XML 文件。

假设我们添加的第二个项目标题为 “What next?”,则 blog 的形式如下:

清单 18. 带有新增项目的 blog
<?xml version="1.0" encoding="UTF-8"?>
<blog xsi:type="blog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <item>
    <title>Hello World</title>
    <description>The traditional opening in all fields of computing</description>
    <date>Thu, 6 Apr 2006 15:25:58 BST</date>
    <guid>b787c22d15b34b0eb29305e9ea17d8e9</guid>
    <from_ip>127.0.0.1</from_ip>
  </item>
  <item>
    <title>What next?</title>
    <description>At this point we need a witty remark</description>
    <date>Thu, 6 Apr 2006 15:46:27 BST</date>
    <guid>582befcb0ab4ac24c5b7966529097b5a</guid>
    <from_ip>127.0.0.1</from_ip>
  </item>
</blog>

RSS 简介

本节尽可能简单地介绍 RSS。尽管内容简洁扼要,但能帮助您理解我们将要创建的 XML 文档的结构,如果您未曾查看过 RSS 提要的内容,则本节将给您极大的帮助。

RSS 提要只是一种 XML 文档,旨在提供部分或全部 Web 站点内容的概要。典型的 RSS 提要包含站点上可用的最新文章列表,其中列出各文章的标题、内容摘要以及一个到正文的链接。就本文示例而言,我们将沿用面向新闻的范式,并从我们的 blog 中提取 RSS 提要。但在看到提取提要的代码之后,您立即就会理解,从拥有一个项目列表的简单结构中提取提要差不多总是这么简单。

很少有人按原样查看 XML 文件,大多数人都会使用所谓的提要阅读器 —— 一种简单的程序,用于读取和格式化提要。有很多种提要阅读器。我们使用一种小型但有代表性的 Windows® 提要阅读器来测试应用程序。有些阅读器支持花体字,但并非所有阅读器对提要的解释方式都是一致的,因此我们将坚持使用 RSS 的一个子集,并以所有提要阅读器都能恰当处理的方式来使用它。本文所用的提要阅读器可免费下载,包括 Awasu Personal Edition、SharpReader、Mozilla Thunderbird 以及 Mozilla Firefox 中的 Live Bookmarks 特性。在开发本应用程序的过程中,我们发现其中的 Awasu 最为有用,部分原因是它可按需要重新读取提要,还有一部分原因是 Awasu 能将提要按最初接收时的原样准确地显示出来。

RSS 模式文件

为处理 XML 文档和 XML DAS,首先需要一个 XML 模式文件,DAS 将用此文件来构建 SDO 模型。不存在官方认可的 RSS V2.0 模式文件,但在 Harvard Law 站点(参见 参考资料 一节)中可找到规范和提要示例。在 Web 上搜索 RSS20.xsd 时,可找到 RSS V2.0 模式文件。但搜索到的模式文件相当长,包含许多我们不想使用的元素,而且不能按照我们的要求正确地提取提要。因此,我们自行编写了一个极其简单的模式文件,仅定义需要的部分 RSS:

清单 19. 经简化的 RSS V2.0 子集模式
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="rss">
    <xs:complexType>
      <xs:attribute name="version" default="2.0" />
      <xs:sequence>
        <xs:element name="channel" type="channel" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>

  <xs:complexType name="channel">
    <xs:sequence>
      <xs:element name="title" type="xs:string"/>
      <xs:element name="link" type="xs:string"/>
      <xs:element name="description" type="xs:string"/>
      <xs:element name="copyright" type="xs:string"/>
      <xs:element name="language" type="xs:string"/>
      <xs:element name="webMaster" type="xs:string"/>
      <xs:element name="lastBuildDate" type="xs:string"/>
      <xs:element name="pubDate" type="xs:string"/>
      <xs:element name="item" type="item" minOccurs="0" maxOccurs="unbounded" />
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="item">
    <xs:sequence>
      <xs:element name="title" type="xs:string"/>
      <xs:element name="link" type="xs:string"/>
      <xs:element name="description" type="xs:string"/>
      <xs:element name="guid" type="guid"/>
      <xs:element name="pubDate" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>
  
  <xs:complexType name="guid">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="isPermaLink" use="optional" type="xs:boolean"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>

</xs:schema>

如果您还不习惯查看 XML 模式,那么清单 19 看上去可能令人畏缩。但它表达的内容如下:

  • 一个提要由单一 RSS 元素组成。
  • 这个 RSS 元素包含多个 channel 元素。
  • 各个 channel 元素包含多个 item 元素。
  • RSS 元素有一个属性:version。
  • 一个 channel 元素拥有多个其他元素,如仅出现一次的版权信息和项目列表。
  • 各个项目都有一个 title 元素、一个描述、一个包含所引用文章 URL 的链接、一个发表日期以及 guid

RSS 规范说明了 guid 的涵义。字符串应为文章所独有的。字符串可为无意义的标识符,也可为指向文章的 URL,在这种情况下,isPermaLink 属性应设置为 true,提要阅读器将其解释为链接。我们选择生成一个无意义的标识符,并使用 <link> 元素来指向文章。

提取提要

稍后我们将讨论从 blog 提取 RSS 提要的应用程序。首先来看一下,如果要提取提要的 blog 中仅有我们在上文中添加的那个项目,提要的形式如何。幸运的是,您能够看出此结构与为 RSS 提要编写的 RSS 模式有着怎样的对应关系。

清单 20. 以 RSS 提要形式输出的 blog
<?xml version="1.0" encoding="UTF-8"?>
<rss xsi:type="rss" \
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">
  <channel>
    <title>My half-baked feed - RSS/PHP edition</title>
    <link>http://localhost/rss/index.php</link>
    <description>Comment from Yours Truly</description>
    <copyright>All mine!</copyright>
    <language>en-gb</language>
    <webMaster>mfp</webMaster>
    <lastBuildDate>Thu, 6 Apr 2006 15:47:20 BST</lastBuildDate>
    <pubDate>Thu, 6 Apr 2006 15:47:20 BST</pubDate>
    <item>
      <title>Hello World</title>
      <link>http://localhost/rss/showitem.\
      php?id=b787c22d15b34b0eb29305e9ea17d8e9</link>
      <description>The traditional opening in all fields \
      of computing</description>
      <guid isPermaLink="false">b787c22d15b34b0eb29305e9ea17d8e9</guid>
      <pubDate>Thu, 6 Apr 2006 15:25:58 BST</pubDate>
    </item>
    <item>
      <title>What next?</title>
      <link>http://localhost/rss/showitem.\
      php?id=582befcb0ab4ac24c5b7966529097b5a</link>
      <description>At this point we need a witty remark</description>
      <guid isPermaLink="false">582befcb0ab4ac24c5b7966529097b5a</guid>
      <pubDate>Thu, 6 Apr 2006 15:46:27 BST</pubDate>
    </item>
  </channel>
</rss>

在 Awasu Personal Edition 提要阅读器中显示时,此提要形式如下图所示:

图 5. 提要在 Awasu 中的显示效果
提要在 Awasu 中的显示效果
提要在 Awasu 中的显示效果

若单击标题 Hello World 或 What next? —— 这两个标题旁都有一个小小的文档图标 —— Awasu 将前往项目 <link> 元素中的 URL。后文将予以介绍。

生成提要

现在来看看生成此提要的 PHP 脚本。

清单 21. 从 blog 中生成提要的 PHP 脚本
<?php
/* Write out the header to indicate XML follows */
header('Content-type: application/xml');

/* Construct an XML DAS using the schema for RSS */
$rss_xmldas             = SDO_DAS_XML::create('./rss.xsd');

/* Load an XML file that contains a few settings */
$rss_document           = $rss_xmldas->loadFile('./base.xml');
$rss_data_object        = $rss_document->getRootDataObject();

/* Set build and publish date on the channel */
$channel                = $rss_data_object->channel;
$channel->lastBuildDate = date("D\, j M Y G:i:s T");
$channel->pubDate       = date("D\, j M Y G:i:s T");

/* Open and load the blog, using a second XML DAS */             
$blog_xmldas            = SDO_DAS_XML::create('./blog.xsd');
$blog_document          = $blog_xmldas->loadFile('./blog.xml');
$blog_data_object       = $blog_document->getRootDataObject();

/* iterate through the items in the blog and for each one
 * create a corresponding item in the rss feed 
 */
foreach ($blog_data_object->item as $item) { 
    $new_channel_item              = $channel->createDataObject('item');

    $new_channel_item->title       = $item->title;
    $new_channel_item->description = $item->description;
    $new_channel_item->pubDate     = $item->date;
    $new_channel_item->link        = \
    "http://localhost/rss/showitem.php?id=" . $item->guid;

    $guid                          = $new_channel_item->createDataObject('guid');
    $guid->value                   = md5($new_channel_item->pubDate);
    $guid->isPermaLink             = false;
}

print $rss_xmldas->saveString($rss_document,2);
?>

此处的目标是为 RSS 提要构建一个数据对象,然后读入 blog,并在 RSS 提要中为各个 blog 项目创建一个相应的项目数据对象。可决定仅提供一些最新的文章(以特定日期为界限),但这里将复制所有项目。

首先,写出恰当的 HTTP 头,表明后接 XML 文档。随后使用 RSS 模式构建一个 XML DAS,并载入 XML 文件。此 XML 文件包含一些在每次生成的 RSS 提要间可能不会发生变化的设置 —— 版权声明、提要标题等。这是保存这些设置的好地方。之前我们轻松地实现了这些设置的硬编码,同样也可轻松地将其指派给脚本中 channel 数据对象的属性。

以这种方式初始化 RSS 提要的属性后,获得了 RSS 数据对象并设置了一些属性 —— 与生成时间相关的属性。可在 RSS V2.0 规范中找到所有这些属性的含义。

后三条语句打开并加载 blog,还会从文档中获取 blog 数据项目。我们使用第二个 XML DAS 来完成这一任务。现在有两个 DAS,分别使用不同的模型载入,因此这种方法非常完美。只要我们不希望一个 DAS 了解另外一个 DAS 所创建的数据对象,就不会出现任何问题。顺便提一下,将两个模式文件载入一个 DAS 应该是可行的,一个 DAS 可管理多个模式文件,但也就意味着使其公共类型名(title、description、item、guid)处于彼此分离的名称空间内。本文不讨论名称空间的用法问题,因此选择分别用不同的 DAS 载入两个模式文件。

现已将两个数据图载入内存。那么要完成 RSS 提要,只需循环遍历 blog 内的项目,并为每个项目在提要内创建一个相应项即可。注意,不能复制项目本身,不仅仅是因为 blog 中的项目结构与提要中的结构略有不同,而且因为不允许将一个 DAS 创建的项目复制到另一个 DAS 创建的图中。

提要中有了需要的所有信息后,应用程序使用 saveString() 将整个 XML 文档写出为一个字符串。为使其格式清晰可读,使用 saveString() 将各个级别缩进两格。

要载入的 XML 基文件如下:

清单 22. 包含提要硬编码值的基文件
<?xml version="1.0" encoding="iso-8859-1"?>
<rss 
  xsi:type="rss" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  version="2.0">
  <channel>
    <title>My half-baked feed - RSS/PHP edition</title>
    <link>http://localhost/rss/index.php</link>
    <description>Comment from Yours Truly</description>
    <copyright>All mine!</copyright>
    <language>en-gb</language>
    <webMaster>mfp</webMaster>
  </channel>
</rss>

显示单个项目的脚本

拼图的最后一块与提要中的 <link> 元素有关。各个项目的 <link> 元素指向 URL http://localhost/rss/showitem.php?...任何 RSS 阅读器都将 <link> 元素解释为访问实际文章所用的链接。在我们的示例中,所访问的是一个简单的页面,它显示项目内容。请注意,guid 位于 <link> 值的最后,这样 showitem.php 即可了解要显示哪个项目。脚本打开 blog,提取带有相应 guid 的项目,然后格式化此项目。代码如下:

清单 23. 显示一条特定项目的 PHP 脚本
<html>

<head>
<title>Show Item</title>
</head>

<body>
<p><strong>My half-baked feed</strong><br />

<?php
/* open and load the blog */
$xmldas    = SDO_DAS_XML::create('./blog.xsd');
$xmldoc    = $xmldas->loadFile('./blog.xml');
$blog      = $xmldoc->getRootDataObject();

/* get the id of the desired item from the URL */
$id        = $_GET['id'];   // id was inserted in the URL by the feed

/* use XPath to find the right item within the blog */
$item      = $blog["item[guid=$id]"]; 

echo "<br/>";
echo "The following item was added on " . $item->date;
echo " from ip address: " . $item->from_ip;
echo "<br/>";
echo "<br/>";
echo "Title: " . $item->title;
echo "<br/>";
echo "Description: " . $item->description;
echo "<br/>";
   
?>
</p>
</body>
</html>

此脚本使用 XSD 创建一个 DAS、载入一个文档并获取根数据对象 —— 一个您已非常熟悉的模式。现在,SDO 只有一个方面尚未介绍:使用类似于 XPath 的表达式来查找希望放入数据图的项目。整个 blog 将作为数据图载入内存,表达式 ["item[guid=$id]"](切记以 PHP 的实际 $id 值取代 $id)将解释为 XPath 搜索字符串。$item 将被指派给具有正确 ID 的项目。SDO 实现一个可以完成此任务的 XPath 子集。

图 6 展示了跟踪第一个项目链接时的显示效果 —— 同样使用了 Awasu:

图 6. Awasu 中显示的第一个项目
Awasu 中显示的第一个项目
Awasu 中显示的第一个项目

运行

或许仅仅阅读本文即可满足您的需求,但如果想下载代码并在自己的计算机上运行(或许还想略加修改),则以下提示可能会有所帮助。

首先请注意,在生成提要的 PHP 脚本中,要使用完整的 URL 指定链接。脚本要求所有文件都安装在 Web 服务器文档根目录下的 rss 子目录中 —— 例如,对于 Apache 来说,此目录应为 htdocs/rss。

不同提要阅读器所实现的与解释 guid 和链接相关的逻辑并非全部相同,特别是在链接未指定、isPermaLink 设置为 true 或未指定时更是如此。在某些情况下,我们观察到 SharpReader 接受 guid,并将其放置在交付提要的站点地址的末尾处,以便构建链接。我们还见过 Awasu 直接将一个 http:// 放在 guid 前边。根据本文介绍得到的最终模式 —— 一个链接及一个 isPermaLink 为 false 的 guid,与文中提到的所有提要阅读器配合良好。

某些提要阅读器有着类似邮箱的功能,如果它们看到一个带有给定 guid 的项目,那么该项目将一直显示在阅读器给出的提要视图中,即便最新的版本已不再有该项目。如果两个项目使用相同的 guid 结尾,或一个项目的 guid 发生了变化,那么也有可能出现类似的混乱。最好不要这样做,但在实验过程中,这些情况有可能发生,而且清除起来相当困难。

通常,从提要阅读器中删除提要已足够,但我们发现,对于 Thunderbird 来说,彻底清除一个提要的历史的惟一方法就是删除 Thunderbird 的 mail 下与 RSS News & Blogs 账户相对应的目录。
注意:务必谨慎,不要删除原本希望保留的其他东西,例如您的 mail 目录。

可以确信,至少您的 RSS 提要是通过检查 Web 服务器的日志记录提供的。如果使用 UNIX® 系统,或者使用带有 MKS 工具包或安装了 cygwin 的 Windows 系统,也可为其使用 tail -f 命令。

结束语

本文旨在介绍服务数据对象(Service Data Object),展示处理 SDO 的 API,并阐明 SDO 提供了在 PHP 中处理 XML 数据的便捷且自然的方法,以自然的形式表示 XML 文档中的结构化数据。我们并未宣称自己编写的是最高级的 blog 应用程序或者最高级的 RSS 提要生成器,但或许您会发现本文提供的应用程序非常有趣。

您已看到,为将 XML 与 SDO 配合使用,确实需要一个 XML 模式文件,XML DAS 将通过此文件初始化类型与属性模型,或许您还会发现这有些令人不快。如果已有了一个模式,SDO API 将管理所有数据图指派和变更,确保对数据和数据图的任何更改都将形成一个与模式一致的文档 —— 这是一种动态模式验证。也许您会发现这很有用。

我们断言,使用 SDO 的初始目标之一就是提供一种方式,在不依赖数据源的情况下处理结构化数据。本文未介绍 Relational DAS 的用法,希望您能理解,我们确实想过介绍相关内容。但如果那样,就需要初始化一个 Relational DAS 而不是 XML DAS,并需要通过一种完全不同的方式完成,而其他所有 SDO 操作都是相同的。

衷心希望您能成功地使用 SDO 在 PHP 中处理结构化数据!


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=159645
ArticleTitle=使用服务数据对象简化 PHP 中的 XML 处理
publish-date=09142006