Regular expressions7 min readExamples run on PHP 8.5.10

preg_replace_callback()

Learn how preg_replace_callback() lets a PHP function compute each regex replacement, with its signature, return values, edge cases and tested examples.

Signature

preg_replace_callback(
    string|array $pattern,
    callable $callback,
    string|array $subject,
    int $limit = -1,
    int &$count = null,
    int $flags = 0
): string|array|null
Available since PHP 4.0.57.48.08.18.28.38.48.5All versions →

Purpose

preg_replace_callback() searches a string with a regular expression and replaces every match with a value your own function computes. It behaves almost like preg_replace(), but instead of a fixed replacement string you supply a callback. This is the right tool whenever the new text depends on the matched text: incrementing numbers, looking up values in a map, changing case selectively, or reformatting a captured fragment.

Signature

Parameters and defaults

  • $pattern: a pattern string, or an array of pattern strings, including delimiters and modifiers.
  • $callback: any callable. It receives one array of matches and should return the replacement string. A typical handler looks like function (array $matches): string.
  • $subject: a string, or an array of strings. With an array, every element is processed and keys are preserved.
  • $limit: the maximum number of replacements per pattern in each subject string. The default -1 means no limit.
  • $count: an optional by-reference variable that receives the number of replacements performed.
  • $flags: a combination of PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL, which change the shape of the matches array in the same way as for preg_match(). The manual lists the flags parameter as added in PHP 7.4.0.

The matches array

Index 0 holds the whole match, index 1 the first parenthesized group, and so on. Named groups such as (?<word>\w+) also appear under their name. With PREG_OFFSET_CAPTURE, each entry becomes a pair of the text and its byte offset. With PREG_UNMATCHED_AS_NULL, groups that did not participate are reported as null rather than an empty string.

Return value and failure cases

The function returns an array when $subject is an array and a string otherwise. If nothing matches, the subject comes back unchanged. On an error it returns null. If the pattern does not compile into a valid regular expression, PHP emits an E_WARNING. You can inspect runtime PCRE problems with preg_last_error().

Example 1: computing a replacement from a capture

The callback receives two groups, the day and month part and the year. It returns the first group unchanged and the year increased by one.

Example
<?php
$text = "Due 04/01/2002 and 12/24/2001";
echo preg_replace_callback(
    '|(\d{2}/\d{2}/)(\d{4})|',
    fn(array $m): string => $m[1] . ((int) $m[2] + 1),
    $text
), "\n";
Output
Due 04/01/2003 and 12/24/2002

Example 2: template placeholders and the count

Here the callback looks up each placeholder name in a map. Unknown names return the original match so they stay visible. The $count variable reports how many replacements the engine performed. A callback that returns the original text still counts as a replacement.

Example
<?php
$template = "Hello {name}, your plan is {plan}. {missing}";
$values = ['name' => 'Ada', 'plan' => 'Pro'];
$count = 0;
$result = preg_replace_callback(
    '/\{(\w+)\}/',
    fn(array $m): string => $values[$m[1]] ?? $m[0],
    $template,
    -1,
    $count
);
echo json_encode(['result' => $result, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"result":"Hello Ada, your plan is Pro. {missing}","count":3}

Example 3: array subjects with a limit

When the subject is an array, the result is an array with the same keys. The limit applies to each element separately, and $count is the total across all elements. Strings without matches are returned untouched.

Example
<?php
$subjects = ['a1 b2 c3', 'x9 y8', 'none'];
$count = 0;
$result = preg_replace_callback(
    '/\d/',
    fn(array $m): string => (string) ($m[0] * 2),
    $subjects,
    1,
    $count
);
echo json_encode(['result' => $result, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"result":["a2 b2 c3","x18 y8","none"],"count":2}

Edge cases

  • Unmatched groups. A group that did not take part in the match may be missing from the array or be an empty string, depending on its position. If you need a predictable shape, use PREG_UNMATCHED_AS_NULL and test for null.
  • Multiple patterns. When $pattern is an array, the patterns run in order, and each one works on the output of the previous one. The same callback is used for all of them.
  • Empty matches. A pattern that can match an empty string, such as /x*/, calls your callback at positions where nothing is consumed. Make sure your callback handles an empty $matches[0].
  • Recursion. A callback may itself call preg_replace_callback(). The manual demonstrates a recursive pattern that handles nested BB code this way.
  • Errors. An invalid pattern gives a warning and a null result, so check for null before you use the value as a string.

Common mistakes

  1. Forgetting to return. A callback that echoes or has no return yields an empty replacement and silently deletes the matched text.
  2. Using the wrong index. $matches[0] is the whole match, not the first group. Capture groups start at 1.
  3. Reading outer variables in a classic closure. A function declared with function needs use ($values) to see outside variables. An arrow function captures them by value automatically.
  4. Expecting $count to count changes. It counts replacements performed, even when the callback returns the same text.
  5. Using the callback for a fixed replacement. If the replacement does not depend on the match, preg_replace() is simpler.
  6. Not escaping user input in patterns. Run user-supplied literals through preg_quote() before putting them into a pattern.
  • preg_replace() replaces matches with a fixed string or with backreferences.
  • preg_replace_callback_array() applies several pattern and callback pairs in one call.
  • preg_quote() escapes literal text for use in a pattern.
  • preg_last_error() reports the code of the last PCRE error.
  • preg_match() documents the flag behavior used for the matches array.

Recap

Use preg_replace_callback() when each replacement must be calculated from the matched text. The callback gets an array of matches and must return a string. Pass $limit to cap replacements, read $count to see how many occurred, and treat a null return as a failure.

Tested examples

Increment years captured by a pattern
<?php
$text = "Due 04/01/2002 and 12/24/2001";
echo preg_replace_callback(
    '|(\d{2}/\d{2}/)(\d{4})|',
    fn(array $m): string => $m[1] . ((int) $m[2] + 1),
    $text
), "\n";
Output
Due 04/01/2003 and 12/24/2002
Fill template placeholders and read the count
<?php
$template = "Hello {name}, your plan is {plan}. {missing}";
$values = ['name' => 'Ada', 'plan' => 'Pro'];
$count = 0;
$result = preg_replace_callback(
    '/\{(\w+)\}/',
    fn(array $m): string => $values[$m[1]] ?? $m[0],
    $template,
    -1,
    $count
);
echo json_encode(['result' => $result, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"result":"Hello Ada, your plan is Pro. {missing}","count":3}
Array subject with a per-string limit
<?php
$subjects = ['a1 b2 c3', 'x9 y8', 'none'];
$count = 0;
$result = preg_replace_callback(
    '/\d/',
    fn(array $m): string => (string) ($m[0] * 2),
    $subjects,
    1,
    $count
);
echo json_encode(['result' => $result, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"result":["a2 b2 c3","x18 y8","none"],"count":2}