[PHP]preg_last_error関数の使い方|正規表現エラーを確実に検出して安全なコードを書く方法

PHP

PHPで正規表現を使っていて、予期しない動作やfalseが返される経験はありませんか?多くの開発者が見落としがちですが、正規表現関数はサイレントに失敗することがあります。そんな時に必要不可欠なのがpreg_last_error関数です。

この記事では、preg_last_error関数の基本的な使い方から、実践的なエラーハンドリングシステムの構築まで詳しく解説します。

preg_last_error関数とは?

preg_last_error()は、PHPの正規表現関数で、最後に実行されたPCRE関数(preg_match、preg_replace等)のエラーコードを返す関数です。

基本構文

int preg_last_error()
  • パラメータ: なし
  • 戻り値: エラーコードを表す整数値(PREG_NO_ERROR、PREG_BACKTRACK_LIMIT_ERROR等)

なぜ必要なのか?

正規表現関数は、エラーが発生しても例外をスローせず、単にfalsenullを返すことがあります。これにより:

  1. サイレントエラー: エラーに気づかず処理が続行
  2. デバッグ困難: 原因特定が難しい
  3. セキュリティリスク: 検証が不完全になる可能性

エラーコードの種類

<?php
// PHPで定義されているエラーコード定数
$errorCodes = [
    PREG_NO_ERROR => 'PREG_NO_ERROR - エラーなし',
    PREG_INTERNAL_ERROR => 'PREG_INTERNAL_ERROR - 内部PCREエラー',
    PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR - バックトラック制限超過',
    PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR - 再帰制限超過',
    PREG_BAD_UTF8_ERROR => 'PREG_BAD_UTF8_ERROR - 不正なUTF-8データ',
    PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR - オフセットがUTF-8文字の途中',
    PREG_JIT_STACKLIMIT_ERROR => 'PREG_JIT_STACKLIMIT_ERROR - JITスタック制限超過'
];

echo "=== PHPの正規表現エラーコード一覧 ===\n";
foreach ($errorCodes as $code => $description) {
    echo "コード {$code}: {$description}\n";
}
?>

基本的な使い方

シンプルなエラーチェック

<?php
function safeMatch($pattern, $subject) {
    $result = preg_match($pattern, $subject, $matches);
    
    $errorCode = preg_last_error();
    
    if ($errorCode !== PREG_NO_ERROR) {
        // エラーが発生した
        echo "エラー発生!\n";
        echo "エラーコード: {$errorCode}\n";
        echo "エラー名: " . getErrorName($errorCode) . "\n";
        return false;
    }
    
    // 正常処理
    return $result;
}

function getErrorName($code) {
    $names = [
        PREG_NO_ERROR => 'PREG_NO_ERROR',
        PREG_INTERNAL_ERROR => 'PREG_INTERNAL_ERROR',
        PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR',
        PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR',
        PREG_BAD_UTF8_ERROR => 'PREG_BAD_UTF8_ERROR',
        PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR',
        PREG_JIT_STACKLIMIT_ERROR => 'PREG_JIT_STACKLIMIT_ERROR'
    ];
    
    return $names[$code] ?? "UNKNOWN_ERROR({$code})";
}

// 正常な正規表現
echo "=== テスト1: 正常なパターン ===\n";
$result = safeMatch('/test/', 'this is a test');
echo "結果: " . ($result ? "マッチ" : "マッチなし") . "\n\n";

// バックトラック制限を超えるパターン
echo "=== テスト2: バックトラック制限超過 ===\n";
ini_set('pcre.backtrack_limit', '10');
$result = safeMatch('/(?:a+)+b/', str_repeat('a', 100));
echo "結果: " . ($result !== false ? "マッチ" : "エラー") . "\n\n";

// 元に戻す
ini_set('pcre.backtrack_limit', '1000000');
?>

各エラータイプの実例

<?php
echo "=== 正規表現エラータイプの実例 ===\n\n";

// 1. PREG_NO_ERROR(エラーなし)
echo "1. PREG_NO_ERROR - 正常動作\n";
$result = preg_match('/hello/', 'hello world');
echo "   エラーコード: " . preg_last_error() . " (PREG_NO_ERROR)\n\n";

// 2. PREG_BACKTRACK_LIMIT_ERROR
echo "2. PREG_BACKTRACK_LIMIT_ERROR - バックトラック制限超過\n";
ini_set('pcre.backtrack_limit', '100');
$pattern = '/(?:a+)+b/';
$subject = str_repeat('a', 1000) . 'c'; // bがないので大量にバックトラック
$result = preg_match($pattern, $subject);
echo "   結果: " . var_export($result, true) . "\n";
echo "   エラーコード: " . preg_last_error() . " (PREG_BACKTRACK_LIMIT_ERROR)\n";
ini_set('pcre.backtrack_limit', '1000000'); // 元に戻す
echo "\n";

// 3. PREG_RECURSION_LIMIT_ERROR
echo "3. PREG_RECURSION_LIMIT_ERROR - 再帰制限超過\n";
ini_set('pcre.recursion_limit', '100');
$pattern = '/(?R)?/'; // 再帰パターン
$subject = str_repeat('x', 1000);
$result = preg_match($pattern, $subject);
echo "   結果: " . var_export($result, true) . "\n";
echo "   エラーコード: " . preg_last_error() . " (PREG_RECURSION_LIMIT_ERROR)\n";
ini_set('pcre.recursion_limit', '100000'); // 元に戻す
echo "\n";

// 4. PREG_BAD_UTF8_ERROR
echo "4. PREG_BAD_UTF8_ERROR - 不正なUTF-8\n";
$pattern = '/test/u'; // uフラグ(UTF-8モード)
$subject = "\xFF\xFE" . 'test'; // 不正なUTF-8バイトシーケンス
$result = preg_match($pattern, $subject);
echo "   結果: " . var_export($result, true) . "\n";
echo "   エラーコード: " . preg_last_error() . "\n\n";

// 5. PREG_BAD_UTF8_OFFSET_ERROR
echo "5. PREG_BAD_UTF8_OFFSET_ERROR - 不正なオフセット\n";
$pattern = '/test/u';
$subject = 'あいうえおtest'; // マルチバイト文字
// UTF-8文字の途中のバイトオフセットを指定
$result = preg_match($pattern, $subject, $matches, 0, 2); // 2はマルチバイト文字の途中
echo "   結果: " . var_export($result, true) . "\n";
echo "   エラーコード: " . preg_last_error() . "\n\n";
?>

実践的な活用例

1. 堅牢な正規表現ラッパークラス

<?php
class SafeRegex {
    private $lastError = PREG_NO_ERROR;
    private $lastErrorMessage = '';
    private $errorLog = [];
    
    public function match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) {
        $result = @preg_match($pattern, $subject, $matches, $flags, $offset);
        
        return $this->handlePregResult($result, 'preg_match', [
            'pattern' => $pattern,
            'subject_length' => strlen($subject)
        ]);
    }
    
    public function matchAll($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) {
        $result = @preg_match_all($pattern, $subject, $matches, $flags, $offset);
        
        return $this->handlePregResult($result, 'preg_match_all', [
            'pattern' => $pattern,
            'subject_length' => strlen($subject)
        ]);
    }
    
    public function replace($pattern, $replacement, $subject, $limit = -1, &$count = null) {
        $result = @preg_replace($pattern, $replacement, $subject, $limit, $count);
        
        return $this->handlePregResult($result, 'preg_replace', [
            'pattern' => $pattern,
            'subject_length' => is_array($subject) ? 'array' : strlen($subject)
        ]);
    }
    
    public function split($pattern, $subject, $limit = -1, $flags = 0) {
        $result = @preg_split($pattern, $subject, $limit, $flags);
        
        return $this->handlePregResult($result, 'preg_split', [
            'pattern' => $pattern,
            'subject_length' => strlen($subject)
        ]);
    }
    
    private function handlePregResult($result, $function, $context) {
        $this->lastError = preg_last_error();
        
        if ($this->lastError !== PREG_NO_ERROR) {
            $this->lastErrorMessage = $this->getErrorMessage($this->lastError);
            
            $errorEntry = [
                'timestamp' => date('Y-m-d H:i:s'),
                'function' => $function,
                'error_code' => $this->lastError,
                'error_name' => $this->getErrorName($this->lastError),
                'error_message' => $this->lastErrorMessage,
                'context' => $context,
                'pcre_version' => PCRE_VERSION
            ];
            
            $this->errorLog[] = $errorEntry;
            
            // エラーログに記録
            error_log(sprintf(
                "PCRE Error in %s: %s (code: %d) - Pattern: %s",
                $function,
                $this->lastErrorMessage,
                $this->lastError,
                $context['pattern'] ?? 'N/A'
            ));
            
            return false;
        }
        
        return $result;
    }
    
    private function getErrorName($code) {
        $names = [
            PREG_NO_ERROR => 'PREG_NO_ERROR',
            PREG_INTERNAL_ERROR => 'PREG_INTERNAL_ERROR',
            PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR',
            PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR',
            PREG_BAD_UTF8_ERROR => 'PREG_BAD_UTF8_ERROR',
            PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR',
            PREG_JIT_STACKLIMIT_ERROR => 'PREG_JIT_STACKLIMIT_ERROR'
        ];
        
        return $names[$code] ?? "UNKNOWN_ERROR";
    }
    
    private function getErrorMessage($code) {
        $messages = [
            PREG_NO_ERROR => 'エラーなし',
            PREG_INTERNAL_ERROR => '内部PCREエラーが発生しました',
            PREG_BACKTRACK_LIMIT_ERROR => 'バックトラック制限を超過しました(pcre.backtrack_limitを増やしてください)',
            PREG_RECURSION_LIMIT_ERROR => '再帰制限を超過しました(pcre.recursion_limitを増やしてください)',
            PREG_BAD_UTF8_ERROR => '不正なUTF-8データが検出されました',
            PREG_BAD_UTF8_OFFSET_ERROR => 'オフセットがUTF-8文字の途中を指しています',
            PREG_JIT_STACKLIMIT_ERROR => 'JITスタック制限を超過しました'
        ];
        
        return $messages[$code] ?? '不明なエラーが発生しました';
    }
    
    public function getLastError() {
        return $this->lastError;
    }
    
    public function getLastErrorMessage() {
        return $this->lastErrorMessage;
    }
    
    public function getErrorLog() {
        return $this->errorLog;
    }
    
    public function hasError() {
        return $this->lastError !== PREG_NO_ERROR;
    }
    
    public function clearError() {
        $this->lastError = PREG_NO_ERROR;
        $this->lastErrorMessage = '';
    }
    
    public function getErrorSummary() {
        $summary = [
            'total_errors' => count($this->errorLog),
            'errors_by_type' => [],
            'errors_by_function' => []
        ];
        
        foreach ($this->errorLog as $entry) {
            $errorName = $entry['error_name'];
            $function = $entry['function'];
            
            if (!isset($summary['errors_by_type'][$errorName])) {
                $summary['errors_by_type'][$errorName] = 0;
            }
            $summary['errors_by_type'][$errorName]++;
            
            if (!isset($summary['errors_by_function'][$function])) {
                $summary['errors_by_function'][$function] = 0;
            }
            $summary['errors_by_function'][$function]++;
        }
        
        return $summary;
    }
}

// 使用例
echo "=== SafeRegex クラスのデモ ===\n\n";

$regex = new SafeRegex();

// 正常なマッチング
echo "1. 正常なパターン:\n";
$result = $regex->match('/test/', 'this is a test', $matches);
echo "   結果: " . ($result ? "マッチ" : "マッチなし") . "\n";
echo "   エラー: " . ($regex->hasError() ? $regex->getLastErrorMessage() : "なし") . "\n\n";

// バックトラック制限超過
echo "2. バックトラック制限超過:\n";
ini_set('pcre.backtrack_limit', '10');
$result = $regex->match('/(?:a+)+b/', str_repeat('a', 100));
echo "   結果: " . ($result !== false ? "マッチ" : "エラー") . "\n";
echo "   エラー: " . $regex->getLastErrorMessage() . "\n\n";
ini_set('pcre.backtrack_limit', '1000000');

// 不正なUTF-8
echo "3. 不正なUTF-8データ:\n";
$result = $regex->match('/test/u', "\xFF\xFE" . 'test');
echo "   結果: " . ($result !== false ? "マッチ" : "エラー") . "\n";
echo "   エラー: " . $regex->getLastErrorMessage() . "\n\n";

// エラーサマリー
echo "=== エラーサマリー ===\n";
$summary = $regex->getErrorSummary();
echo "総エラー数: " . $summary['total_errors'] . "\n";
echo "エラータイプ別:\n";
foreach ($summary['errors_by_type'] as $type => $count) {
    echo "  - {$type}: {$count}件\n";
}
?>

2. 自動リトライ機能付き正規表現マッチャー

<?php
class RetryableRegex {
    private $maxRetries = 3;
    private $backtrackLimitMultiplier = 10;
    private $recursionLimitMultiplier = 10;
    
    public function matchWithRetry($pattern, $subject, &$matches = null) {
        $originalBacktrackLimit = ini_get('pcre.backtrack_limit');
        $originalRecursionLimit = ini_get('pcre.recursion_limit');
        
        $attempt = 0;
        
        while ($attempt < $this->maxRetries) {
            $attempt++;
            
            $result = @preg_match($pattern, $subject, $matches);
            $error = preg_last_error();
            
            if ($error === PREG_NO_ERROR) {
                // 成功
                return $result;
            }
            
            echo "試行 {$attempt}/{$this->maxRetries}: ";
            
            // エラータイプに応じて制限を調整
            if ($error === PREG_BACKTRACK_LIMIT_ERROR) {
                $newLimit = (int)ini_get('pcre.backtrack_limit') * $this->backtrackLimitMultiplier;
                ini_set('pcre.backtrack_limit', $newLimit);
                echo "バックトラック制限を {$newLimit} に増加\n";
                
            } elseif ($error === PREG_RECURSION_LIMIT_ERROR) {
                $newLimit = (int)ini_get('pcre.recursion_limit') * $this->recursionLimitMultiplier;
                ini_set('pcre.recursion_limit', $newLimit);
                echo "再帰制限を {$newLimit} に増加\n";
                
            } else {
                // リトライ不可能なエラー
                echo "リトライ不可能なエラー(コード: {$error})\n";
                break;
            }
        }
        
        // 設定を元に戻す
        ini_set('pcre.backtrack_limit', $originalBacktrackLimit);
        ini_set('pcre.recursion_limit', $originalRecursionLimit);
        
        return false;
    }
}

// デモ
echo "=== リトライ機能付き正規表現デモ ===\n\n";

$retryRegex = new RetryableRegex();

// 初期制限を低く設定
ini_set('pcre.backtrack_limit', '10');

echo "複雑なパターンをリトライ付きでマッチング:\n";
$result = $retryRegex->matchWithRetry(
    '/(?:a+)+b/', 
    str_repeat('a', 50) . 'b'
);

if ($result !== false) {
    echo "\n✓ マッチ成功!\n";
} else {
    echo "\n✗ マッチ失敗\n";
}
?>

3. 包括的なエラーモニタリングシステム

<?php
class RegexErrorMonitor {
    private static $instance = null;
    private $errors = [];
    private $statistics = [
        'total_operations' => 0,
        'successful_operations' => 0,
        'failed_operations' => 0
    ];
    
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function monitoredMatch($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) {
        return $this->monitorOperation('preg_match', function() use ($pattern, $subject, &$matches, $flags, $offset) {
            return preg_match($pattern, $subject, $matches, $flags, $offset);
        }, compact('pattern', 'subject', 'flags', 'offset'));
    }
    
    public function monitoredReplace($pattern, $replacement, $subject, $limit = -1, &$count = null) {
        return $this->monitorOperation('preg_replace', function() use ($pattern, $replacement, $subject, $limit, &$count) {
            return preg_replace($pattern, $replacement, $subject, $limit, $count);
        }, compact('pattern', 'subject', 'limit'));
    }
    
    private function monitorOperation($operationName, $callback, $context) {
        $this->statistics['total_operations']++;
        
        $startTime = microtime(true);
        $startMemory = memory_get_usage();
        
        $result = $callback();
        
        $endTime = microtime(true);
        $endMemory = memory_get_usage();
        
        $error = preg_last_error();
        
        $record = [
            'timestamp' => date('Y-m-d H:i:s'),
            'operation' => $operationName,
            'error_code' => $error,
            'error_name' => $this->getErrorName($error),
            'success' => ($error === PREG_NO_ERROR),
            'execution_time' => $endTime - $startTime,
            'memory_used' => $endMemory - $startMemory,
            'context' => $context
        ];
        
        if ($error !== PREG_NO_ERROR) {
            $this->statistics['failed_operations']++;
            $this->errors[] = $record;
            
            // 警告を出力
            trigger_error(
                "PCRE Error in {$operationName}: " . $this->getErrorMessage($error),
                E_USER_WARNING
            );
        } else {
            $this->statistics['successful_operations']++;
        }
        
        return $result;
    }
    
    public function getErrors() {
        return $this->errors;
    }
    
    public function getStatistics() {
        $stats = $this->statistics;
        
        if ($stats['total_operations'] > 0) {
            $stats['success_rate'] = round(
                ($stats['successful_operations'] / $stats['total_operations']) * 100,
                2
            );
        } else {
            $stats['success_rate'] = 0;
        }
        
        return $stats;
    }
    
    public function generateReport() {
        $report = "=== 正規表現エラーモニタリングレポート ===\n\n";
        
        $stats = $this->getStatistics();
        $report .= "統計情報:\n";
        $report .= "  総操作数: " . $stats['total_operations'] . "\n";
        $report .= "  成功: " . $stats['successful_operations'] . "\n";
        $report .= "  失敗: " . $stats['failed_operations'] . "\n";
        $report .= "  成功率: " . $stats['success_rate'] . "%\n\n";
        
        if (!empty($this->errors)) {
            $report .= "エラー詳細:\n";
            foreach ($this->errors as $index => $error) {
                $report .= "\nエラー #" . ($index + 1) . ":\n";
                $report .= "  時刻: " . $error['timestamp'] . "\n";
                $report .= "  操作: " . $error['operation'] . "\n";
                $report .= "  エラー: " . $error['error_name'] . "\n";
                $report .= "  実行時間: " . round($error['execution_time'] * 1000, 2) . "ms\n";
                $report .= "  メモリ使用: " . round($error['memory_used'] / 1024, 2) . "KB\n";
                
                if (isset($error['context']['pattern'])) {
                    $report .= "  パターン: " . $error['context']['pattern'] . "\n";
                }
            }
        }
        
        return $report;
    }
    
    private function getErrorName($code) {
        $names = [
            PREG_NO_ERROR => 'PREG_NO_ERROR',
            PREG_INTERNAL_ERROR => 'PREG_INTERNAL_ERROR',
            PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR',
            PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR',
            PREG_BAD_UTF8_ERROR => 'PREG_BAD_UTF8_ERROR',
            PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR',
            PREG_JIT_STACKLIMIT_ERROR => 'PREG_JIT_STACKLIMIT_ERROR'
        ];
        
        return $names[$code] ?? "UNKNOWN({$code})";
    }
    
    private function getErrorMessage($code) {
        $messages = [
            PREG_INTERNAL_ERROR => '内部PCREエラー',
            PREG_BACKTRACK_LIMIT_ERROR => 'バックトラック制限超過',
            PREG_RECURSION_LIMIT_ERROR => '再帰制限超過',
            PREG_BAD_UTF8_ERROR => '不正なUTF-8データ',
            PREG_BAD_UTF8_OFFSET_ERROR => '不正なUTF-8オフセット',
            PREG_JIT_STACKLIMIT_ERROR => 'JITスタック制限超過'
        ];
        
        return $messages[$code] ?? '不明なエラー';
    }
}

// 使用例
echo "=== エラーモニタリングデモ ===\n\n";

$monitor = RegexErrorMonitor::getInstance();

// 様々なパターンでテスト
echo "1. 正常なマッチング\n";
$monitor->monitoredMatch('/test/', 'this is a test');

echo "\n2. 複雑なパターン(成功)\n";
$monitor->monitoredMatch('/[a-z]+/', 'hello world');

echo "\n3. バックトラック制限超過\n";
ini_set('pcre.backtrack_limit', '10');
$monitor->monitoredMatch('/(?:a+)+b/', str_repeat('a', 100));
ini_set('pcre.backtrack_limit', '1000000');

echo "\n4. 不正なUTF-8\n";
$monitor->monitoredMatch('/test/u', "\xFF\xFE" . 'test');

// レポート生成
echo "\n" . $monitor->generateReport();
?>

まとめ

preg_last_error関数は、PHPの正規表現処理において、エラーを確実に検出し、堅牢なコードを書くために必須の関数です。

✨ 主要なメリット

  • エラー検出: サイレントエラーの防止
  • デバッグ支援: 問題の早期発見
  • 信頼性向上: 予期しない動作の防止
  • セキュリティ: 検証処理の確実性向上

🚀 実践的応用

  • バリデーション: ユーザー入力の安全な検証
  • データ処理: 大量データの確実な処理
  • 監視システム: 正規表現エラーの追跡
  • 自動リトライ: エラー時の自動復旧

この記事で紹介した実装例を活用して、より安全で信頼性の高いPHPアプリケーションを構築してください!

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