yano3nora
8/23/2017 - 10:15 AM

[php: Generator] count function as generator sample. - #php

[php: Generator] count function as generator sample. - #php

OVERVIEW

ジェネレータとは - php.net

PHP 5.5 より実装された「独自イテレータを簡単に実装できる」機能で、大体どの言語にも存在する。通常の関数は return を一度だけ返すが、ジェネレータオブジェクトはイテレーションの中で実行結果を何度も yield する。端的に言うと 関数の動きを一時停止して、再度再開することが出来る

  • クロージャ ( コールバック ) の受け渡しだけでは実装できないような「処理の共通化」に使える
  • 大規模データの加工 & ストリーミング出力への対応に使える
  • 実行結果を何度も呼び出せるため、メモリの大幅な節約になる

Refs

SQL のリザルトセットを一度配列に格納して foreach ループ、みたいな処理は 配列の大きさが一定を超えるとメモリ超過でコケる 。このような場合に 「実行結果をコレクションしておきイテレートされたときに 1 行ずつ yield する」 ようなジェネレータを作成すると、最初にクエリ実行したあとは 1 レコードずつ配列を返却するような機能を実装できる ( = SQL 実行結果のストリーミング ) 。

/**
 * Generator: Fetch users.
 */
function fetchUsers(): \Generator
{
    $dbh  = new PDO(/* ~ */);
    $stmt = $dbh->prepare("SELECT * FROM users");
    $stmt->execute();
    while ($user = $stmt->fetch(PDO::FETCH_OBJ)) {
       yield $user;
    }
}


// Iterate generator without huge array.
foreach (fetchUsers() as $user) {
    print_r($user);
}

SAMPLE CODE

CakePHP3 の find() の結果セットを一行ずつ返す

Query オブジェクトの SQL 実行は foreach による初回イテレート時一度だけ ... なんて省メモリ!!!

class LogsTable extends Table
{
    public function fetch(): \Generator
    {
        $logs = $this->find('recent');
        foreach ($logs as $log) {
            yield $log;
        }
    }
}

// Using.
class LogsController extends AppController
{
    public function print()
    {
        foreach ($this->Logs->fetch() as $log) {
            print_r($log->toArray());
        }
    }
}
<?php

/**
 * Count generator.
 * 
 * @param  int      $start
 * @param  int      $step
 * @param  int|null $limit
 * @return Generator
 */
function countGenerator(int $start=0, int $step=1, int $limit=null): \Generator
{
    if (empty($limit)) for ($i = $start; ; $i += $step) yield $i;
    else for ($i = $start; $i <= $limit; $i += $step)   yield $i;
}

// Create generator as iterable object.
// $counts = countGenerator(0, 1);
$counts = countGenerator(0, 1, 5);

// Call yield on loop.
foreach ($counts as $count) {
  echo $count;
  if ($count === 100) break;
}