PHPpreg_match, preg_replace & More

preg_match, preg_replace & More

Knowing PCRE pattern syntax is only half the picture — the other half is the family of preg_* functions that actually apply a pattern to a string. Each function answers a different question: does this string match, what parts matched, what should the string become after a substitution, or how should this string be broken apart.

preg_match(): does it match, and what did it capture?

preg_match(string $pattern, string $subject, array &$matches = null) tests whether a pattern matches anywhere in the subject string. It returns 1 if it matched, 0 if it didn't, and false if the pattern itself was malformed. When a third argument is passed by reference, it gets filled with the full match at index 0, and any parenthesized capture groups at indices 1, 2, and so on.

Capturing parts of a matched string

PHP
<?php
$log = '[2025-06-15 14:30:00] ERROR: Connection refused';

if (preg_match('/^\[(.+?)\] (\w+): (.+)$/', $log, $matches)) {
    echo "Timestamp: {$matches[1]}\n";
    echo "Level: {$matches[2]}\n";
    echo "Message: {$matches[3]}\n";
}
Timestamp: 2025-06-15 14:30:00
Level: ERROR
Message: Connection refused

Named capture groups, written (?<name>...), make the resulting array self-documenting instead of relying on positional indices that break if the pattern is later reordered.

Named capture groups

PHP
<?php
$log = '[2025-06-15 14:30:00] ERROR: Connection refused';
$pattern = '/^\[(?<timestamp>.+?)\] (?<level>\w+): (?<message>.+)$/';

preg_match($pattern, $log, $matches);
echo $matches['level'], "\n";
ERROR
preg_match_all(): every match, not just the first

preg_match() stops at the first match. preg_match_all() finds every non-overlapping match in the subject and returns them all, as an array of arrays grouped by capture group.

Finding every match in a string

PHP
<?php
$text = 'Contact us at sales@example.com or support@example.com';

preg_match_all('/[\w.+-]+@[\w.-]+\.\w+/', $text, $matches);

print_r($matches[0]);
Array
(
    [0] => sales@example.com
    [1] => support@example.com
)
preg_replace(): pattern-based substitution

preg_replace(string $pattern, string $replacement, string $subject) substitutes every match with a replacement string. The replacement can reference captured groups using $1, $2, and so on, letting parts of the original match be reused in the output.

Reformatting a date using captured groups

PHP
<?php
$input = '2025-06-15';
$output = preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', $input);

echo $output, "\n";
15/06/2025
preg_replace_callback(): replacement logic in code

Sometimes a substitution can't be expressed as a static string with group references — it depends on the matched text itself, like upper-casing it or looking it up somewhere. preg_replace_callback() takes a closure that receives the match array and returns the replacement string for each match.

Computing each replacement with a closure

PHP
<?php
$text = 'hello world, this is php';

$titleCased = preg_replace_callback(
    '/\b\w/',
    fn (array $match): string => strtoupper($match[0]),
    $text
);

echo $titleCased, "\n";
Hello World, This Is Php

\\b is a word-boundary anchor — it matches the position between a word character and a non-word character without consuming any characters itself, so \\b\\w matches exactly the first letter of each word.

preg_split(): breaking a string apart by pattern

explode() splits on a fixed, literal string. preg_split() splits on anything a pattern can describe, which matters when the separator isn't a single consistent character.

Splitting on one or more whitespace characters

PHP
<?php
$input = "one   two\tthree\n\nfour";
$words = preg_split('/\s+/', $input);

print_r($words);
Array
(
    [0] => one
    [1] => two
    [2] => three
    [3] => four
)

Regardless of whether the separator in the original text was spaces, a tab, or multiple newlines, \\s+ treats it all as one delimiter and collapses it away.

A note on performance

PHP's PCRE engine compiles each unique pattern string into an internal representation before running it, and it automatically caches that compiled form — calling the same preg_* function with the same pattern string repeatedly does not recompile it from scratch each time. In practice this means there's rarely a reason to manually "optimize" by trying to reuse a compiled pattern object; the more meaningful performance lever is writing an efficient pattern in the first place.

Unbounded nested quantifiers on user input can hang the process (ReDoS)
Certain pattern shapes — most notoriously nested quantifiers like `(a+)+` or `(.*)*` — can cause the PCRE engine's backtracking to explode combinatorially on a carefully (or accidentally) crafted input, making a single `preg_match()` call take seconds, minutes, or effectively forever. This is a real, well-documented denial-of -service vector (ReDoS) when a pattern like this is run against untrusted input, such as a user-submitted string. Avoid nesting a quantifier directly around a group that itself ends in a quantifier, prefer more specific character classes over broad `.*` wherever possible, and consider setting a `pcre.backtrack_limit` or validating input length before matching against attacker-controlled strings.
  • preg_match() reports whether a pattern matched and captures the first match into a $matches array.

  • preg_match_all() finds every match in the subject, not just the first.

  • preg_replace() substitutes matches with a string that can reference capture groups via $1, $2, etc.

  • preg_replace_callback() computes each replacement with a closure instead of a static string.

  • preg_split() breaks a string apart wherever a pattern matches.

  • Compiled patterns are cached automatically by PHP — manual caching is not needed.

Note
All of these functions accept an array of patterns (and, for `preg_replace()`, an array of replacements) to apply several rules in a single call, which is often clearer than chaining multiple separate calls together.
Tip
Reach for `preg_replace_callback()` instead of `preg_replace_callback _array()`-style tricks whenever a substitution needs anything beyond static text — it keeps the transformation logic readable as ordinary PHP code rather than encoded into an unreadable replacement string.