PHP RegEx Functions
PHP's regular expression support is provided by the PCRE extension, which is always enabled. This lesson is a complete reference to its functions, flags, error codes, and settings, with examples for t
PHP's regular expression support is provided by the PCRE extension, which is always enabled. This lesson is a complete reference to its functions, flags, error codes, and settings, with examples for the functions not covered in the previous lessons (preg_grep(), preg_filter(), and preg_quote()), plus a reusable approach to error handling.
Function Reference
| Function | Purpose | Returns |
|---|---|---|
preg_match($pattern, $subject, &$matches, $flags, $offset) | First match | 1, 0, or false |
preg_match_all($pattern, $subject, &$matches, $flags, $offset) | All matches | Number of matches, or false |
preg_replace($pattern, $replacement, $subject, $limit, &$count) | Replace matches | String/array, or null on error |
preg_replace_callback($pattern, $callback, $subject, $limit, &$count, $flags) | Replace using a callback | String/array, or null |
preg_replace_callback_array($patternsToCallbacks, $subject, $limit, &$count, $flags) | Several patterns, each with its callback | String/array, or null |
preg_filter($pattern, $replacement, $subject, $limit, &$count) | Like preg_replace(), but returns only subjects that matched | String/array, or null |
preg_split($pattern, $subject, $limit, $flags) | Split by a pattern | Array, or false |
preg_grep($pattern, $array, $flags) | Array elements that match | Array, or false |
preg_quote($string, $delimiter) | Escape regex special characters | Escaped string |
preg_last_error() | Error code of the last PCRE call | Integer constant |
preg_last_error_msg() | Error message of the last PCRE call (PHP 8.0+) | String |
The core functions are explained in preg_match, preg_replace, and preg_split.
preg_grep() — Filter an Array by Pattern
<?php
$values = ['10', 'abc', '25', '3.5', '007'];
print_r(preg_grep('/^\d+$/', $values));
print_r(preg_grep('/^\d+$/', $values, PREG_GREP_INVERT));
Output
Array
(
[0] => 10
[2] => 25
[4] => 007
)
Array
(
[1] => abc
[3] => 3.5
)
preg_grep()returns the elements that match, preserving their keys.PREG_GREP_INVERTreturns the elements that do not match.
It is a shortcut for array_filter($values, fn($v) => preg_match($pattern, $v) === 1).
preg_filter() — Replace Only Where Something Matched
<?php
$items = ['item1', 'misc', 'item22'];
print_r(preg_replace('/(\d+)/', '#$1', $items));
print_r(preg_filter('/(\d+)/', '#$1', $items));
Output
Array
(
[0] => item#1
[1] => misc
[2] => item#22
)
Array
(
[0] => item#1
[2] => item#22
)
preg_replace() returns every subject, changed or not. preg_filter() returns only the subjects in which at least one replacement happened — combining filtering and replacement in one step. For a single string subject, it returns null when nothing matched.
preg_quote() — Escape Text for Use in a Pattern
<?php
$userSearch = 'C++ (v2.0) #1';
$pattern = '/' . preg_quote($userSearch, '/') . '/i';
echo $pattern . "\n";
var_dump(preg_match($pattern, 'Learn c++ (V2.0) #1 today'));
Output
/C\+\+ \(v2\.0\) \#1/i
int(1)
preg_quote() adds a backslash before every character with special meaning in a pattern:
. \ + * ? [ ^ ] $ ( ) { } = ! < > | : - #
plus the NUL character and, if given, the delimiter (the second argument). The # character has been escaped since PHP 7.3, which makes quoted text safe with # delimiters and in extended (x) mode. Always quote user input and other dynamic text before inserting it into a pattern.
Flags Reference
| Flag | Used With | Effect |
|---|---|---|
PREG_PATTERN_ORDER | preg_match_all() | Results grouped by group number (default) |
PREG_SET_ORDER | preg_match_all() | Results grouped by match |
PREG_OFFSET_CAPTURE | preg_match(), preg_match_all(), callbacks | Adds the byte offset to each match |
PREG_UNMATCHED_AS_NULL | preg_match(), preg_match_all(), callbacks | Unmatched groups become null |
PREG_SPLIT_NO_EMPTY | preg_split() | Removes empty pieces |
PREG_SPLIT_DELIM_CAPTURE | preg_split() | Includes captured separators |
PREG_SPLIT_OFFSET_CAPTURE | preg_split() | Adds offsets to pieces |
PREG_GREP_INVERT | preg_grep() | Returns non-matching elements |
Flags can be combined with |, for example PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL.
Errors
How Errors Are Reported
PCRE functions do not throw exceptions. Instead:
| Problem | What Happens |
|---|---|
| Invalid pattern (syntax error, missing delimiter) | A warning is emitted, and the function returns false (or null for replace functions) |
Runtime error (invalid UTF-8 with u, limits exceeded) | No warning; the function returns false/null, and the error is available from preg_last_error() |
Error Codes
| Constant | Meaning |
|---|---|
PREG_NO_ERROR | No error |
PREG_INTERNAL_ERROR | Internal error (including pattern compilation failures) |
PREG_BACKTRACK_LIMIT_ERROR | pcre.backtrack_limit exceeded |
PREG_RECURSION_LIMIT_ERROR | pcre.recursion_limit exceeded |
PREG_BAD_UTF8_ERROR | The subject contains invalid UTF-8 (with u) |
PREG_BAD_UTF8_OFFSET_ERROR | The offset points into the middle of a UTF-8 character |
PREG_JIT_STACKLIMIT_ERROR | The JIT compiler ran out of stack space |
A Wrapper That Throws Exceptions
Because silent false results are easy to miss, applications often wrap PCRE calls:
<?php
function regexMatch(string $pattern, string $subject, ?array &$matches = null): bool
{
$result = preg_match($pattern, $subject, $matches);
if ($result === false) {
throw new RuntimeException('Regex error: ' . preg_last_error_msg());
}
return $result === 1;
}
var_dump(regexMatch('/^\d+$/', '12345'));
try {
regexMatch('/\w+/u', "abc\xFF");
} catch (RuntimeException $e) {
echo $e->getMessage() . "\n";
}
Output
bool(true)
Regex error: Malformed UTF-8 characters, possibly incorrectly encoded
The wrapper returns a clean boolean for "matched / not matched" and turns failures into exceptions, so errors cannot be mistaken for "no match". Libraries such as composer/pcre provide complete, well-tested wrappers of this kind for all PCRE functions.
PCRE Configuration
php.ini Setting | Default | Purpose |
|---|---|---|
pcre.backtrack_limit | 1000000 | Maximum backtracking steps per call |
pcre.recursion_limit | 100000 | Maximum recursion depth |
pcre.jit | 1 | Enables the just-in-time compiler for faster matching |
These limits protect the server from patterns that would otherwise run for a very long time. Raising them hides the problem; rewriting an inefficient pattern solves it.
PHP also caches compiled patterns, so using the same pattern repeatedly in a loop is efficient. Building a different pattern for every iteration (for example, from user input) cannot benefit from the cache as much.
Choosing the Right Function
| Task | Function |
|---|---|
| Validate a format | preg_match() |
| Extract parts of one match | preg_match() with groups |
| Extract all occurrences | preg_match_all() |
| Replace with a fixed pattern | preg_replace() |
| Replace with calculated text | preg_replace_callback() |
| Replace and keep only changed items | preg_filter() |
| Split by varying separators | preg_split() |
| Filter an array by pattern | preg_grep() |
| Insert dynamic text into a pattern | preg_quote() |
Practical Example: A Reusable Format Validator
<?php
const FORMATS = [
'pincode' => '/^[1-9]\d{5}\z/',
'mobile' => '/^[6-9]\d{9}\z/',
'ifsc' => '/^[A-Z]{4}0[A-Z0-9]{6}\z/',
'vehicle' => '/^[A-Z]{2}\d{2}[A-Z]{1,2}\d{4}\z/',
];
function validateFormats(array $input): array
{
$results = [];
foreach ($input as $field => $value) {
if (!array_key_exists($field, FORMATS)) {
$results[$field] = 'unknown field';
continue;
}
$normalized = strtoupper(preg_replace('/\s+/', '', (string) $value) ?? '');
$results[$field] = preg_match(FORMATS[$field], $normalized) === 1 ? 'valid' : 'invalid';
}
return $results;
}
$input = [
'pincode' => '600 042',
'mobile' => '98765',
'ifsc' => 'sbin0001234',
'vehicle' => 'TN 09 AB 1234',
'aadhaar' => '1234 5678 9012',
];
foreach (validateFormats($input) as $field => $result) {
echo str_pad($field, 8) . " $result\n";
}
Output
pincode valid
mobile invalid
ifsc valid
vehicle valid
aadhaar unknown field
Step-by-Step Explanation
- All patterns live in one constant array, so formats are defined in a single place and reused across forms.
array_key_exists()checks whether a format is defined. (isset()cannot be used on a constant expression such asFORMATS[$field]; that is a compile-time error.)- Input is normalized before validation: whitespace is removed with
preg_replace()and letters are uppercased. Users can type600 042ortn 09 ab 1234and still pass. \zanchors ensure nothing follows the valid part — not even a newline.- Fields without a known format are reported instead of silently accepted.
An IFSC code (Indian bank branch code) has four letters for the bank, a 0, and six characters for the branch. As with all format checks, a valid format does not prove that the code actually exists.
Common Mistakes
Treating false/null as "No Match"
Effect: Pattern and encoding errors go unnoticed.
Fix: Check === false / === null, or use a throwing wrapper.
Not Escaping Dynamic Text
Effect: User input breaks the pattern or changes its meaning.
Fix: preg_quote($text, $delimiter).
Raising PCRE Limits to Fix Slow Patterns
Effect: The server spends more time on inefficient matches, which can be exploited for denial of service.
Fix: Rewrite the pattern (avoid nested quantifiers; use specific classes).
Re-implementing preg_grep() and preg_filter()
Effect: More code than necessary.
Fix: Use the built-in functions.
Best Practices
- Pick the most specific function for each task.
- Check for errors and convert them to exceptions in application code.
- Escape dynamic text with
preg_quote(). - Keep reusable patterns in one place, and normalize input before validating it.
- Keep PCRE limits at their defaults and optimize patterns instead.
Official reference: https\://www.php.net/manual/en/ref.pcre.php