PHP RegEx
What Is a Regular Expression? In simple terms: A regular expression (regex) is a pattern that describes what text should look like — for example, "exactly ten digits starting with 6, 7, 8, or 9" or "a
What Is a Regular Expression?
In simple terms: A regular expression (regex) is a pattern that describes what text should look like — for example, "exactly ten digits starting with 6, 7, 8, or 9" or "a word that starts with #". PHP can use such a pattern to check, find, extract, replace, or split text.
Technically: PHP's regex functions use the PCRE2 library (Perl Compatible Regular Expressions), which is built into PHP and always available. Functions in this family start with preg_. A pattern is written as a string with delimiters around it and optional modifiers after it.
Why Regular Expressions Are Useful
Simple string functions (str_contains(), str_replace(), explode()) work with fixed text. Regular expressions work with patterns:
| Task | Without Regex | With Regex |
|---|---|---|
"Does the text contain php?" | str_contains() | Not needed |
| "Is this a valid Indian mobile number?" | Many checks on length, digits, first digit | /^[6-9]\d{9}$/ |
| "Extract all hashtags from a post" | Manual character-by-character parsing | /#(\w+)/ |
| "Collapse any run of whitespace into one space" | Loops | preg_replace('/\s+/', ' ', $text) |
Use string functions for fixed text, and regular expressions when the text follows a pattern.
Anatomy of a Pattern
/^[6-9]\d{9}\$/i
│└────┬─────┘│└── modifier(s), e.g. i \= case-insensitive
│ │ └─── closing delimiter
│ └────────── the pattern itself
└──────────────── opening delimiter
- Delimiters mark the start and end of the pattern.
/is most common, but any non-alphanumeric, non-whitespace character except backslash works, such as#or~. Brackets can also be used in pairs:{...},(...),[...]. - Modifiers after the closing delimiter change how the pattern is applied (see RegEx Modifiers).
- Write patterns in single-quoted strings, so PHP does not interpret backslashes and
$before the regex engine sees them.
Choosing a different delimiter avoids escaping when the pattern contains /:
<?php
var_dump(preg_match('/^https:\/\//', 'https\://gocourse.in'));
var_dump(preg_match('#^https\://#', 'https\://gocourse.in'));
Output
int(1)
int(1)
Both patterns are equivalent; the second is easier to read.
The preg_* Functions
| Function | Purpose | Returns | Lesson |
|---|---|---|---|
preg_match() | Test a pattern / get the first match | 1, 0, or false on error | preg_match |
preg_match_all() | Find all matches | Number of matches, or false | preg_match |
preg_replace() | Replace matches | New string (or array) | preg_replace |
preg_replace_callback() | Replace using a function | New string | preg_replace |
preg_split() | Split a string by a pattern | Array of pieces | preg_split |
preg_grep() | Filter an array by a pattern | Matching elements | PHP RegEx Functions |
preg_quote() | Escape special characters in text | Escaped string | PHP RegEx Functions |
preg_last_error() / preg_last_error_msg() | Get the last error | Error code / message (8.0+) | PHP RegEx Functions |
First Examples
Testing for a Match
<?php
var_dump(preg_match('/php/i', 'I love PHP'));
var_dump(preg_match('/python/i', 'I love PHP'));
Output
int(1)
int(0)
preg_match() returns 1 if the pattern is found and 0 if not. The i modifier makes the match case-insensitive. Because it can also return false on error, compare with === 1 when using it as a condition.
Validating a Format
<?php
function isIndianMobile(string \$number): bool
{
return preg_match('/^[6-9]\d{9}\$/', \$number) \=== 1;
}
foreach (['9876543210', '1234567890', '98765', '98765432101'] as \$number) {
echo \$number . ': ' . (isIndianMobile(\$number) ? 'valid' : 'invalid') . "\n";
}
Output
9876543210: valid
1234567890: invalid
98765: invalid
98765432101: invalid
Reading the Pattern
| Part | Meaning |
|---|---|
^ | Start of the string |
[6-9] | One character in the range 6–9 |
\d{9} | Exactly nine digits |
$ | End of the string |
Without ^ and $, the pattern would also match a valid number inside longer text, such as abc9876543210xyz. Anchors are essential for validation. (Strictly, $ also allows a trailing newline; use \z or the D modifier for exact end-of-string matching — see RegEx Patterns.)
Finding All Matches
<?php
\$text \= 'Order 12 has 3 items costing 450';
\$count \= preg_match_all('/\d+/', \$text, \$matches);
echo "Found \$count numbers: " . implode(', ', \$matches[0]) . "\n";
Output
Found 3 numbers: 12, 3, 450
\d+ means "one or more digits". preg_match_all() stores every match in $matches[0].
Replacing and Splitting
<?php
echo preg_replace('/\s+/', ' ', "too many \n\t spaces") . "\n";
print_r(preg_split('/[,;\s]+/', 'php, mysql;laravel html'));
Output
too many spaces
Array
(
[0] \=> php
[1] \=> mysql
[2] \=> laravel
[3] \=> html
)
\s+matches any run of whitespace (spaces, tabs, newlines), which is replaced by a single space.[,;\s]+matches any run of commas, semicolons, or whitespace, so the string can be split no matter which separators were used.
Escaping User Input With preg_quote()
When building a pattern from user input, special characters in the input must be escaped; otherwise, they are interpreted as regex syntax:
<?php
\$search \= 'C++ (advanced)';
\$pattern \= '/' . preg_quote(\$search, '/') . '/i';
echo \$pattern . "\n";
var_dump(preg_match(\$pattern, 'Course: c++ (Advanced) batch'));
Output
/C\+\+ \(advanced\)/i
int(1)
preg_quote() escapes every character with a special meaning, and its second argument also escapes the delimiter. Without it, + and ( would be treated as a quantifier and a group, and the search would fail or produce an error.
Handling Errors
An invalid pattern does not throw an exception. PHP emits a warning and the function returns false:
<?php
\$result \= preg_match('/[a-z/', 'abc');
var_dump(\$result);
echo preg_last_error_msg() . "\n";
Output
PHP Warning: preg_match(): Compilation failed: missing terminating ] for character class at offset 4 in /path/script.php on line 2
bool(false)
Internal error
- The pattern is missing the closing
]of a character class, so PCRE cannot compile it. falseis easy to confuse with0(no match) in a loose condition likeif (preg_match(...)). Use=== 1for "matched".preg_last_error_msg()(PHP 8.0+) describes the last error. Runtime errors, such as exceeding the backtracking limit or invalid UTF-8 input with theumodifier, are also reported through it.
Catastrophic Backtracking
Some patterns with nested repetition, such as /(a+)+$/, can take an enormous amount of time on certain inputs, because the engine tries an exponential number of ways to match. PHP limits this (pcre.backtrack_limit), and the function returns false with an error such as "Backtrack limit exhausted". Avoid nested quantifiers on the same characters, and keep patterns specific.
When Not to Use Regular Expressions
| Task | Better Tool |
|---|---|
| Check for fixed text | str_contains(), str_starts_with(), str_ends_with() |
| Replace fixed text | str_replace() |
| Split on a single fixed separator | explode() |
| Validate emails, URLs, IPs, integers | filter_var() with validation filters (see PHP Form URL/E-mail) |
| Parse HTML or XML | DOMDocument (or Dom\HTMLDocument in PHP 8.4+) |
| Parse JSON or CSV | json_decode(), fgetcsv() / str_getcsv() |
String functions are faster and easier to read for simple tasks, and dedicated parsers handle complex formats correctly where a regex would fail on edge cases.
Practical Example: Extracting Hashtags and Mentions
<?php
function extractTags(string \$post): array
{
preg_match_all('/#(\w+)/', \$post, \$hashtags);
preg_match_all('/@(\w+)/', \$post, \$mentions);
return [
'hashtags' \=> array_values(array_unique(array_map('strtolower', \$hashtags[1]))),
'mentions' \=> array_values(array_unique(\$mentions[1])),
];
}
\$post \= 'Learning #PHP with @asha and @bala_dev! #webdev #php8 #php';
print_r(extractTags(\$post));
Output
Array
(
[hashtags] \=> Array
(
[0] \=> php
[1] \=> webdev
[2] \=> php8
)
[mentions] \=> Array
(
[0] \=> asha
[1] \=> bala_dev
)
)
Step-by-Step Explanation
#(\w+)matches#followed by one or more word characters (letters, digits, underscore). The parentheses create a capturing group, so$hashtags[1]contains just the tag text without#.@(\w+)does the same for mentions.- Hashtags are lowercased and de-duplicated, so
#PHPand#phpcount as one tag. array_values()re-indexes the results afterarray_unique().
Note that \w matches only ASCII letters by default; for hashtags in other scripts (such as Tamil), use the u modifier and Unicode classes, covered in RegEx Patterns and RegEx Modifiers.
Common Mistakes
Forgetting Delimiters
Effect: preg_match('^\d+$', ...) produces a "Delimiter must not be alphanumeric or backslash" warning (the exact wording differs slightly between versions).
Fix: Wrap the pattern: '/^\d+$/'.
Missing Anchors in Validation
Effect: '/\d{6}/' accepts 'abc123456xyz'.
Fix: Use ^ and $ (or \A and \z).
Using Double-Quoted Patterns
Effect: PHP interprets \$, \n, or variables before the regex engine sees them.
Fix: Use single quotes.
Inserting User Input Unescaped
Effect: Broken patterns, errors, or unintended matches.
Fix: preg_quote($input, '/').
Treating false as "No Match"
Effect: Pattern errors go unnoticed.
Fix: Check === 1 for a match and handle false as an error.
Best Practices
- Use regular expressions for patterns and string functions for fixed text.
- Anchor validation patterns and keep them as specific as possible.
- Write patterns in single quotes and choose delimiters that avoid escaping.
- Escape dynamic text with
preg_quote(). - Check for errors with
=== falseandpreg_last_error_msg().
Official reference: https\://www.php.net/manual/en/book.pcre.php