Lesson 92 of 100
Regular expressions15 min5 checksPHP 8.5

Transform Text with Regex Callbacks

Learn how preg_replace_callback() lets a function compute each replacement, using match arrays, named groups and a replacement counter.

01 · THE IDEAREAD → TRY → LEARN

Why use a callback?

A plain replacement string works when every match becomes the same text, or when you can build the result from fixed pieces such as backreferences. Often the new text has to be computed: add one to a year, pad a number, or look a word up in a table. For those jobs PHP offers preg_replace_callback(). It finds every match of a pattern, hands each match to your function, and puts whatever string the function returns in place of the match.

preg_replace_callback(
    string|array $pattern,
    callable $callback,
    string|array $subject,
    int $limit = -1,
    int &$count = null,
    int $flags = 0
): string|array|null

The callback contract

The callback receives one array. Index 0 holds the whole match, index 1 holds the first capture group, index 2 the second, and so on. If you name a group with (?<name>...), the array also has that name as a key. The callback must return a string, and that string is inserted literally. It is not scanned for $1 style references.

The subject can be a string or an array of strings. The return value has the same shape. If the pattern is invalid, a warning is emitted and the result is null. If nothing matches, you get the subject back unchanged.

A first example

The match is always a string, so convert it before doing arithmetic, then convert the result back.

Example
<?php
$text = "Release 2024, backport 2023";
echo preg_replace_callback('/\d{4}/', fn(array $m): string => (string) ((int) $m[0] + 1), $text), "\n";
Output
Release 2025, backport 2024

Every run of four digits is passed to the arrow function, incremented and returned as text.

Capture groups, limit and count

The fourth argument, $limit, caps the replacements per pattern and per subject. -1 means no cap. The fifth argument is passed by reference and receives the number of replacements made, so initialise a variable before the call.

Example
<?php
$text = "cat=3; dog=10";
$count = 0;
$result = preg_replace_callback(
    '/([a-z]+)=(\d+)/',
    fn(array $m): string => strtoupper($m[1]) . ':' . str_pad($m[2], 3, '0', STR_PAD_LEFT),
    $text,
    -1,
    $count
);
echo json_encode(['text' => $result, 'count' => $count], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"text":"CAT:003; DOG:010","count":2}

Here $m[1] is the word and $m[2] is the number. The count is 2 because two matches were replaced.

Lookups with named groups

A very common use is a tiny template engine. The callback looks the token name up in an array. If the name is missing, returning $m[0] leaves the original token in place, which is a safe default.

Example
<?php
$values = ['name' => 'Ada', 'role' => 'admin'];
$template = 'Hi {name}, role: {role}, team: {team}';
echo preg_replace_callback(
    '/\{(?<key>[a-z]+)\}/',
    fn(array $m): string => $values[$m['key']] ?? $m[0],
    $template
), "\n";
Output
Hi Ada, role: admin, team: {team}

The {team} token has no entry, so the null coalescing operator falls back to the original text.

Pitfalls to watch

  • Greedy or too-narrow patterns. \d matches one digit, while \d+ matches a whole number. Choosing the wrong one changes what the callback sees. For example 15 would become two separate matches.
  • Forgetting type conversion. Captured values are strings. Cast them before arithmetic.
  • Returning non-strings. The callback is meant to return a string. Be explicit with a return type and a cast.
  • Replacement is not re-scanned. The text you return is not matched again, so a callback cannot accidentally loop.
  • Unchecked failures. A broken pattern yields null. In real code, check for it before using the result.

Exercise

Write double_numbers(string $text): string. Every run of consecutive digits in the text must be replaced by its value multiplied by two, and all other characters stay as they are. Leading zeros disappear because the value is converted to an integer, so 007 becomes 14. Think about which quantifier makes a whole number a single match, and remember that the callback receives an array, not a string.

Recap

Use preg_replace_callback() when replacement text must be calculated from the match. Read the whole match from $m[0], groups from numeric or named keys, return a string, and use $limit and $count when you need to control or measure the work. Return $m[0] to leave a match unchanged.

Before you start

Functions · Arrays · Types

You’ll practice
  • Replace matched tokens using deterministic callbacks
  • Implement and test a deterministic PHP 8.4 function
Your task

Make double_numbers() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.