導入
個人開発にハマり中の新卒のエンジニアです。
簡単な自己紹介は以下に
- 私立文系卒
- 大学2年の冬から
- 学生時代は1年半ほどプログラミングスクールでインターン
- めちゃめちゃ柏レイソルサポーター
- 本当に今年は最高
今回は、PHPのIterator(イテレータ)についてまとめました。
僕があまりイテレータについて理解していなかったためです。
「普通に、foreachで繰り返し処理回せばよくね?」
と思っていました。
ですが、単なるforeachでは「もう少し綺麗に・柔軟に扱いたい…」という瞬間が必ず来ます。
そんな時に登場するのが Iterator です
操作位置を自由に操り、同じコレクションを何度でも再利用できる秘密兵器。
Iterator(イテレータ)の概要
Iteratorとは
コレクションクラスを繰り返し処理しやすいようオブジェクトとして実装したものです。
おそらくこれでは少しわかりづらいと思うので、具体例を交えていきます。
具体例:注文管理
顧客から受けた注文を管理するシステムがあるとします。
システム構成を考えた際に、それぞれの注文をオブジェクト化したOrderエンティティが必要だと考えられるでしょう。
おそらく、以下の感じ。※簡易的
class Order
{
public function __construct(
public int $id,
public int $amount, // 金額
public string $createdAt, // 'Y-m-d H:i:s'
) {}
}
インスタンスが生成された際、とりあえずコンストラクタで、
- 識別子のID
- 購入金額
- いつ注文されたか
これらが初期化されるでしょう。
注文を管理する側から見ると、このOrderエンティティが数えきれないほど多くある場合を考えてみましょう。
注文が殺到する人気ブランド、ECサイト、いくらでもあるでしょう。
受注した視点で考えると、この多くの注文を管理しなければいけません。
例えば、ダッシュボードに表示して管理するとしましょう。
この「Recent Orders」みたいに、注文を列挙して一覧できるなんてよくありそうな仕様です。
勘の鋭い人なら分かるでしょう。
この列挙に繰り返し処理が用いられているのです。
[
{
"id": 1, // 注文番号
"customerName": "John Doe", // 顧客名
"date": "2023-09-01", // 注文日 (YYYY‑MM‑DD)
"status": "Shipped", // 状態: Shipped | Pending | Cancelled など
"total": 100.00, // 合計金額
"currency": "USD" // 通貨 (UI では $100.00 として表示)
},
{
"id": 2,
"customerName": "Jane Smith",
"date": "2023-08-30",
"status": "Pending",
"total": 250.00,
"currency": "USD"
},
以下省略
]
大体はこんなJSONで渡されてくるのではないでしょうか。
Iteratorの実装
複数の注文をリストとして管理するために、OrderListクラスを作ってみましょう。
class OrderList implements Iterator
{
private array $orders; // 走査対象
private int $pos = 0; // 現在位置
public function __construct(array $orders)
{
$this->orders = $orders;
}
public function current(): mixed { return $this->orders[$this->pos]; }
public function key(): mixed { return $this->pos; }
public function next(): void { ++$this->pos; }
public function rewind(): void { $this->pos = 0; }
public function valid(): bool { return isset($this->orders[$this->pos]); }
※一旦、以下省略
}
Listというくらいなので、必ずと言ってよいほど繰り返し処理をするでしょう。
ここにIteratorインターフェースを実装すると、Iteratorを使う準備完了です。
Iteratorインターフェースには、実装なければならない5つのメソッドがあります。
interface Iterator {
public current(): mixed; // 繰り返し処理内で、現在の処理対象の値
public key(): mixed; // 繰り返し処理内で、現在の処理対象のキー
public next(): void; // 繰り返し処理内で、処理対象を次に
public rewind(): void; // 繰り返し処理内で、処理対象を先頭に初期化
public valid(): bool; // 繰り返し処理内で、次に処理する要素はあるかboolで返す
}
それぞれコメント通りの役割を与えられています。
これらのメソッドを実装し、繰り返しロジックを自らカスタマイズすることができます。
※何も0から順番に+1ずつって訳ではないってことね
rewind()って必要?
こう思われた方もいるのではないでしょうか。
僕は思いました。
繰り返し処理内で、処理対象を先頭に初期化することで、
OrderListを何度もインスタンス化しなくてよくなります。
一度、繰り返し処理を施したコレクションを再利用することができるようになります。
※これは以下で再度説明します。
考えられる繰り返し処理が必要なロジック
今回は注文エンティティをリストとしてダッシュボードに表示する、いわゆる管理画面的なシステムを想定しています。
であれば、何らかの条件でOrderエンティティを並び替えるという機能が存在してもよいのではないでしょうか。
この繰り返し処理を実装していく中で、Iteratorの利点について説明していくことにしましょう。
以下は先ほどのコードに追加する形です。
class OrderList implements Iterator
{
※他にも先ほどのプロパティ
private string $sort = 'newest'; // 'newest' | 'amount'
※他にも先ほどの5つのメソッド
public function setSort(string $mode): void
{
if (!in_array($mode, ['newest', 'amount'], true)) {
throw new InvalidArgumentException('unknown sort mode');
}
$this->sort = $mode;
$this->applySort();
$this->rewind(); // 先頭に戻す
}
private function applySort(): void
{
switch ($this->sort) {
case 'newest': // 新着順:createdAt が新しいものから
usort($this->orders,
fn($a, $b) => strcmp($b->createdAt, $a->createdAt));
break;
case 'amount': // 金額順:少ない順
usort($this->orders,
fn($a, $b) => $a->amount <=> $b->amount);
break;
}
}
}
デフォルトでは新着順でソートするロジックです。
処理フロー
ではこの一連の処理で、Iteratorはどこに必要なのか、以下に図示しました。
┌─① UI: 並べ替えボタンが押される─────────────────────────────────┐
│ Controller / ViewModel などが $list->setSort('amount') を呼ぶ │
└────────────────────────────────────────────────────┘
↓
1) OrderList::setSort('amount')
├─1-a モード検証('newest' または 'amount' 以外なら例外)
├─1-b $this->sort に 'amount' を代入
├─1-c OrderList::applySort() ← ★ 並べ替え実施
│ └─ usort() で金額順に並び替え
└─1-d OrderList::rewind() ← ★ 走査位置を先頭に戻す
└─ $this->pos = 0
※ ここでデータは“金額順”に入れ替わり、内部ポインタも 0 行目に戻った
──────────────────────────────────────────────
┌─② View: foreach ($lists as $list) { … } が始まる──────────────┐
└──────────────────────────────────────────────
┌─❶ OrderList::valid() → true ? ──┐
│ │ trueなら続行 / falseでループ終了
│ ┌─❷ OrderList::current() │ // 現在行の Order を返す
│ │ │ (key() も必要ならここで呼ばれる)
│ │ → テンプレートが $list を描画 │
│ │ │
│ └─❸ OrderList::next() │ // $this->pos++ で次へ進む
└────❹ ループ先頭へ戻る─────────────┘
↳ valid() が false になった瞬間にレンダリング終了
ここで発揮するIteratorの利点は以下です。
- View側は繰り返しロジックを把握できない
- 拡張性が担保されている
- 再利用可能
viewでは渡されてきたオブジェクトをforeachでレンダリングしているだけです。
Iteratorの繰り返し処理は把握していません。
そもそもフロントエンドにロジックを実装すること自体あまり好ましくありません。
また、Iteratorの利点として、オブジェクトを再利用可能なことが挙げられます。
OrderListインスタンスのordersプロパティには、複数のOrderエンティティが配列として入っています。
ソートなどの繰り返し処理を再度走らせる際にも、同じインスタンスのordersプロパティに再度ソートをかける形になります。
これを可能にしているのが、rewind()です。
ordersプロパティに対する繰り返し処理の、操作位置を先頭に戻すことで、何度も同じインスタンスとして再利用できるわけです。
Iteratorによる拡張性
public function current(): mixed { return $this->orders[$this->pos]; }
public function key(): mixed { return $this->pos; }
public function next(): void { ++$this->pos; }
public function rewind(): void { $this->pos = 0; }
public function valid(): bool { return isset($this->orders[$this->pos]); }
で行われる繰り返し処理って、、、
for($i = 0 ; $i < count($this->orders) ; $i++ )
{
return $this->orders[$i];
}
と変わらないじゃん、思うでしょう。
シンプルなデータ群を、シンプルなロジックで扱う分にはIteratorはあまり必要ではないでしょう。
ただ、複雑なものになってくると、forのスクリプトブロックに多くの処理を書かなければなりません。
それを分割するのにもIteratorは有効なのです。
current()、key()、next()、rewind()、valid()の役割に合わせて、処理を分散させることができます。
それゆえ、拡張性も上がるのです。
- next()を編集することで、要素をスキップすることができそう
- valid()やnext()を編集することで、遅延ローディングなどができそう
まとめ
- 同じインスタンスで何度も繰り返し処理を走らせる場合に有効
- ロジックを追加しやすい
- 振る舞いはViewからは把握できない
ロジックの分散、拡張性、カプセル化など、
考えなければならないことの、レベルが上がるほど、Iteratorを使う場面は増えてくることでしょう。