How to Use Regex in PHP preg Functions With Examples
Use PHP preg functions: preg_match, preg_match_all, preg_replace and callbacks, delimiters and flags, named groups, error checks and the u modifier.

PHP's regex functions are a thin wrapper over the PCRE library, so most Perl-style syntax works, including lookarounds and named groups. The wrapper adds delimiters, return conventions and a few modifiers that you have to know. Errors are silent unless you check. This post is the working tour, building on grep -E versus grep -P for the PCRE background. The examples assume PHP 8.3.
One match
<?php
$line = '203.0.113.7 - - [03/Nov/2026:09:41:02 +0000] "GET /index.html HTTP/1.1" 200 512';
if (preg_match('/^(\S+) .*"(\w+) (\S+) [^"]*" (\d{3}) (\d+)$/', $line, $m) === 1) {
[$all, $ip, $method, $path, $status, $bytes] = $m;
echo "$ip $method $path $status\n";
}203.0.113.7 GET /index.html 200
The delimiter is any non-alphanumeric character that does not appear in the pattern; use # or ~ to avoid escaping slashes in paths. preg_match returns 1 for a match, 0 for none and false for an error, so write === 1 rather than a truthiness test. The array holds the whole match and then each group.
Named groups and all matches
<?php
$text = 'a=1; b=22; c=333';
preg_match_all('/(?<key>\w)=(?<val>\d+)/', $text, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
echo $m['key'], ' => ', $m['val'], "\n";
}a => 1
b => 22
c => 333
The default order for preg_match_all is PREG_PATTERN_ORDER, where the result is grouped by group number; PREG_SET_ORDER gives one array per match, which is easier to loop over. Named groups appear under both the name and the number. See named capture groups for the idea in other languages.
Replace and callbacks
<?php
echo preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', '2026-11-03'), "\n";
echo preg_replace_callback('/\d+/', fn ($m) => $m[0] * 2, 'price: 5 and 12'), "\n";
print_r(preg_split('/[\s,]+/', 'a, b c,d', -1, PREG_SPLIT_NO_EMPTY));03/11/2026
price: 10 and 24
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
References in the replacement are written $1 or \1. Prefer the callback form when the replacement needs logic, because it keeps the pattern simple. The e modifier that once evaluated code was removed in PHP 7, which closed an injection hole.
UTF-8 and errors
<?php
var_dump(preg_match('/^\p{L}+$/u', 'héllo')); // int(1)
var_dump(preg_match('/^\w+$/', 'héllo')); // int(0) without u
$r = preg_match('/(?:a+)+$/', str_repeat('a', 40) . 'b');
if ($r === false) {
echo preg_last_error_msg(), "\n";
}int(1)
int(0)
Backtrack limit exhausted
Without u the engine works on bytes, so accented characters are not letters and . splits multibyte characters. With it, invalid UTF-8 makes the call return false. The backtrack limit (pcre.backtrack_limit, 1,000,000 by default) turns a runaway pattern into an error instead of a hang, but a pattern that hits it is a bug; see catastrophic backtracking. The sample message is representative of PHP 8's wording.
Input in patterns
<?php
$term = 'a.b (c)';
$re = '/' . preg_quote($term, '/') . '/i';
var_dump(preg_match($re, 'A.B (C)')); // int(1)Pass the delimiter as the second argument of preg_quote so it is escaped too. Never skip this for user input. For simple substring checks, str_contains is faster and clearer.
Return values and the checks that matter
The preg_* functions have three kinds of result, and treating them as two causes bugs. preg_match returns 1 for a match, 0 for no match and false for an error. Writing if (preg_match(...)) treats an error like a non-match, so a pattern that fails to compile or a subject that exceeds the backtracking limit looks like ordinary input that did not match. Compare strictly with === false when failure must be noticed, and call preg_last_error_msg() to see the reason in readable form.
The same applies to preg_replace, which returns null on error, not the original string. Assigning the result back to the variable without a check replaces the text with nothing. A tiny wrapper that throws on false or null makes errors loud in one place and keeps call sites clean.
Delimiters, flags and anchors
A PHP pattern is a string with delimiters and trailing flags, such as /^\d+$/i. The delimiter can be any non-alphanumeric character, and choosing one that does not appear in the pattern avoids escaping, so a path-like pattern is cleaner as #^/var/log/.*\.log$#. Flags include i for case-insensitive, m for multiline anchors, s for a dot that matches newlines, x for extended mode with comments, and u for UTF-8. The $ anchor matches before a trailing newline unless the D flag is set, which surprises validation code: "abc\n" passes /^abc$/. Use \z or the D flag when a trailing newline must fail.
In PHP source, backslashes need care in quoting. Single-quoted strings keep most backslashes literal, so '/\d+/' works, while double-quoted strings process escapes first and need doubled backslashes. Prefer single quotes for patterns and a nowdoc for long ones.
Splitting, filtering and callbacks
preg_split breaks text on a pattern and accepts flags such as PREG_SPLIT_NO_EMPTY to drop empty pieces and PREG_SPLIT_DELIM_CAPTURE to keep the separators. preg_grep filters an array of strings and returns the entries that match, or with PREG_GREP_INVERT the ones that do not. preg_quote escapes a string so it can be embedded in a pattern as literal text, and takes the delimiter as a second argument so that it is escaped as well. Without it, user input containing a dot or a bracket changes the meaning of the pattern.
For replacements that depend on the match, use preg_replace_callback with a function that receives the match array and returns the replacement. It is clearer than the removed e modifier ever was and safe, since no code is evaluated from the string. Use preg_replace_callback_array to apply several patterns in a single pass over the subject.
Performance and safety limits
PHP uses PCRE with a JIT compiler enabled by default, which makes most patterns fast. The settings pcre.backtrack_limit and pcre.recursion_limit cap pathological work and cause preg_match to return false when exceeded. Raising them hides a poor pattern and exposes the server to denial of service, so fix the pattern: avoid nested quantifiers such as (a+)+, use possessive quantifiers or atomic groups where backtracking cannot help, and anchor patterns so the engine fails early. Never run a pattern from untrusted users without a length limit on the subject. Before reaching for a regular expression at all, check whether str_contains, str_starts_with or explode does the job, since they are faster and have no error states to handle.
A final habit is to keep patterns in named constants and test them. A class constant such as private const DATE = '/^(\d{4})-(\d{2})-(\d{2})$/D'; can be covered by a table of valid and invalid strings in a unit test, including the empty string, a string with a trailing newline and a multibyte one. Those edge cases are where production surprises hide, and a test that exercises them keeps a later edit from loosening the pattern unnoticed.
For validation of common formats, check whether a dedicated function such as filter_var or DateTime::createFromFormat already does the job with fewer ways to fail.
Where this does not apply
Do not parse HTML or JSON with regex; use DOMDocument and json_decode. The newer preg_* behaviours follow the PCRE2 version PHP is built with, so features like \K or possessive quantifiers need a recent build. See the PHP PCRE manual and the PCRE2 pattern reference.


