Pro PHP:第 9 章:标准 PHP 库简介





5.00/5 (3投票s)
标准 PHP 库 (SPL) 为 PHP 中更高级的面向对象概念提供了支持。本示例章节介绍了 SPL 的一些功能,例如索引器和迭代器、观察者/报告者模式、数组重载等等。
引言
标准 PHP 库 (SPL) 是 PHP 5 的面向对象功能真正大放异彩的地方。它通过五种关键方式改进了该语言:迭代器、异常、数组重载、XML 以及文件和数据处理。它还提供了一些其他有用的项,例如观察者模式、计数、用于对象标识的辅助函数以及迭代器处理。此外,它还为自动加载类和接口提供了高级功能。本章将向您介绍这个非常重要的库,并在接下来的章节中,您将了解更多关于一些高级 SPL 类的内容。
SPL 默认启用,并且在大多数 PHP 5 系统上都可用。但是,由于 SPL 的功能在 PHP 5.2 版本中得到了极大的扩展,因此如果您想真正利用此库的优势,建议使用该版本或更新版本。
SPL 基础知识
SPL 是一系列 Zend Engine 2 的扩展、内部类和一套 PHP 示例。在引擎层面,SPL 实现了一组六个类和接口,它们提供了所有的“魔法”。
这些接口和 Exception
类比较特殊,因为它们与传统的接口不太一样。它们具有额外的功能,并允许引擎以一种特殊的方式挂钩到您的代码中。以下是对这些元素的简要描述:
ArrayAccess
:ArrayAccess
接口允许您创建可以视为数组的类。在其他语言中,此功能通常由索引器提供。
Exception
:异常类已在第 4 章中介绍。SPL 扩展包含一系列针对此内置类的增强和分类。
Iterator
:Iterator
接口使您的对象能够与foreach
等循环结构一起工作。此接口要求您实现一系列方法,这些方法定义了哪些条目存在以及检索它们的顺序。
IteratorAggregate
:IteratorAggregate
接口在Iterator
的概念上更进一步,允许您将Iterator
接口所需的方法委托给另一个类。这允许您使用其他 SPL 内置迭代器类,从而在无需直接在类中实现Iterator
方法的情况下获得迭代器功能。
Serializable
:Serializable
接口挂钩到Serialize
和Unserialize
函数,以及可能自动序列化您的类的任何其他功能(例如会话)。使用此接口,您可以确保您的类能够正确地持久化和恢复。如果没有它,在会话中存储对象数据可能会导致问题,尤其是在使用资源类型变量时。
Traversable
:Traversable
接口由Iterator
和IteratorAggregate
接口使用,以确定类是否可以使用foreach
进行迭代。这是一个内部接口,用户无法实现;您需要实现Iterator
或IteratorAggregate
。
在本章的其余部分,我们将仔细研究一些 SPL 功能,首先是迭代器。
迭代器
迭代器是实现 Iterator
接口的类。通过实现此接口,类可以在循环结构中使用,并可以提供一些高级数据访问模式。
迭代器接口
Iterator
接口在 C 代码中是内部定义的,但如果用 PHP 表示,它看起来可能类似于清单 9-1。
清单 9-1. 迭代器接口
interface Iterator {
public function current();
public function key();
public function next();
public function rewind();
public function valid();
}
注意
您无需自己声明
Iterator
或任何其他 SPL 接口。这些接口由 PHP 自动提供。所有可迭代对象都负责跟踪其当前状态。在普通数组中,这称为数组指针。在您的对象中,可以使用任何类型的变量来跟踪当前元素。记住元素的位置非常重要,因为迭代需要一次向前一个元素进行移动。
表 9-1 列出了 Iterator
接口中的方法。图 9-1 显示了 foreach
循环中 Iterator
接口方法的流程。
表 9-1. 迭代器接口方法
方法 | 描述 |
current() |
返回当前元素的值 |
key() |
返回当前键名或索引 |
next() |
将数组指针向前移动一个元素 |
rewind() |
将指针移到数组的开头 |
valid() |
确定是否存在当前元素;在调用 next() 和 rewind() 之后调用 |
迭代器的用途范围很广,从遍历对象到遍历数据库结果集,甚至遍历文件。在下一章中,您将了解内置迭代器类的类型及其用途。
迭代器辅助函数
有几个有用的便利函数可用于迭代器。
iterator_to_array($iterator)
:此函数可以接受任何迭代器,并返回一个包含迭代器中所有数据的数组。在处理迭代器时,它可以为您节省一些冗长的迭代和数组构建循环。
iterator_count($iterator)
:此函数返回迭代器中元素的准确数量,从而执行迭代器。
注意
如果您在没有定义结束点的迭代器上调用
iterator_to_array($iterator)
和 iterator_count($iterator)
函数,可能会产生一些“诡异”的行为。这是因为它们需要对整个迭代器进行内部执行。因此,如果您的迭代器的 valid()
方法永远不会返回 false
,请不要使用这些函数,否则您将创建一个无限循环。iterator_apply(iterator, callback, [user data])
:此函数用于将一个函数应用于迭代器的每个元素,方式与 array_walk()
用于数组的方式相同。清单 9-2 展示了一个简单的 iterator_apply()
应用。
清单 9-2. 使用 iterator_apply
function print_entry($iterator) {
print( $iterator->current() );
return true;
}
$array = array(1,2,3);
$iterator = new ArrayIterator($array);
iterator_apply($iterator, 'print_entry', array($iterator));
此代码输出以下内容:
123
只要回调函数返回 true
,循环就会继续执行。一旦返回 false
,循环就会退出。
数组重载
数组重载是将对象用作数组的过程。这意味着允许通过 []
数组语法进行数据访问。ArrayAccess
接口是此过程的核心,并为 Zend Engine 提供了所需的挂钩。
ArrayAccess 接口
ArrayAccess
接口在清单 9-3 中进行了描述。
清单 9-3. ArrayAccess 接口
interface ArrayAccess {
public function offsetExists($offset);
public function offsetSet($offset, $value);
public function offsetGet($offset);
public function offsetUnset($offset);
}
表 9-2 列出了 ArrayAccess
接口中的方法。
表 9-2. ArrayAccess 接口方法
方法 | 描述 |
offsetExists |
确定给定偏移量是否存在于数组中 |
offsetSet |
设置或替换给定偏移量处的数据 |
offsetGet |
返回给定偏移量处的数据 |
offsetUnset |
将给定偏移量处的数据置空 |
在接下来的章节中,您将接触到数组重载的一些高级用法。
计数和 ArrayAccess
在处理充当数组的对象时,通常有利的做法是允许它们像普通数组一样使用。然而,ArrayAccess
实现本身并不定义计数函数,也不能与 count()
函数一起使用。这是因为并非所有 ArrayAccess
对象都具有有限的长度。
幸运的是,有一个解决方案:Countable
接口。此接口就是为此目的而提供的,它定义了一个单独的方法,如清单 9-4 所示。
清单 9-4. Countable 接口
interface Countable {
public function count();
}
实现后,Countable
接口的 count()
方法必须返回 Array
对象中有效元素的数量。一旦实现了 Countable
,PHP 的 count()
函数就可以正常使用了。
观察者模式
观察者模式是一种非常简单的事件系统,它包含两个或多个交互的类。
此模式允许类观察另一个类的状态,并在被观察类的状态发生变化时收到通知。
在观察者模式中,被观察的类称为 *主题*(subject),而进行观察的类称为 *观察者*(observers)。为了表示这些,SPL 提供了 SplSubject
和 SplObserver
接口,如清单 9-5 和 9-6 所示。
清单 9-5. SplSubject 接口
interface SplSubject {
public function attach(SplObserver $observer);
public function detach(SplObserver $observer);
public function notify();
}
清单 9-6. SplObserver 接口
interface SplObserver {
public function update(SplSubject $subject);
}
其思想是,SplSubject
类维护一定状态,当该状态发生变化时,它会调用 notify()
。当调用 notify()
时,先前通过 attach()
注册的任何 SplObserver
实例的 update()
方法都将被调用。
清单 9-7 展示了使用 SplSubject
和 SplObserver
的示例。
清单 9-7. 观察者模式
class DemoSubject implements SplSubject {
private $observers, $value;
public function __construct() {
$this->observers = array();
}
public function attach(SplObserver $observer) {
$this->observers[] = $observer;
}
public function detach(SplObserver $observer) {
if($idx = array_search($observer,$this->observers,true)) {
unset($this->observers[$idx]);
}
}
public function notify() {
foreach($this->observers as $observer) {
$observer->update($this);
}
}
public function setValue($value) {
$this->value = $value;
$this->notify();
}
public function getValue() {
return $this->value;
}
}
class DemoObserver implements SplObserver {
public function update(SplSubject $subject) {
echo 'The new value is '. $subject->getValue();
}
}
$subject = new DemoSubject();
$observer = new DemoObserver();
$subject->attach($observer);
$subject->setValue(5);
清单 9-7 生成以下输出:
The new value is 5
观察者模式的好处在于,可以有许多或零个观察者附加到订阅者,而且您不需要事先知道哪些类将从您的主题类接收事件。
PHP 6 引入了 SplObjectStorage
类,它改进了此模式的冗余性。
此类类似于数组,但它只能存储唯一的对象,并且只存储这些对象的引用。它提供了一些好处。其中一个好处是您不能两次附加一个类,就像在清单 9-7 的示例中一样,因此,您可以防止对同一对象进行 *multipleupdate()* 调用。您还可以从集合中删除对象,而无需遍历/搜索集合,这提高了效率。
由于 SplObjectStorage
支持 Iterator
接口,您可以使用它在 foreach
循环中,就像普通数组一样。清单 9-8 展示了使用 SplObjectStorage
的 PHP 6 模式。
清单 9-8. SplObjectStorage 和观察者模式
class DemoSubject implements SplSubject {
private $observers, $value;
public function __construct() {
$this->observers = new SplObjectStorage();
}
public function attach(SplObserver $observer) {
$this->observers->attach($observer);
}
public function detach(SplObserver $observer) {
$this->observers->detach($observer);
}
public function notify() {
foreach($this->observers as $observer) {
$observer->update($this);
}
}
public function setValue($value) {
$this->value = $value;
$this->notify();
}
public function getValue() {
return $this->value;
}
}
class DemoObserver implements SplObserver {
public function update(SplSubject $subject) {
echo 'The new value is '. $subject->getValue();
}
}
$subject = new DemoSubject();
$observer = new DemoObserver();
$subject->attach($observer);
$subject->setValue(5);
清单 9-8 生成以下输出:
The new value is 5
序列化
SPL 的 Serializable
接口为一些高级序列化场景提供了支持。非 SPL 的序列化魔术方法 __sleep
和 __wakeup
有几个问题,SPL 接口解决了这些问题。
魔术方法无法序列化基类的 *private* 变量。您实现的 __sleep
函数必须返回一个要包含在序列化输出中的变量名数组。由于序列化发生的位置,基类的 *private* 成员会受到限制。
Serializable
移除了此限制,方法是允许您对父类调用 serialize()
,从而返回该类的序列化 *private* 成员。
清单 9-9 演示了魔术方法无法处理的场景。
清单 9-9. 魔术方法序列化
error_reporting(E_ALL); //Ensure notices show
class Base {
private $baseVar;
public function __construct() {
$this->baseVar = 'foo';
}
}
class Extender extends Base {
private $extenderVar;
public function __construct() {
parent::__construct();
$this->extenderVar = 'bar';
}
public function __sleep() {
return array('extenderVar', 'baseVar');
}
}
$instance = new Extender();
$serialized = serialize($instance);
echo $serialized . "\n";
$restored = unserialize($serialized);
运行清单 9-9 中的代码会产生以下通知:
Notice: serialize(): "baseVar" returned as member variable from __sleep()
but does not exist ...
O:8:"Extender":2:{s:21:"ExtenderextenderVar";s:3:"bar";
s:7:"baseVar";N;}
要解决此问题并正确序列化 baseVar
成员,您需要使用 SPL 的 Serializable
接口。该接口很简单,如清单 9-10 所示。
清单 9-10. Serializable 接口
interface Serializable {
public function serialize();
public function unserialize( $serialized );
}
当您实现 serialize()
方法时,它要求您返回表示对象的序列化 *string*;这通常通过使用 serialize()
函数来提供。
unserialize()
函数将允许您重建对象。它以序列化字符串作为输入。
清单 9-11 展示了基类的 *private* 成员的序列化。
清单 9-11. 序列化基类中的私有成员
error_reporting(E_ALL);
class Base implements Serializable {
private $baseVar;
public function __construct() {
$this->baseVar = 'foo';
}
public function serialize() {
return serialize($this->baseVar);
}
public function unserialize($serialized) {
$this->baseVar = unserialize($serialized);
}
public function printMe() {
echo $this->baseVar . "\n";
}
}
class Extender extends Base {
private $extenderVar;
public function __construct() {
parent::__construct();
$this->extenderVar = 'bar';
}
public function serialize() {
$baseSerialized = parent::serialize();
return serialize(array($this->extenderVar, $baseSerialized));
}
public function unserialize( $serialized ) {
$temp = unserialize($serialized);
$this->extenderVar = $temp[0];
parent::unserialize($temp[1]);
}
}
$instance = new Extender();
$serialized = serialize($instance);
echo $serialized . "\n";
$restored = unserialize($serialized);
$restored->printMe();
清单 9-11 的输出如下:
C:8:"Extender":42:{a:2:{i:0;s:3:"bar";i:1;s:10:"s:3:"foo";";}}
foo
如您所见,基类的 foo
值得到了正确地保留和恢复。清单 9-11 中的代码非常简单,但您可以将代码与 get_object_vars()
等函数结合使用,以序列化对象的所有成员。
Serializable
接口提供了一些其他好处。与在对象构造后调用的 __wakeup
魔术方法不同,unserialize()
方法是一种构造函数,它将提供机会,通过将构造输入存储在序列化数据中来正确构造对象。这与 __wakeup
不同,后者在类构造后调用,并且不接受任何输入。
Serializable
接口提供了许多高级序列化功能,并且比魔术方法方法能够创建更健壮的序列化场景。
SPL 自动加载
如果定义了 __autoload($classname)
魔术函数,则可以在首次使用类时动态加载它们。这使您可以弃用 require_once
语句。定义后,此函数将在每次调用未定义的类或接口时被调用。清单 9-12 演示了 __autoload($classname)
方法。
清单 9-12. __autoload 魔术方法
function __autoload($class) {
require_once($class . '.inc');
}
$test = new SomeClass(); //Calls autoload to find SomeClass
这并不是 SPL。但是,SPL 将此概念提升到了一个新的水平,引入了声明多个自动加载函数的能力。
如果您有一个大型应用程序,由几个不同的小型应用程序或库组成,每个应用程序可能希望声明一个 __autoload()
函数来查找其文件。问题在于,您不能简单地全局声明两个 __autoload()
函数而不出现重定义错误。幸运的是,解决方案很简单。
SPL 扩展提供的 spl_autoload_register()
函数消除了 __autoload()
的魔术功能,取而代之的是它自己的魔术。在调用 spl_autoload_register()
之后,对未定义类的调用将按顺序调用所有已注册到 spl_autoload_register()
的函数,而不是自动调用 __autoload()
。
spl_autoload_register()
函数接受两个参数:一个要添加到自动加载堆栈的函数,以及是否在加载器找不到类时抛出异常。第一个参数是可选的,默认为 spl_autoload()
函数,该函数会自动在路径中搜索小写类名,使用 .php 或 .inc 扩展名,或使用 spl_autoload_extensions()
函数注册的任何其他扩展名。您还可以注册一个自定义函数来加载缺失的类。
清单 9-13 展示了默认方法的注册、为默认 spl_autoload()
函数配置文件扩展名以及注册自定义加载器。
清单 9-13. SPL 自动加载
spl_autoload_register(null,false);
spl_autoload_extensions('.php,.inc,.class,.interface');
function myLoader1($class) {
//Do something to try to load the $class
}
function myLoader2($class) {
//Maybe load the class from another path
}
spl_autoload_register('myLoader1',false);
spl_autoload_register('myLoader2',false);
$test = new SomeClass();
在清单 9-13 中,spl_autoload()
函数将在包含路径中搜索 someclass.php、someclass.inc、someclass.class 和 someclass.interface。在路径中找不到定义后,它将调用 myLoader()
方法来尝试定位类。如果在调用 myLoader()
后仍未定义该类,将抛出关于类未正确声明的异常。
重要的是要记住,一旦调用了 spl_autoload_register()
,应用程序中其他地方的 __autoload()
函数可能会失效。如果不想这样,一个更安全的初始 spl_autoload_register()
调用将如清单 9-14 所示。
清单 9-14. 安全的 spl_autoload_register 调用
if(false === spl_autoload_functions()) {
if(function_exists('__autoload')) {
spl_autoload_register('__autoload',false);
}
}
//Continue to register autoload functions
清单 9-14 中的初始化首先调用 spl_autoload_functions()
函数,该函数返回已注册函数的数组,或者(如本例所示)如果 SPL 自动加载堆栈尚未初始化,则返回布尔值 false
。然后,您检查是否存在名为 __autoload()
的函数;如果存在,则将该函数注册为自动加载堆栈中的第一个函数,并保留其功能。之后,您可以继续注册自动加载函数,如清单 9-13 所示。
您还可以调用 spl_autoload_register()
来注册一个回调,而不是提供函数的 *string* 名称。例如,提供一个数组,如 array('class','method')
,将允许您使用对象的方法。
接下来,您可以通过调用 spl_autoload_call('className')
函数来手动调用加载器,而无需实际尝试使用该类。此函数可以与 function class_exists('className', false)
结合使用,以尝试加载类并在所有自动加载器都找不到该类时优雅地失败。
注意
class_exists()
的第二个参数控制它是否尝试调用自动加载机制。在自动加载模式下,spl_autoload_call()
函数已经与 class_exists()
集成了。清单 9-15 展示了一个使用 spl_autoload_call()
和 class_exists()
(非自动加载模式)的干净失败加载尝试的示例。
清单 9-15. 干净加载
//Try to load className.php
if(spl_autoload_call('className')
&& class_exists('className',false)
) {
echo 'className was loaded';
//Safe to instantiate className
$instance = new className();
} else {
//Not safe to instantiate className
echo 'className was not found';
}
对象标识
有时为类的每个实例拥有一个唯一的代码是有益的。为此,SPL 提供了 spl_object_hash()
函数。清单 9-16 展示了它的调用。
清单 9-16. 调用 spl_object_hash
class a {}
$instance = new a();
echo spl_object_hash($instance);
此代码生成以下输出:
c5e62b9f928ed0ca74013d3e85bbf0e9
在单个调用上下文中,每个哈希保证对每个对象都是唯一的。
重复执行很可能产生相同的哈希,但不能保证产生重复的哈希。在同一调用中对同一对象的引用保证是相同的,如清单 9-17 所示。
清单 9-17. spl_object_hash 和引用
class a {}
$instance = new a();
$reference = $instance;
echo spl_object_hash($instance) . "\n";
echo spl_object_hash($reference) . "\n";
清单 9-17 生成以下输出:
c5e62b9f928ed0ca74013d3e85bbf0e9
c5e62b9f928ed0ca74013d3e85bbf0e9
这些数据类似于 comparison === operator
;然而,某些用途可能受益于哈希码方法。例如,在数组中注册对象时,可以使用哈希码作为键,以便于访问。
事实要点
在本章中,您了解了 SPL。接下来的章节将在此基础上进行扩展。
迭代器可用于循环结构。SPL 提供了 Iterator
接口,以及一些迭代器辅助函数,包括 iterator_to_array()
、iterator_count()
和 iterator_apply()
。数组重载允许您将对象视为数组。
SPL 包括 Countable
接口。您可以使用它来挂钩全局 count()
函数,用于您自定义的类数组对象。
使用 SPL 观察者模式和 PHP 6 特有的 SplObjectStorage
类,您可以让某些对象监视其他对象的变化。
SPL 自动加载由 spl_autoload()
、spl_autoload_register()
、spl_autoload_functions()
、spl_autoload_extensions()
和 spl_autoload_call()
函数提供。
对象标识由 spl_object_hash()
函数提供。在同一调用中对同一对象的引用保证是相同的。
在下一章中,您将接触到一些更高级的迭代器模式,所以请务必牢记关于迭代器及其辅助函数的知识。
此示例内容摘录自书籍 Pro PHP: Patterns, Frameworks, Testing and More,作者 Kevin McArthur,版权所有 2008,Apress, Inc.
本书的源代码可在 http://apress.com/book/downloadfile/3934 处供读者获取。
KEVIN MCARTHUR 是一位开源开发者,居住在加拿大不列颠哥伦比亚省。他是一位自学成才的企业家,八年来一直经营着一个非常成功的 PHP 应用程序开发工作室。他的公司 StormTide Digital Studios 曾与美国和加拿大的行业合作,为 Web 统计、VoIP 和打印自动化提供扩展解决方案。作为一名活跃的 IRC 用户,Kevin 协助管理着最大的 PHP 支持组织之一 PHP EFnet。Kevin 对开源项目(包括 Zend Framework)的贡献使他成为业界公认的权威。他曾为 PHPRiot.com 撰写过多篇文章,主题包括反射、标准 PHP 库、面向对象编程和 PostgreSQL。