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
張貼留言