| 接口 | 用途 | 必須實現的方法 |
| ArrayAccess | 對象支持 `$obj['key']` 讀寫 | offsetExists, offsetGet, offsetSet, offsetUnset |
| Iterator | 對象支持 `foreach ($obj as $k => $v)` | current, key, next, rewind, valid |
| Countable | 支持 `count($obj)` | count() |
| Serializable | 自定義序列化 | serialize(), unserialize() |
完整示例:虛擬文件系統節點
場景:將目錄結構映射為對象樹,支持 `$node['subdir']` 訪問子節點、`foreach` 遍歷當前層級、懶加載文件內容。
<?php
class FSNode implements ArrayAccess, Iterator, Countable {
private string $path; // 物理路徑
private array $children = []; // 緩存的子節點 [name => FSNode]
private int $position = 0; // Iterator 內部指針
private array $keys = []; // 子節點名稱列表
public function __construct(string $path) {
$this->path = rtrim($path, '/');
}
// ===== ArrayAccess =====
public function offsetExists(mixed $offset): bool {
$this->loadChildren();
return isset($this->children[$offset]);
}
public function offsetGet(mixed $offset): mixed {
$this->loadChildren();
return $this->children[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void {
throw new LogicException('FSNode 是只讀的');
}
public function offsetUnset(mixed $offset): void {
throw new LogicException('FSNode 是只讀的');
}
// ===== Iterator =====
public function current(): mixed {
$this->loadChildren();
$key = $this->keys[$this->position] ?? null;
return $key ? $this->children[$key] : null;
}
public function key(): mixed {
return $this->keys[$this->position] ?? null;
}
public function next(): void {
$this->position++;
}
public function rewind(): void {
$this->position = 0;
}
public function valid(): bool {
$this->loadChildren();
return isset($this->keys[$this->position]);
}
// ===== Countable =====
public function count(): int {
$this->loadChildren();
return count($this->children);
}
// ===== 內部邏輯 =====
private function loadChildren(): void {
if ($this->children !== []) return; // 已緩存
if (!is_dir($this->path)) return;
$items = scandir($this->path);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$full = $this->path . '/' . $item;
if (is_dir($full) || is_file($full)) {
$this->children[$item] = new self($full);
$this->keys[] = $item;
}
}
}
// 便利方法
public function isDir(): bool { return is_dir($this->path); }
public function isFile(): bool { return is_file($this->path); }
public function getContent(): string|false {
return $this->isFile() ? file_get_contents($this->path) : false;
}
public function getPath(): string { return $this->path; }
}
使用效果
$root = new FSNode('/var/www/html');
// 像數組一樣訪問
$config = $root['config']['database.php'];
if ($config) {
echo $config->getContent(); // 直接讀取文件內容
}
// 遍歷
foreach ($root as $name => $node) {
echo $name . ($node->isDir() ? '/' : '') . "\n";
}
// 計數
echo "項目數: " . count($root);
關鍵設計要點
- 懶加載:`loadChildren()` 僅在首次訪問時掃描目錄,避免構建整棵樹的性能開銷。
- 只讀語義:`offsetSet/Unset` 拋出異常,明確傳達不可變意圖。
- Iterator 狀態隔離:`position`、`keys` 為實例屬性,多重 `foreach` 互不干擾(PHP 內部會 clone 迭代器)。
- 配合 `__get/__set`:可進一步支持 `$node->config` 寫法,但需注意與 `ArrayAccess` 優先級(`__get` 只在屬性不存在時觸發)。
進階:實現 JsonSerializable
public function jsonSerialize(): array {
$this->loadChildren();
$data = [];
foreach ($this->children as $name => $node) {
$data[$name] = $node->isFile()
? ['type' => 'file', 'size' => filesize($node->getPath())]
: ['type' => 'dir', 'children' => $node]; // 遞歸序列化
}
return $data;
}
// json_encode($root) 直接輸出結構化 JSON
總結
實現 `ArrayAccess` + `Iterator` + `Countable` 只需十幾行樣板代碼,卻能賦予對象原生數組般的操作體驗,極大提升 API 易用性。適用於集合類、樹形結構、配置對象、查詢結果集封裝等場景。
- 上一篇文章: 64 位 Windows IIS 運行 PHP 報錯「%1 不是有效的 Win32 應用程式」與「Service Unavailable」完整解決方案
- 下一篇文章: Yii2 通過 .htaccess 隱藏 index.php 實現美觀 URL
Copyright ©
程式碼之家 All Rights Reserved