[PHP]iterator_apply()関数の使い方と実践的な活用法を徹底解説

PHP

PHPでイテレータを扱う際、各要素に対して関数を適用したい場面があります。そんな時に威力を発揮するのがiterator_apply()関数です。

この記事では、iterator_apply()関数の基本的な使い方から実践的な活用例まで、具体的なコード例を交えながら詳しく解説します。

iterator_apply()関数とは?

iterator_apply()は、イテレータの各要素に対してユーザー定義関数を適用するPHPの組み込み関数です。配列のarray_walk()のイテレータ版と考えるとわかりやすいでしょう。

基本構文

iterator_apply(Traversable $iterator, callable $function, ?array $args = null): int

パラメータ

  • $iterator: 処理対象のイテレータ(Traversableインターフェースを実装したオブジェクト)
  • $function: 各要素に適用するコールバック関数
  • $args: コールバック関数に渡す追加の引数(配列)

戻り値

  • int: 成功した場合は反復回数、失敗した場合は-1

基本的な使用例

シンプルな例

<?php
// ArrayIteratorを使った基本例
$data = new ArrayIterator(['apple', 'banana', 'cherry']);

// 各要素を出力する関数
function printItem($iterator) {
    echo $iterator->current() . "\n";
    return true; // 継続するためにtrueを返す
}

// 各要素に関数を適用
$count = iterator_apply($data, 'printItem', [$data]);
echo "処理した要素数: {$count}\n";

// 出力:
// apple
// banana  
// cherry
// 処理した要素数: 3
?>

クロージャを使った例

<?php
$numbers = new ArrayIterator([1, 2, 3, 4, 5]);

// クロージャで各要素を2倍して出力
$doubleAndPrint = function($iterator) {
    $value = $iterator->current() * 2;
    echo "元の値: {$iterator->current()}, 2倍: {$value}\n";
    return true;
};

iterator_apply($numbers, $doubleAndPrint, [$numbers]);

// 出力:
// 元の値: 1, 2倍: 2
// 元の値: 2, 2倍: 4
// 元の値: 3, 2倍: 6
// 元の値: 4, 2倍: 8
// 元の値: 5, 2倍: 10
?>

実践的な活用例

1. データ検証とフィルタリング

<?php
class DataValidator 
{
    private $errors = [];
    
    public function validateUsers($userIterator) 
    {
        $this->errors = [];
        
        $validator = function($iterator, $validatorInstance) {
            $user = $iterator->current();
            $key = $iterator->key();
            
            // メールアドレスの検証
            if (!isset($user['email']) || !filter_var($user['email'], FILTER_VALIDATE_EMAIL)) {
                $validatorInstance->errors[] = "ユーザー{$key}: 無効なメールアドレス";
            }
            
            // 年齢の検証
            if (!isset($user['age']) || $user['age'] < 0 || $user['age'] > 150) {
                $validatorInstance->errors[] = "ユーザー{$key}: 無効な年齢";
            }
            
            // 名前の検証
            if (!isset($user['name']) || empty(trim($user['name']))) {
                $validatorInstance->errors[] = "ユーザー{$key}: 名前が空です";
            }
            
            return true;
        };
        
        $count = iterator_apply($userIterator, $validator, [$userIterator, $this]);
        
        return [
            'processed' => $count,
            'errors' => $this->errors,
            'valid' => empty($this->errors)
        ];
    }
}

// 使用例
$users = new ArrayIterator([
    ['name' => 'John', 'email' => 'john@example.com', 'age' => 25],
    ['name' => '', 'email' => 'invalid-email', 'age' => -5],
    ['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 30],
]);

$validator = new DataValidator();
$result = $validator->validateUsers($users);

print_r($result);
?>

2. ログファイルの処理

<?php
class LogProcessor 
{
    private $stats = [
        'total' => 0,
        'errors' => 0,
        'warnings' => 0,
        'info' => 0
    ];
    
    public function processLogFile($filename) 
    {
        if (!file_exists($filename)) {
            throw new InvalidArgumentException("ログファイルが見つかりません: {$filename}");
        }
        
        $fileIterator = new SplFileObject($filename);
        $fileIterator->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
        
        $processor = function($iterator, $statsRef) {
            $line = $iterator->current();
            $statsRef->stats['total']++;
            
            // ログレベルを判定
            if (strpos($line, 'ERROR') !== false) {
                $statsRef->stats['errors']++;
                echo "エラーログ発見: " . substr($line, 0, 100) . "...\n";
            } elseif (strpos($line, 'WARNING') !== false) {
                $statsRef->stats['warnings']++;
            } elseif (strpos($line, 'INFO') !== false) {
                $statsRef->stats['info']++;
            }
            
            return true;
        };
        
        iterator_apply($fileIterator, $processor, [$fileIterator, $this]);
        
        return $this->stats;
    }
}

// 使用例
/*
$logProcessor = new LogProcessor();
$stats = $logProcessor->processLogFile('/var/log/application.log');
print_r($stats);
*/
?>

3. データベース結果の処理

<?php
class DatabaseResultProcessor 
{
    private $processedData = [];
    
    public function processResults($resultIterator, $callback = null) 
    {
        $this->processedData = [];
        
        $processor = function($iterator, $processorInstance, $userCallback) {
            $row = $iterator->current();
            
            // デフォルト処理
            $processedRow = [
                'id' => $row['id'] ?? null,
                'processed_at' => date('Y-m-d H:i:s'),
                'data' => $row
            ];
            
            // ユーザー定義のコールバックがあれば適用
            if ($userCallback && is_callable($userCallback)) {
                $processedRow = $userCallback($processedRow, $row);
            }
            
            $processorInstance->processedData[] = $processedRow;
            
            return true;
        };
        
        $count = iterator_apply($resultIterator, $processor, [$resultIterator, $this, $callback]);
        
        return [
            'count' => $count,
            'data' => $this->processedData
        ];
    }
}

// PDO結果セットを模擬
class MockPDOStatement implements Iterator 
{
    private $data;
    private $position = 0;
    
    public function __construct(array $data) {
        $this->data = $data;
    }
    
    public function current() { return $this->data[$this->position]; }
    public function key() { return $this->position; }
    public function next() { ++$this->position; }
    public function rewind() { $this->position = 0; }
    public function valid() { return isset($this->data[$this->position]); }
}

// 使用例
$mockResults = new MockPDOStatement([
    ['id' => 1, 'name' => 'Product A', 'price' => 100],
    ['id' => 2, 'name' => 'Product B', 'price' => 200],
    ['id' => 3, 'name' => 'Product C', 'price' => 300],
]);

$processor = new DatabaseResultProcessor();

// カスタムコールバックで価格を税込み価格に変換
$result = $processor->processResults($mockResults, function($processedRow, $originalRow) {
    $processedRow['data']['price_with_tax'] = $originalRow['price'] * 1.1;
    return $processedRow;
});

print_r($result);
?>

4. XML/JSON データの変換

<?php
class DataConverter 
{
    private $convertedData = [];
    private $errors = [];
    
    public function convertJsonObjects($jsonObjectsIterator, $targetFormat = 'array') 
    {
        $this->convertedData = [];
        $this->errors = [];
        
        $converter = function($iterator, $converterInstance, $format) {
            try {
                $jsonString = $iterator->current();
                $data = json_decode($jsonString, true);
                
                if (json_last_error() !== JSON_ERROR_NONE) {
                    $converterInstance->errors[] = "JSON解析エラー: " . json_last_error_msg();
                    return true;
                }
                
                switch ($format) {
                    case 'xml':
                        $xml = new SimpleXMLElement('<root/>');
                        $converterInstance->arrayToXml($data, $xml);
                        $converterInstance->convertedData[] = $xml->asXML();
                        break;
                    
                    case 'csv':
                        $converterInstance->convertedData[] = $converterInstance->arrayToCsv($data);
                        break;
                    
                    default:
                        $converterInstance->convertedData[] = $data;
                }
                
            } catch (Exception $e) {
                $converterInstance->errors[] = "変換エラー: " . $e->getMessage();
            }
            
            return true;
        };
        
        $count = iterator_apply($jsonObjectsIterator, $converter, [$jsonObjectsIterator, $this, $targetFormat]);
        
        return [
            'count' => $count,
            'data' => $this->convertedData,
            'errors' => $this->errors
        ];
    }
    
    private function arrayToXml($data, SimpleXMLElement $xml) 
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $child = $xml->addChild($key);
                $this->arrayToXml($value, $child);
            } else {
                $xml->addChild($key, htmlspecialchars($value));
            }
        }
    }
    
    private function arrayToCsv($data) 
    {
        $output = fopen('php://temp', 'r+');
        fputcsv($output, array_keys($data));
        fputcsv($output, array_values($data));
        rewind($output);
        $csv = stream_get_contents($output);
        fclose($output);
        return $csv;
    }
}

// 使用例
$jsonData = new ArrayIterator([
    '{"id": 1, "name": "John", "email": "john@example.com"}',
    '{"id": 2, "name": "Alice", "email": "alice@example.com"}',
    '{"id": 3, "name": "Bob", "email": "bob@example.com"}'
]);

$converter = new DataConverter();
$result = $converter->convertJsonObjects($jsonData, 'array');

print_r($result);
?>

高度な使用パターン

1. 条件付き処理の実装

<?php
class ConditionalProcessor 
{
    public static function processWithCondition($iterator, $condition, $action) 
    {
        $processor = function($iter, $conditionFunc, $actionFunc) {
            $current = $iter->current();
            $key = $iter->key();
            
            // 条件をチェック
            if ($conditionFunc($current, $key)) {
                return $actionFunc($current, $key);
            }
            
            return true; // 条件に合わない場合は継続
        };
        
        return iterator_apply($iterator, $processor, [$iterator, $condition, $action]);
    }
}

// 使用例:偶数のみを処理
$numbers = new ArrayIterator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

$isEven = function($value, $key) {
    return $value % 2 === 0;
};

$printDouble = function($value, $key) {
    echo "偶数 {$value} の2倍は " . ($value * 2) . "\n";
    return true;
};

ConditionalProcessor::processWithCondition($numbers, $isEven, $printDouble);
?>

2. 早期終了の実装

<?php
function findFirstMatch($iterator, $condition) 
{
    $found = null;
    
    $finder = function($iter, $conditionFunc, &$result) {
        $current = $iter->current();
        
        if ($conditionFunc($current)) {
            $result = $current;
            return false; // 早期終了
        }
        
        return true; // 継続
    };
    
    iterator_apply($iterator, $finder, [$iterator, $condition, &$found]);
    
    return $found;
}

// 使用例:最初の負の数を見つける
$numbers = new ArrayIterator([5, 3, -2, 8, -1, 4]);

$isNegative = function($value) {
    return $value < 0;
};

$firstNegative = findFirstMatch($numbers, $isNegative);
echo "最初の負の数: {$firstNegative}\n"; // -2
?>

3. 統計情報の収集

<?php
class StatisticsCollector 
{
    private $stats = [];
    
    public function collectStats($iterator, $fields = []) 
    {
        $this->stats = array_fill_keys($fields, [
            'count' => 0,
            'sum' => 0,
            'min' => PHP_INT_MAX,
            'max' => PHP_INT_MIN,
            'values' => []
        ]);
        
        $collector = function($iter, $collectorInstance, $targetFields) {
            $current = $iter->current();
            
            foreach ($targetFields as $field) {
                if (isset($current[$field]) && is_numeric($current[$field])) {
                    $value = $current[$field];
                    $stats = &$collectorInstance->stats[$field];
                    
                    $stats['count']++;
                    $stats['sum'] += $value;
                    $stats['min'] = min($stats['min'], $value);
                    $stats['max'] = max($stats['max'], $value);
                    $stats['values'][] = $value;
                }
            }
            
            return true;
        };
        
        iterator_apply($iterator, $collector, [$iterator, $this, $fields]);
        
        // 平均値を計算
        foreach ($this->stats as $field => &$stat) {
            if ($stat['count'] > 0) {
                $stat['average'] = $stat['sum'] / $stat['count'];
            }
        }
        
        return $this->stats;
    }
}

// 使用例
$salesData = new ArrayIterator([
    ['product' => 'A', 'sales' => 100, 'profit' => 20],
    ['product' => 'B', 'sales' => 150, 'profit' => 30],
    ['product' => 'C', 'sales' => 80, 'profit' => 15],
    ['product' => 'D', 'sales' => 200, 'profit' => 50],
]);

$collector = new StatisticsCollector();
$stats = $collector->collectStats($salesData, ['sales', 'profit']);

print_r($stats);
?>

エラーハンドリングとベストプラクティス

安全な実装パターン

<?php
class SafeIteratorProcessor 
{
    public static function safeApply($iterator, $callback, $args = []) 
    {
        try {
            // イテレータの妥当性をチェック
            if (!($iterator instanceof Traversable)) {
                throw new InvalidArgumentException('引数はTraversableである必要があります');
            }
            
            // コールバックの妥当性をチェック
            if (!is_callable($callback)) {
                throw new InvalidArgumentException('コールバック関数が無効です');
            }
            
            // 安全にイテレータを処理
            $safeCallback = function($iter, $userCallback, $userArgs) {
                try {
                    return call_user_func($userCallback, $iter, ...$userArgs);
                } catch (Exception $e) {
                    error_log("Iterator処理中にエラー: " . $e->getMessage());
                    return true; // 継続
                }
            };
            
            return iterator_apply($iterator, $safeCallback, [$iterator, $callback, $args]);
            
        } catch (Exception $e) {
            error_log("SafeIteratorProcessor エラー: " . $e->getMessage());
            return -1;
        }
    }
}

// 使用例
$data = new ArrayIterator([1, 2, 'invalid', 4, 5]);

$riskyProcessor = function($iterator) {
    $value = $iterator->current();
    
    // 数値でない場合は例外を投げる可能性がある処理
    $result = $value * 2;
    echo "処理結果: {$result}\n";
    
    return true;
};

$count = SafeIteratorProcessor::safeApply($data, $riskyProcessor);
echo "処理完了: {$count}件\n";
?>

パフォーマンスの考慮事項

メモリ効率的な実装

<?php
// 大量データを扱う場合のメモリ効率的な実装
class MemoryEfficientProcessor 
{
    public static function processLargeDataset($iterator, $batchSize = 1000) 
    {
        $processed = 0;
        $batch = [];
        
        $processor = function($iter, &$batchArray, &$processedCount, $maxBatchSize) {
            $batchArray[] = $iter->current();
            
            // バッチサイズに達したら処理
            if (count($batchArray) >= $maxBatchSize) {
                // バッチ処理(例:データベースへの一括挿入など)
                echo "バッチ処理: " . count($batchArray) . "件\n";
                
                $processedCount += count($batchArray);
                $batchArray = []; // バッチをクリア
                
                // メモリ使用量をチェック
                if (memory_get_usage() > 50 * 1024 * 1024) { // 50MB
                    gc_collect_cycles(); // ガベージコレクション実行
                }
            }
            
            return true;
        };
        
        iterator_apply($iterator, $processor, [$iterator, &$batch, &$processed, $batchSize]);
        
        // 残りのバッチを処理
        if (!empty($batch)) {
            echo "最終バッチ処理: " . count($batch) . "件\n";
            $processed += count($batch);
        }
        
        return $processed;
    }
}
?>

まとめ

iterator_apply()関数は、イテレータに対する汎用的な処理を行う強力なツールです。

主な利点

  • メモリ効率: 大量データを一度にメモリに読み込まず、順次処理可能
  • 柔軟性: あらゆるTraversableオブジェクトに対応
  • 関数型プログラミング: コールバック関数による宣言的な処理
  • 早期終了: 条件に応じて処理を中断可能

適用場面

  • 大容量ファイルの処理: ログファイル、CSVファイル等
  • データベース結果セットの処理: PDOStatementなど
  • データ変換・検証: JSON、XMLデータの処理
  • 統計情報の収集: リアルタイムデータ分析

ベストプラクティス

  • エラーハンドリングを適切に実装する
  • メモリ使用量を監視し、必要に応じてガベージコレクションを実行
  • バッチ処理でパフォーマンスを最適化
  • 早期終了の仕組みを活用して効率化

iterator_apply()を適切に活用することで、メモリ効率的で保守性の高いPHPアプリケーションを構築することができます。特に大量データの処理や、リアルタイムでのデータ変換が必要な場面では、この関数の真価を発揮できるでしょう。


この記事がPHPでのイテレータ活用の参考になれば幸いです。他にも気になるPHP関数があれば、お気軽にコメントでお聞かせください。

タイトルとURLをコピーしました