[PHP]preg_replace_callback関数の使い方完全ガイド!動的な文字列置換をマスターしよう

PHP

はじめに

preg_replaceでは実現できない複雑な置換処理が必要になったことはありませんか?例えば、マッチした数値を計算して置き換えたり、データベースから情報を取得して置換したり…そんなときに活躍するのがpreg_replace_callbackです。

この記事では、preg_replace_callbackの基本から実践的な活用法まで、豊富なサンプルコードとともに詳しく解説します。

preg_replace_callbackとは?

preg_replace_callbackは、正規表現でマッチした部分に対して関数(コールバック)を実行し、その戻り値で置換を行うPHP関数です。

preg_replaceとの違い

機能preg_replacepreg_replace_callback
置換方法固定文字列・後方参照関数の戻り値
動的な処理
計算・変換
条件分岐
外部データ参照

基本構文

preg_replace_callback(
    string|array $pattern,           // 正規表現パターン
    callable $callback,              // コールバック関数
    string|array $subject,           // 置換対象の文字列
    int $limit = -1,                 // 置換回数の上限
    int &$count = null,              // 置換回数を格納する変数
    int $flags = 0                   // フラグ
): string|array|null

コールバック関数の引数: マッチ結果の配列が渡されます

  • $matches[0]: マッチした全体の文字列
  • $matches[1], $matches[2], …: キャプチャグループ

戻り値: 置換後の文字列(または配列)を返します。

基本的な使用例

例1: 無名関数を使った基本的な置換

<?php
$text = "商品価格: 1000円、2000円、3000円";
$pattern = '/(\d+)円/';

$result = preg_replace_callback($pattern, function($matches) {
    $price = (int)$matches[1];
    $taxIncluded = (int)($price * 1.1);  // 消費税10%を加算
    return $taxIncluded . '円(税込)';
}, $text);

echo $result . "\n";
// 出力: 商品価格: 1100円(税込)、2200円(税込)、3300円(税込)
?>

例2: アロー関数を使った簡潔な書き方(PHP 7.4+)

<?php
$text = "温度: 32°F, 68°F, 86°F";
$pattern = '/(\d+)°F/';

// 華氏から摂氏に変換
$result = preg_replace_callback($pattern, 
    fn($m) => round(($m[1] - 32) * 5 / 9) . '°C',
    $text
);

echo $result . "\n";
// 出力: 温度: 0°C, 20°C, 30°C
?>

例3: 名前付き関数を使う方法

<?php
function convertToUpper($matches) {
    return strtoupper($matches[0]);
}

$text = "hello world from php";
$pattern = '/\b\w+\b/';

$result = preg_replace_callback($pattern, 'convertToUpper', $text);
echo $result . "\n";
// 出力: HELLO WORLD FROM PHP
?>

実践的な使用例

例1: Markdown風のリンク変換

<?php
function convertMarkdownLinks($text) {
    // [リンクテキスト](URL)の形式を検出
    $pattern = '/\[([^\]]+)\]\(([^\)]+)\)/';
    
    return preg_replace_callback($pattern, function($matches) {
        $text = htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8');
        $url = htmlspecialchars($matches[2], ENT_QUOTES, 'UTF-8');
        return "<a href=\"{$url}\" target=\"_blank\">{$text}</a>";
    }, $text);
}

$markdown = "詳細は[公式サイト](https://example.com)をご覧ください。";
echo convertMarkdownLinks($markdown) . "\n";
// 出力: 詳細は<a href="https://example.com" target="_blank">公式サイト</a>をご覧ください。
?>

例2: 変数展開(テンプレートエンジン風)

<?php
function expandVariables($text, $variables) {
    $pattern = '/\{([a-zA-Z_]\w*)\}/';
    
    return preg_replace_callback($pattern, function($matches) use ($variables) {
        $varName = $matches[1];
        return $variables[$varName] ?? $matches[0];  // 変数が存在しなければそのまま
    }, $text);
}

$template = "こんにちは、{name}さん。あなたのスコアは{score}点です。";
$data = ['name' => '太郎', 'score' => 95];

echo expandVariables($template, $data) . "\n";
// 出力: こんにちは、太郎さん。あなたのスコアは95点です。
?>

例3: カラーコードの変換(短縮形→完全形)

<?php
function expandColorCodes($css) {
    $pattern = '/#([0-9a-fA-F]{3})\b/';
    
    return preg_replace_callback($pattern, function($matches) {
        $short = $matches[1];
        // #abc → #aabbcc
        $expanded = '#' . $short[0] . $short[0] 
                        . $short[1] . $short[1] 
                        . $short[2] . $short[2];
        return $expanded;
    }, $css);
}

$css = "color: #f00; background: #0a5;";
echo expandColorCodes($css) . "\n";
// 出力: color: #ff0000; background: #00aa55;
?>

例4: 相対URLを絶対URLに変換

<?php
function makeAbsoluteUrls($html, $baseUrl) {
    $pattern = '/href=["\'](?!http|https|\/\/|#)([^"\']+)["\']/i';
    
    return preg_replace_callback($pattern, function($matches) use ($baseUrl) {
        $relativeUrl = $matches[1];
        $absoluteUrl = rtrim($baseUrl, '/') . '/' . ltrim($relativeUrl, '/');
        return "href=\"{$absoluteUrl}\"";
    }, $html);
}

$html = '<a href="page.html">リンク</a> <a href="/about">概要</a>';
$baseUrl = 'https://example.com';

echo makeAbsoluteUrls($html, $baseUrl) . "\n";
// 出力: <a href="https://example.com/page.html">リンク</a> <a href="https://example.com/about">概要</a>
?>

例5: 日時のフォーマット変換

<?php
function formatDates($text) {
    // YYYY-MM-DD形式を検出
    $pattern = '/(\d{4})-(\d{2})-(\d{2})/';
    
    return preg_replace_callback($pattern, function($matches) {
        $year = $matches[1];
        $month = $matches[2];
        $day = $matches[3];
        $timestamp = mktime(0, 0, 0, (int)$month, (int)$day, (int)$year);
        return date('Y年n月j日(D)', $timestamp);
    }, $text);
}

$text = "会議は2024-10-25に開催されます。";
echo formatDates($text) . "\n";
// 出力: 会議は2024年10月25日(Fri)に開催されます。
?>

例6: BBCode風タグのHTML変換

<?php
function convertBBCode($text) {
    $conversions = [
        '/\[b\](.*?)\[\/b\]/s' => function($m) {
            return '<strong>' . htmlspecialchars($m[1]) . '</strong>';
        },
        '/\[i\](.*?)\[\/i\]/s' => function($m) {
            return '<em>' . htmlspecialchars($m[1]) . '</em>';
        },
        '/\[url\](.*?)\[\/url\]/s' => function($m) {
            $url = htmlspecialchars($m[1]);
            return "<a href=\"{$url}\">{$url}</a>";
        },
    ];
    
    foreach ($conversions as $pattern => $callback) {
        $text = preg_replace_callback($pattern, $callback, $text);
    }
    
    return $text;
}

$bbcode = "これは[b]太字[/b]で、[i]斜体[/i]です。[url]https://example.com[/url]";
echo convertBBCode($bbcode) . "\n";
// 出力: これは<strong>太字</strong>で、<em>斜体</em>です。<a href="https://example.com">https://example.com</a>
?>

複雑な処理の例

例1: キャッシュ機能付き画像サイズ取得

<?php
function addImageDimensions($html) {
    static $cache = [];
    $pattern = '/<img\s+src=["\']([^"\']+)["\']/i';
    
    return preg_replace_callback($pattern, function($matches) use (&$cache) {
        $src = $matches[1];
        
        // キャッシュチェック
        if (!isset($cache[$src])) {
            if (file_exists($src)) {
                $size = getimagesize($src);
                $cache[$src] = [
                    'width' => $size[0] ?? 0,
                    'height' => $size[1] ?? 0
                ];
            } else {
                $cache[$src] = ['width' => 0, 'height' => 0];
            }
        }
        
        $width = $cache[$src]['width'];
        $height = $cache[$src]['height'];
        
        if ($width && $height) {
            return "<img src=\"{$src}\" width=\"{$width}\" height=\"{$height}\"";
        }
        
        return $matches[0];
    }, $html);
}

$html = '<img src="photo.jpg"><img src="logo.png">';
echo addImageDimensions($html) . "\n";
?>

例2: 数式の計算

<?php
function calculateExpressions($text) {
    $pattern = '/\{(\d+)\s*([\+\-\*\/])\s*(\d+)\}/';
    
    return preg_replace_callback($pattern, function($matches) {
        $num1 = (float)$matches[1];
        $operator = $matches[2];
        $num2 = (float)$matches[3];
        
        $result = match($operator) {
            '+' => $num1 + $num2,
            '-' => $num1 - $num2,
            '*' => $num1 * $num2,
            '/' => $num2 != 0 ? $num1 / $num2 : 'ERROR',
            default => 'ERROR'
        };
        
        return is_numeric($result) ? (string)$result : $result;
    }, $text);
}

$text = "合計: {100 + 200}円、割引後: {500 - 50}円、単価: {1000 / 4}円";
echo calculateExpressions($text) . "\n";
// 出力: 合計: 300円、割引後: 450円、単価: 250円
?>

例3: ショートコードの展開(WordPress風)

<?php
class ShortcodeProcessor {
    private $shortcodes = [];
    
    public function add($tag, $callback) {
        $this->shortcodes[$tag] = $callback;
    }
    
    public function process($content) {
        $pattern = '/\[(\w+)(?:\s+([^\]]*))?\]/';
        
        return preg_replace_callback($pattern, function($matches) {
            $tag = $matches[1];
            $attrs = $matches[2] ?? '';
            
            if (!isset($this->shortcodes[$tag])) {
                return $matches[0];
            }
            
            // 属性をパース
            $attributes = $this->parseAttributes($attrs);
            
            return call_user_func($this->shortcodes[$tag], $attributes);
        }, $content);
    }
    
    private function parseAttributes($str) {
        $attrs = [];
        if (preg_match_all('/(\w+)=["\']([^"\']+)["\']/', $str, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $attrs[$match[1]] = $match[2];
            }
        }
        return $attrs;
    }
}

$processor = new ShortcodeProcessor();

// ショートコードを登録
$processor->add('button', function($attrs) {
    $text = $attrs['text'] ?? 'ボタン';
    $url = $attrs['url'] ?? '#';
    return "<button onclick=\"location.href='{$url}'\">{$text}</button>";
});

$processor->add('alert', function($attrs) {
    $message = $attrs['message'] ?? '';
    $type = $attrs['type'] ?? 'info';
    return "<div class=\"alert alert-{$type}\">{$message}</div>";
});

$content = '[button text="クリック" url="/page"] [alert type="warning" message="注意事項"]';
echo $processor->process($content) . "\n";
// 出力: <button onclick="location.href='/page'">クリック</button> <div class="alert alert-warning">注意事項</div>
?>

データベースとの連携

<?php
// ユーザーID(@user123)をユーザー名に変換
function replaceUserMentions($text, $pdo) {
    $pattern = '/@user(\d+)/';
    
    return preg_replace_callback($pattern, function($matches) use ($pdo) {
        $userId = (int)$matches[1];
        
        // データベースからユーザー名を取得
        $stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($user) {
            return "<a href=\"/user/{$userId}\">@{$user['username']}</a>";
        }
        
        return $matches[0];  // ユーザーが見つからない場合はそのまま
    }, $text);
}

// 使用例
// $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
// $text = "こんにちは @user123 さん、@user456 さん";
// echo replaceUserMentions($text, $pdo);
?>

パフォーマンスの最適化

テクニック1: 外部変数の参照を最小限に

<?php
// ❌ 遅い: 毎回配列を参照
$prices = ['apple' => 100, 'banana' => 80];
$result = preg_replace_callback('/\{(\w+)\}/', function($m) use ($prices) {
    return $prices[$m[1]] ?? 0;
}, $text);

// ✅ 速い: 必要なデータだけ参照
$result = preg_replace_callback('/\{(\w+)\}/', 
    fn($m) => $prices[$m[1]] ?? 0, 
    $text
);
?>

テクニック2: 結果のキャッシング

<?php
function processWithCache($text) {
    static $cache = [];
    
    return preg_replace_callback('/expensive_(\w+)/', function($matches) use (&$cache) {
        $key = $matches[1];
        
        if (!isset($cache[$key])) {
            // 重い処理(例: API呼び出し)
            $cache[$key] = expensiveOperation($key);
        }
        
        return $cache[$key];
    }, $text);
}
?>

エラーハンドリング

<?php
function safeReplace($text) {
    $pattern = '/\{eval:([^}]+)\}/';
    
    return preg_replace_callback($pattern, function($matches) {
        $expression = $matches[1];
        
        try {
            // 安全な評価(実際にはeval()は避けるべき)
            // ここでは例示のため
            if (preg_match('/^[\d\s\+\-\*\/\(\)]+$/', $expression)) {
                // 単純な数式のみ許可
                eval('$result = ' . $expression . ';');
                return (string)$result;
            }
            return '[計算エラー: 不正な式]';
        } catch (Throwable $e) {
            return '[計算エラー]';
        }
    }, $text);
}

$text = "結果: {eval:10 + 20}";
echo safeReplace($text) . "\n";
// 出力: 結果: 30
?>

よくある間違いと解決法

間違い1: コールバック内でreturnを忘れる

// ❌ 間違い: 何も返さない
preg_replace_callback('/\d+/', function($m) {
    $m[0] * 2;  // returnがない!
}, $text);

// ✅ 正しい
preg_replace_callback('/\d+/', function($m) {
    return $m[0] * 2;
}, $text);

間違い2: matchesの構造を理解していない

// ❌ 間違い: $matches全体を返す
preg_replace_callback('/(\w+)/', function($matches) {
    return $matches;  // 配列を返してはいけない
}, $text);

// ✅ 正しい
preg_replace_callback('/(\w+)/', function($matches) {
    return strtoupper($matches[1]);  // 文字列を返す
}, $text);

間違い3: useを使わずに外部変数にアクセス

$multiplier = 2;

// ❌ 間違い: $multiplierにアクセスできない
preg_replace_callback('/\d+/', function($m) {
    return $m[0] * $multiplier;  // エラー!
}, $text);

// ✅ 正しい: useを使う
preg_replace_callback('/\d+/', function($m) use ($multiplier) {
    return $m[0] * $multiplier;
}, $text);

preg_replace_callback_arrayで複数パターン処理

<?php
$text = "Hello [b]world[/b]! Visit [url]example.com[/url]";

$result = preg_replace_callback_array([
    '/\[b\](.*?)\[\/b\]/' => fn($m) => '<strong>' . $m[1] . '</strong>',
    '/\[url\](.*?)\[\/url\]/' => fn($m) => '<a href="' . $m[1] . '">' . $m[1] . '</a>',
], $text);

echo $result . "\n";
// 出力: Hello <strong>world</strong>! Visit <a href="example.com">example.com</a>
?>

まとめ

preg_replace_callbackは、PHPで動的な文字列置換を実現する強力な関数です。

重要ポイント:

  • コールバック関数で柔軟な置換処理が可能
  • 計算、条件分岐、外部データ参照など何でもできる
  • $matches配列の構造を理解することが重要
  • 必ず文字列をreturnする
  • パフォーマンスを考慮してキャッシングを活用
  • エラーハンドリングを忘れずに

使い分けガイド:

  • preg_replace: 固定的な置換、後方参照で十分な場合
  • preg_replace_callback: 動的な処理、計算、条件分岐が必要な場合
  • preg_replace_callback_array: 複数の異なるパターンを一度に処理

この関数をマスターすれば、テンプレートエンジンやマークダウンパーサーなど、高度な文字列処理が自在に実装できます!

参考リンク


この記事が役立ったら、ぜひシェアしてください!

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