2007年6月26日 星期二

Iterators in PHP5

讀了石頭成的《為什麼還不升級PHP5》 以及 jaceju 於《PHP5 將滿 4 歲》.
一轉眼, PHP5 發表已經四個年頭了呀~真快.

個人覺得 PHP5(Zend Engine 2) 最大的演進, 在於 Exceptions / SPL / OO 的導入.
還記得四年前在 Zend 剛看到 PHP5 Exceptions/SPL/OO 的功能介紹, 內心的震撼很大.
因為它承襲了 java 的許多特性, 而這也意味著 PHP5 由 PHP3/PHP4 給人的 Scripting Language 印象,
成為一個更為強固的語言(那是什麼??? 還是 Scripting Language .. 來亂的).
##CONTINUE##

而 Exception Handle 的導入, 我們不再是利用 @function_name 或 set error_reporting 的方式來 "隱藏" 或"吃掉" 問題, 而是主動把可能發生例外的地方 try catch 起來, 這讓用 php5 開發的 web site 更加強固及可預測性.

而 SPL 則提供了類 Java 的 collections 架構. iterator interface, 這個在 Java 中是大量被使用的方法.
然而, 利用 Google 查詢一下, 可以發現討論和使用 SPL 及 Iterator 特點的 php 討論並不多..果然是會令 Zend 及 PHP5 開發團隊有些失落.
PHP5 擁有了類似 java 的優點及特性, 卻保有了 PHP 的精巧及簡便的思維來實作.. 這在當時, 讓阿土伯流了很多口水.

這篇著重在 PHP5 的 Iterator 這個觀念上.

Loop 迴圈

<?php

/* Looping over an array */
foreach ( $array as $item ) {
// Do something with $item
}

/* Looping over a MySQL result set */
while ( $row = mysql_fetch_array($result) ) {
// Do something with $row
}

/* Looping over files in a directory */
while ( false !== ($file = readdir($dir)) ) {
// Do something with $file
}
?>

上面是 php 中常見的 collections 或 datas 利用迴圈走訪每一筆資料的作法.. 它們看起來類似, 但是對於不同的資源(array, files, database) 又有些許的差異.

對於不同的物件及程式實作品, 你便需要知道每一個差異及輪巡的方式, 才能夠利用迴圈一一輪巡他們.

我們能夠提供一個標準的方式, 不論我們面對的是什麼資源(array, files, database, xml, ldap), 都只要使用相同且最直覺的方式輪巡嗎?

是的, 這就是 SPL 的 Iterator 的目的, 它不是解決您迴圈輪巡的效率問題, 它提供標準介面解決單一操作方式.


foreach($iterator as $obj){/* ...... */ }


Iterator of Array

<?php

$colors = array ('red','green','blue');
$iterator = new ArrayObject($colors);

foreach($iterator as $color) {
echo "color is: " . $color . "\n";
}
?>


OKOK!! 我知道大部份人會說, 不透過 ArrayObject 這個 Wrapper , 直接 foreach ($colors as $color) 也是可以.

沒錯, 同上所述, 在 array 的 iterator 上, 和傳統寫法是一致且類似的, 因為"單一操作"的解決 solution.

但在後面進階應用中 Iterator Iterator, 您會發現取得物件的 iterator 帶來的額外好處. 包含您對 ArrayIterator 加上 FilterIterator 或 LimitIterator.

順便說一下 java 的方式. 在 java 1.4 以前, 對於 collections 的 iterator 必需要


while(iterator.hasNext()) {
Object obj = iterator.next();
}

並加上 class cast 等.. 十分不方便, java 1.5 tiger 提供了類似 PHP5 SPL 的作法

for(Object obj : collection ) {
}

相信 php5 / java 1.5 和 ruby 的 collect.each 已經提供了相對優雅簡便的操作方式.


Turn Object to Iterator

前面提到的, 有時侯我們希望對物件輪巡多筆資料也變的很容易且直覺, 我們不再需要暴露出一個內部 properties array 讓使用者輪巡, 有時也可能內部資料不是個simple array(也許是properties, 也許是 data), 利用 SPL 實作的 Bookmark.php 範例


<?php

class Bookmark implements IteratorAggregate {
/* 使用 array 示範資料, 在 real case 中, 資料不一定是個 simple array */
private $links = array('阿土伯程式大觀園', 'google', 'delicious');

/* 部份原 Object 實作的專屬功能, 和 iterator 無關.... */
public function getResponseHtml() { /* ....... */ }
public function imRobot() { /* ....... */ }

/* 僅實作 getIterator 即可 */
public function getIterator() {
return new ArrayObject($this->links);
}
}

?>

<?php
$bookmark = new Bookmark();
foreach ($bookmark as $link) {
echo "link is: $link \n";
}
?>


而我們一樣只要使用標準的 foreach 語法, 即可對我們 Bookmark 物件輪巡資料, 而不再只是對 array foreach.
透過實作 IteratorAggregate 介面, 我們能為任何 Class 提供 getIterator 的功能並實現了此介面的功能.

註: 本文假設您已了解 Java/PHP5 Interface 的定義, 這裡的任何類別實作了 XXXX 介面, 即可提供 XXXX 功能.
實作 - 除了 keyword 的 implements 外, 您必須要在您的程式內"實作(現)" 介面中所定義抽象方法.
所有範例中, 都有實作所有定義的抽象方法, 而不僅於 class A implements IteratorAggregate {} 就存檔執行.


Iterator Iterator

在說明 Iterator Iterator 部份, 我以寫作一個類似 Unix 下的 find 指令為範例, 並示範了幾個 SPL 已經實作的幾個有用的 Iterator.

和 Java collections framework 相同, 您在使用 SPL 預設實作品時, 應保握一個原則, "優先考慮復合(composition), 然後才是繼承(inheritance)" ( Effective Java Programming Language Guide , item 14).

也就是優先考慮 has a , 然後才是 is a 的關係.

也就是說對於 ArrayObject / ArrayIterator / DirectoryIterator /RecursiveDirectoryIterator .... 等實作品, 在大部份情況下, 請不要繼承它, 而是復合它.


第一版的 RackFind, 我們先把目錄下的檔案全部列出來, DirectoryIterator 和 RecursiveDirectoryIterator 是 SPL 提供的目錄操作的方便 class , 差別在於後者會一併例出子目錄下的檔案.


<?php

class RackFind implements IteratorAggregate {

private $path = "";
private $file = "";

public function __construct($path, $file="") {
$this->path = $path;
$this->file = $file;
}

public function getIterator() {
return new RecursiveDirectoryIterator($this->path);
}
}

$find = new RackFind("c:/tmp", "test");
foreach($find as $f) {
echo $f->getFileName() . "\n";
}
?>


上面程式已能將 tmp 目錄下的所以檔案列出來了, 那 find 功能在哪呢??
我們需要為 RecursiveDirectoryIterator 提供一個 Filename FilterIterator. 完整範例二如下:

<?php

class RackFind implements IteratorAggregate {
private $path = "";
private $file = "";

public function __construct($path, $file="") {
$this->path = $path;
$this->file = $file;
}

public function getIterator() {
return new FilenameFilter(new RecursiveDirectoryIterator($this->path), $this->file);
}
}

class FilenameFilter extends FilterIterator {

private $file = "";

public function __construct($it, $file="") {
parent::__construct($it);
$this->file = $file;
}

public function accept(){
if (strlen($this->file) ==0) return true;
return ereg($this->file, $this->current()->getFileName());
}
}

$find = new RackFind("c:/tmp", "test");
foreach($find as $f) {
echo $f->getFileName() . "\n";
}
?>


OK~~ 現在只有檔名中包含 test 的會被 iterator 出來...

最後, 老板永遠是機車的, 一個 find 決對沒那麼容易, 假設老板說, 檔案太多, 可不可以分頁, 每頁只列出 5 個檔案...
這時, 我們讓我們的 Iterator 再經過 LimitIterator 的簡單實作.


<?php
class RackFind implements IteratorAggregate {

private $path = "";
private $file = "";
private $size =0;

public function __construct($path, $file="", $size=0) {
$this->path = $path;
$this->file = $file;
$this->size = $size;
}

public function getIterator() {
if ($this->size >0) {
return new LimitIterator(new FilenameFilter(new RecursiveDirectoryIterator($this->path), $this->file), 0, $this->size);
}else {
return new FilenameFilter(new RecursiveDirectoryIterator($this->path), $this->file);
}
}
}

class FilenameFilter extends FilterIterator {
private $file = "";

public function __construct($it, $file="") {
parent::__construct($it);
$this->file = $file;
}

public function accept(){
if (strlen($this->file) ==0) return true;
return ereg($this->file, $this->current()->getFileName());
}
}

$find = new RackFind("c:/tmp", "test", 5);
foreach($find as $f) {
echo $f->getFileName() . "\n";
}

?>



Iterator useful classes
最後, 你可以透過 spl_classes() 來得到 SPL 的實作 classes , 它提供了您很多的預設實作品, 大部份情況下都夠用的.

如 5.2.3 提供了:


Array
(
[AppendIterator] => AppendIterator
[ArrayIterator] => ArrayIterator
[ArrayObject] => ArrayObject
[CachingIterator] => CachingIterator
[DirectoryIterator] => DirectoryIterator
[EmptyIterator] => EmptyIterator
[FilterIterator] => FilterIterator
[InfiniteIterator] => InfiniteIterator
[IteratorIterator] => IteratorIterator
[LimitIterator] => LimitIterator
[NoRewindIterator] => NoRewindIterator
[OuterIterator] => OuterIterator
[ParentIterator] => ParentIterator
[RecursiveArrayIterator] => RecursiveArrayIterator
[RecursiveCachingIterator] => RecursiveCachingIterator
[RecursiveDirectoryIterator] => RecursiveDirectoryIterator
[RecursiveFilterIterator] => RecursiveFilterIterator
[RecursiveIterator] => RecursiveIterator
[RecursiveIteratorIterator] => RecursiveIteratorIterator
[RecursiveRegexIterator] => RecursiveRegexIterator
[RegexIterator] => RegexIterator
[SeekableIterator] => SeekableIterator
[SimpleXMLIterator] => SimpleXMLIterator
[SplFileInfo] => SplFileInfo
[SplFileObject] => SplFileObject
[SplObjectStorage] => SplObjectStorage
[SplObserver] => SplObserver
[SplSubject] => SplSubject
[SplTempFileObject] => SplTempFileObject
)


且 Adodb 最新的版本, 對於 ResultSet 也都支援 SPL 的 Iterator , 所以, 您都可以利用本篇的技巧處理它們.

$rs = $db->Execute($sql);
foreach($rs as $row) {
echo "r1=".$row[0]." r2=".$row[1]."
";
}

來取代傳統的.

while (!$recordSet->EOF) {
print $recordSet->fields[0].' '.$recordSet->fields[1].'
';
$recordSet->MoveNext();
}


透過 SPL Iterator , 不論您對到的是任何資料, 處理的方式都是相同, 這對於您將資料處理程式包成 DAO
來說, 不管您是後端是利用 XML, Array, Database , LDAP , 利用 iterator , 所有的呼叫方式及使用方式是相同的.

附錄 Adodb Iterator 實作, 也是相當直覺優雅
http://api.lifetype.org.tw/d5/ddf/adodb-iterator_8inc_8php-source.html

8 則留言:

Jace Ju 提到...

沒想到阿土伯也對 PHP5 感興趣,真是讓我太驚喜了~~

這篇談到的 Iterators 真的很不錯,這也是 PHP5 特色之一。

另外我先前也寫了一篇有關
SPL 的文章
,不過沒有您寫得詳細。然而我倒是體會到了 PHP 在 Iterator 上的強大,例如:

foreach (new RecursiveIteratorIterator($dir) as $entry)
{
    echo "$entry\n";
}

這段程式可以列舉某 $dir 目錄下所有子目錄,真的非常簡單而強大。

希望往後能和您多多交流 PHP5 的心得 :)

Unknown 提到...

Dear Jaceju:
在單一 Iterator 使用 foreach 所帶來的強大是"標準操作"的統一好處..
但 Iterator 之間的串接, 也就是 Decorator Pattern 才是它強大且讓整個專案精巧直覺的關鍵.
Java InputStream/OutputStream 就是大家最常接觸到的 Decorator Pattern.
myStringBuffer=new StringBuffer("This is a sample string to be read");
FilterInputStream myStream=new LineNumberInputStream
( new BufferInputStream( new StringBufferInputStream( myStringBuffer)));

匿名 提到...

"透過實作 IteratorAggregate 介面, 我們能為任何 Class 提供 getIterator 的能力."

這句話不太完整。因為 IteratorAggregate 只有一個抽象行為: getIterator() 。此行為必須回傳一個 primitive array 或實作 Traversable 介面的實例 (PHP 會檢查)。

但我卻覺得這個實現方式卻不夠直覺。在C++要overload一個operator時,直接定義方法就行了。哪需要什麼介面?我會想 PHP5 幹嘛不用 magic method 機制?舉例來說,來個 __getIterator() 的 magic method。嗯,效果一樣嘛。

Unknown 提到...

Dear 石頭成:
可能我是以一個 Java fan 的述語來陳訴 "實作" 介面的概念.
Java 中的 "實作", 表示了您必須在您的 Class 中 "提供實現" 該介面的程式碼...
而不是實作該介面即擁有該功能, 這在 java 中, 利用了提供 Abstract Adapter Class, 來省下您自行”提供實現"的程式碼.

小弟覺得 Traversable 本身沒有任何的 method 需要被實作,比較像是一個 "標示" 介面. 如同 java 中的 Cloneable or Serializable.
在 spl 文件中建議不要/也不能僅實作 Traversable (Abstract base interface that cannot be implemented alone. Instead it must be implemented by either IteratorAggregate or Iterator.)
而 Iterator 介面本身便是 implements Traversable.
http://www.php.net/~helly/php/ext/spl/interfaceTraversable.html

至於 Interface, 只養成習慣利用介面來操作物件的方式, 而不要直接操作物件, 這看起來像多餘, 但確是一個標準化以及未來專案擴充變更時的保命符..
REF: Effective Java Programming Language Guide.(Item 34: Refer to objects by their interfaces)

匿名 提到...

"不是實作該介面即擁有該功能"

嗯,你對 Java 的解釋有點怪。不知道你看的是哪本書,這樣的用字遣詞反而讓人愈看愈迷糊了。

介面是「只有純虛擬函數的抽象類別」。這意味著它只有函數的「原型宣告」而無「內容(程式碼)定義」。

implement 表示 "必須" 在此類別中實作介面的函數定義。換句話說,當我 implement 某介面,就表示我實現了此介面的功能。否則編譯器會擲出錯誤。

Unknown 提到...

Dear 石頭成:
我覺得我們在一個 implements / implemented 的英譯上打轉已失去了意義..
以依您的高見, 修改我 Blog 上的文字說明.
宣告的 implements (實作) , 而實際實作的程式碼 implemented as, 大部份的中文書亦稱為 (實作) .

為了表達不是只有宣告要實作(implements)一個介面, 並必須要實作 (implemented) 它, 這個 implemented 為了和 implements 區分, 我又形容它為 (提供實現).

《介面是「只有純虛擬函數的抽象類別」。這意味著它只有函數的「原型宣告」而無「內容(程式碼)定義」。

implement 表示 "必須" 在此類別中實作介面的函數定義。換句話說,當我 implement 某介面,就表示我實現了此介面的功能。否則編譯器會擲出錯誤。》
我實不知和您形容的差異有多大,或許我的中文表達實在有問題.

英文 implements 及 implemented as before.
http://java.sun.com/docs/books/tutorial/java/concepts/interface.html

匿名 提到...

Sun 的說法是「在類別宣告中使用關鍵字 "implements" 。這讓類別以更正式的形式承諾它提供了這些行為」。動作是宣告(declare, promise),名詞子句是 "implements this"。

This class declare it will implement this interface.

你看的中譯書,把名詞子句變成主格的動詞了。好的譯本,在這一點上就把握得很到位。國內翻譯者的水準參差不齊,尤其在程式語言這種專業書籍上差異更明顯。有時我也不得不多看幾本,再中英對照,才能知道正確意涵。

Unknown 提到...

Dear 石頭成:
這和中譯本作者水準及是否有把握得很到位無關.
是阿土伯看書不求慎解, 每一位作者的水準都比阿土伯高.
或許我文章一開始, 沒有先聲明 "本文重點在 Iterators in PHP5, 讀者必需先了解 Interface 概念".

在 Java 論壇中, 當有人問如: "我要如何在 Tomcat 中為每一個 Request/Response 轉 Charset"
答:"您可以實作 javax.servlet.Filter 介面, 然後將它加入 web.xml 即可"

又有人問:"我要如何監視 Session 的生成及銷毀, 並做後續的 Log 或其它動作"
答: "您可以實作 javax.servlet.http.HttpSessionListener介面, 然後將它加入 web.xml 即可"

這樣的簡式回答, 當然是假設讀者知道, 實作不是指在 class 後面跟個 implements keyword 而已, 而是必須把 interface 中的所有方法實作.
所以我文章中只有一語帶過 "您只要實作 XXX 即可", 因為我假設來看這篇文章的讀者, 不會只打 implements XXX 就存檔執行.

借用一句口頭禪: "謝謝指教"