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関数があれば、お気軽にコメントでお聞かせください。