あっぽログ
← 記事一覧に戻る

PHPのジェネレータを使いこなす:yieldで省メモリな反復処理を実現する

ジェネレータとは何か

PHPには**ジェネレータ(Generator)**という、メモリ効率の良い反復処理を実現する仕組みがあります。PHP 5.5から導入されたこの機能は、大量のデータを一度にメモリへ展開せず、必要なときに1件ずつ値を生成できるのが特徴です。

通常の配列ですべての値を返す関数と比較してみましょう。

<?php

// 通常の関数:すべての値を配列に格納してから返す
function rangeArray(int $start, int $end): array
{
    $result = [];
    for ($i = $start; $i <= $end; $i++) {
        $result[] = $i;
    }
    return $result; // 100万件なら100万件分のメモリを消費
}

// ジェネレータ:1件ずつ値を生成する
function rangeGenerator(int $start, int $end): Generator
{
    for ($i = $start; $i <= $end; $i++) {
        yield $i; // ここで一時停止し、呼び出し元に値を返す
    }
}

foreach (rangeGenerator(1, 1_000_000) as $value) {
    // 常に1件分のメモリしか使わない
}

yieldキーワードが登場したとき、関数は値を返して一時停止します。次に呼び出されると、停止した箇所から処理が再開されます。この仕組みによって、大量データでもメモリ消費を最小限に抑えられます。

yieldの基本的な使い方

キーと値のペアを返す

yieldは配列と同様に、キーと値のペアで値を生成できます。

<?php

function indexedColors(): Generator
{
    yield 'red'   => '#FF0000';
    yield 'green' => '#00FF00';
    yield 'blue'  => '#0000FF';
}

foreach (indexedColors() as $name => $hex) {
    echo "{$name}: {$hex}\n";
}
// red: #FF0000
// green: #00FF00
// blue: #0000FF

yieldの戻り値を受け取る

send()メソッドを使うと、外部からジェネレータに値を渡すことができます。yield式の評価値として受け取れます。

<?php

function logger(): Generator
{
    while (true) {
        $message = yield; // 外から値を受け取る
        if ($message === null) {
            return;
        }
        echo '[LOG] ' . $message . "\n";
    }
}

$log = logger();
$log->current(); // ジェネレータを最初のyieldまで進める

$log->send('アプリケーション起動');
$log->send('ユーザーがログイン');
$log->send('処理完了');
// [LOG] アプリケーション起動
// [LOG] ユーザーがログイン
// [LOG] 処理完了

実践的な使用例

CSVファイルの行単位読み込み

大きなCSVファイルを処理するときにジェネレータは非常に効果的です。

<?php

function readCsvLines(string $filepath): Generator
{
    $handle = fopen($filepath, 'r');
    if ($handle === false) {
        throw new RuntimeException("ファイルを開けません: {$filepath}");
    }

    try {
        while (($line = fgetcsv($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle); // 必ずファイルを閉じる
    }
}

// 使い方
foreach (readCsvLines('large_data.csv') as $row) {
    // 1行ずつ処理。ファイル全体をメモリに読み込まない
    [$id, $name, $email] = $row;
    echo "ID: {$id}, 名前: {$name}\n";
}

ページネーション付きAPIの取得

外部APIを複数ページにわたって取得する処理もジェネレータでスッキリ書けます。

<?php

function fetchAllUsers(string $baseUrl): Generator
{
    $page = 1;

    while (true) {
        $response = file_get_contents("{$baseUrl}?page={$page}");
        $data = json_decode($response, true);

        if (empty($data['users'])) {
            return; // データがなければ終了
        }

        foreach ($data['users'] as $user) {
            yield $user; // 1ユーザーずつ返す
        }

        if (!isset($data['next_page'])) {
            return; // 次のページがなければ終了
        }

        $page++;
    }
}

foreach (fetchAllUsers('https://api.example.com/users') as $user) {
    echo $user['name'] . "\n";
}

yield fromで別のジェネレータに委譲する

PHP 7から導入されたyield fromを使うと、別のジェネレータやイテラブルに処理を委譲できます。

<?php

function innerGenerator(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}

function outerGenerator(): Generator
{
    yield 0;
    yield from innerGenerator(); // 内部のジェネレータに委譲
    yield 4;
}

foreach (outerGenerator() as $value) {
    echo $value . "\n";
}
// 0, 1, 2, 3, 4

yield fromは配列にも使えます。

<?php

function combined(): Generator
{
    yield from [10, 20, 30];
    yield from new ArrayIterator([40, 50]);
    yield 60;
}

通常の配列との使い分け

観点配列ジェネレータ
メモリ消費全件分常に1件分
再利用性何度でも一方向のみ
ランダムアクセス可能不可
向いているケース小〜中規模データ大量データ・無限列

ジェネレータはすでに処理した値に戻れません。もし複数回ループしたい場合は、iterator_to_array()で配列に変換するか、通常の配列を使いましょう。

<?php

$gen = rangeGenerator(1, 5);
$array = iterator_to_array($gen); // [1, 2, 3, 4, 5]

まとめ

PHPのジェネレータは、大量データの処理やストリーム処理においてメモリ効率を大幅に改善できる強力な機能です。

  • yieldで値を1件ずつ返して処理を一時停止できる
  • キーと値のペアやsend()による双方向通信も可能
  • yield fromで別のジェネレータや配列に委譲できる
  • CSVや外部API取得など、大量・逐次データの処理に最適

最初は難しく感じるかもしれませんが、まずは大きなファイルや大量レコードの処理でジェネレータに置き換えてみると、その恩恵を実感できるはずです。

← 記事一覧に戻る