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等)
なぜ必要なのか?
正規表現関数は、エラーが発生しても例外をスローせず、単にfalseやnullを返すことがあります。これにより:
- サイレントエラー: エラーに気づかず処理が続行
- デバッグ困難: 原因特定が難しい
- セキュリティリスク: 検証が不完全になる可能性
エラーコードの種類
<?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アプリケーションを構築してください!
