65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2008 年 4 月 28 日

CPOL

14分钟阅读

viewsIcon

47418

标准 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 类比较特殊,因为它们与传统的接口不太一样。它们具有额外的功能,并允许引擎以一种特殊的方式挂钩到您的代码中。以下是对这些元素的简要描述:

ArrayAccessArrayAccess 接口允许您创建可以视为数组的类。在其他语言中,此功能通常由索引器提供。

Exception异常类已在第 4 章中介绍。SPL 扩展包含一系列针对此内置类的增强和分类。

IteratorIterator 接口使您的对象能够与 foreach 等循环结构一起工作。此接口要求您实现一系列方法,这些方法定义了哪些条目存在以及检索它们的顺序。

IteratorAggregateIteratorAggregate 接口在 Iterator 的概念上更进一步,允许您将 Iterator 接口所需的方法委托给另一个类。这允许您使用其他 SPL 内置迭代器类,从而在无需直接在类中实现 Iterator 方法的情况下获得迭代器功能。

SerializableSerializable 接口挂钩到 SerializeUnserialize 函数,以及可能自动序列化您的类的任何其他功能(例如会话)。使用此接口,您可以确保您的类能够正确地持久化和恢复。如果没有它,在会话中存储对象数据可能会导致问题,尤其是在使用资源类型变量时。

TraversableTraversable 接口由 IteratorIteratorAggregate 接口使用,以确定类是否可以使用 foreach 进行迭代。这是一个内部接口,用户无法实现;您需要实现 IteratorIteratorAggregate

在本章的其余部分,我们将仔细研究一些 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() 之后调用

foreach Iterator method flow

图 9-1. foreach 迭代器方法流程

迭代器的用途范围很广,从遍历对象到遍历数据库结果集,甚至遍历文件。在下一章中,您将了解内置迭代器类的类型及其用途。

迭代器辅助函数

有几个有用的便利函数可用于迭代器。

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 提供了 SplSubjectSplObserver 接口,如清单 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 展示了使用 SplSubjectSplObserver 的示例。

清单 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.phpsomeclass.incsomeclass.classsomeclass.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。

© . All rights reserved.