RegEx Patterns
A regular expression pattern is built from small pieces: literal characters, character classes, quantifiers, anchors, groups, and assertions. This lesson explains each building block with a short exam
A regular expression pattern is built from small pieces: literal characters, character classes, quantifiers, anchors, groups, and assertions. This lesson explains each building block with a short example, so you can both write patterns and read patterns written by others.
Literal Characters and Metacharacters
Most characters match themselves: the pattern /cat/ finds the text cat. A few characters have special meaning — these are the metacharacters:
. ^ $ * + ? { } [ ] \ | ( )
To match a metacharacter literally, escape it with a backslash:
<?php
var_dump(preg_match('/1.5/', '1x5')); // . matches any character
var_dump(preg_match('/1\.5/', '1x5')); // \. matches only a dot
var_dump(preg_match('/1\.5/', '1.5'));
Output
int(1)
int(0)
int(1)
The unescaped dot matched x, which is a common source of overly permissive patterns — for example, /example.com/ also matches exampleXcom.
Character Classes [...]
A character class matches one character from a set:
| Pattern | Matches |
|---|---|
[aeiou] | One vowel |
[a-z] | One lowercase ASCII letter |
[A-Za-z0-9] | One letter or digit |
[^0-9] | One character that is not a digit (^ at the start negates) |
[-.] or [.0] | A hyphen or a dot |
Inside a class, most metacharacters lose their special meaning: [.] matches a literal dot. Only ], 0, ^(at the start), and-` (between characters) are special.
Example: Vehicle Registration Numbers
<?php
$pattern = '/^[A-Z]{2}\d{2}[A-Z]{1,2}\d{4}$/';
foreach (['TN09AB1234', 'KA05M4321', 'TN9AB1234', 'tn09ab1234'] as $plate) {
echo $plate . ': ' . (preg_match($pattern, $plate) === 1 ? 'valid' : 'invalid') . "\n";
}
Output
TN09AB1234: valid
KA05M4321: valid
TN9AB1234: invalid
tn09ab1234: invalid
The pattern requires two uppercase letters (state code), two digits (district), one or two letters (series), and four digits. Lowercase input fails because [A-Z] is case-sensitive; normalize with strtoupper() first or add the i modifier.
Shorthand Classes and the Dot
| Shorthand | Matches | Equivalent (ASCII) |
|---|---|---|
\d | A digit | [0-9] |
\D | A non-digit | [^0-9] |
\w | A word character | [A-Za-z0-9_] |
\W | A non-word character | [^A-Za-z0-9_] |
\s | Whitespace (space, tab, newline, …) | |
\S | Non-whitespace | |
. | Any character except a newline |
<?php
preg_match_all('/\w+/', 'Hello, world_2026! Ready?', $matches);
print_r($matches[0]);
Output
Array
(
[0] => Hello
[1] => world_2026
[2] => Ready
)
Punctuation and spaces are not word characters, so they separate the matches. Note that the underscore is a word character.
Quantifiers
Quantifiers specify how many times the preceding element may repeat:
| Quantifier | Meaning | Example | Matches |
|---|---|---|---|
* | 0 or more | ab*c | ac, abc, abbbc |
+ | 1 or more | ab+c | abc, abbbc (not ac) |
? | 0 or 1 (optional) | colou?r | color, colour |
{n} | Exactly n | \d{6} | Six digits |
{n,} | n or more | \d{2,} | Two or more digits |
{n,m} | Between n and m | [a-z]{3,8} | 3 to 8 lowercase letters |
Greedy vs Lazy
By default, quantifiers are greedy: they match as much as possible. Adding ? after a quantifier makes it lazy: it matches as little as possible.
<?php
$html = '<b>bold</b> text';
preg_match('/<.+>/', $html, $greedy);
preg_match('/<.+?>/', $html, $lazy);
echo $greedy[0] . "\n";
echo $lazy[0] . "\n";
Output
<b>bold</b>
<b>
- Greedy
.+runs to the last>in the string. - Lazy
.+?stops at the first>.
An even more precise alternative is a negated class: /<[^>]+>/ matches "a <, then any characters that are not >, then >", which is both clear and efficient. (For real HTML processing, use a DOM parser rather than regular expressions.)
Anchors and Boundaries
Anchors match positions, not characters:
| Anchor | Position |
|---|---|
^ | Start of the string (or line, with the m modifier) |
$ | End of the string — or just before a final newline (or end of line with m) |
\A | Start of the string only |
\z | End of the string only |
\b | A word boundary (between a word character and a non-word character) |
\B | Not a word boundary |
Word Boundaries
<?php
$text = 'cat catalog concat cat.';
echo preg_match_all('/cat/', $text) . "\n";
echo preg_match_all('/\bcat\b/', $text) . "\n";
Output
4
2
Without boundaries, cat is also found inside catalog and concat. With \b on both sides, only the whole word matches.
$ vs \z
<?php
var_dump(preg_match('/^\d+$/', "123\n"));
var_dump(preg_match('/^\d+\z/', "123\n"));
Output
int(1)
int(0)
$ allows a trailing newline, so "123\n" passes a ^\d+$ check. For strict validation, use \z (or the D modifier, see RegEx Modifiers).
Alternation and Groups
Alternation |
| means "or". Combined with a group, it limits the alternatives to part of the pattern:
<?php
$pattern = '/\.(?:jpe?g|png|webp)$/i';
foreach (['photo.JPG', 'icon.png', 'scan.jpeg', 'notes.pdf'] as $file) {
echo $file . ': ' . (preg_match($pattern, $file) === 1 ? 'image' : 'not an image') . "\n";
}
Output
photo.JPG: image
icon.png: image
scan.jpeg: image
notes.pdf: not an image
(?:...)is a non-capturing group: it groups the alternatives without storing the match.jpe?gmatches bothjpgandjpeg.- Checking a file name's extension like this is fine for display or routing, but not for security — see \$_FILES for validating uploads by content.
Capturing Groups
Parentheses (...) capture the text they match. Captured groups appear in the matches array by number:
<?php
preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', '2026-10-15', $m);
print_r($m);
Output
Array
(
[0] => 2026-10-15
[1] => 2026
[2] => 10
[3] => 15
)
Index 0 holds the full match; indexes 1, 2, 3 hold the groups, numbered by the position of their opening parenthesis.
Named Groups
Naming groups makes code self-explanatory:
<?php
preg_match('/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/', '2026-10-15', $m);
echo "Day {$m['day']}, month {$m['month']}, year {$m['year']}\n";
Output
Day 15, month 10, year 2026
The matches array contains each named group twice — by name and by number — so $m['year'] and $m[1] are the same value.
Backreferences
\1, \2, … refer to the text captured by a previous group. This finds repeated words:
<?php
preg_match_all('/\b(\w+)\s+\1\b/i', 'This is is a test test case', $m);
print_r($m[1]);
Output
Array
(
[0] => is
[1] => test
)
(\w+) captures a word, \s+ matches the space, and \1 requires the same word again.
Lookarounds
Lookarounds check what comes before or after a position without including it in the match:
| Syntax | Name | Meaning |
|---|---|---|
(?=...) | Positive lookahead | Followed by ... |
(?!...) | Negative lookahead | Not followed by ... |
(?<=...) | Positive lookbehind | Preceded by ... |
(?<!...) | Negative lookbehind | Not preceded by ... |
Example: Password Requirements With Lookaheads
<?php
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/';
foreach (['Secure2026', 'secure2026', 'Short1', 'NoDigitsHere'] as $password) {
echo $password . ': ' . (preg_match($pattern, $password) === 1 ? 'accepted' : 'rejected') . "\n";
}
Output
Secure2026: accepted
secure2026: rejected
Short1: rejected
NoDigitsHere: rejected
Each (?=.*X) checks "somewhere ahead there is an X" from the start of the string, without consuming characters. The final .{8,} enforces the minimum length. (Modern password guidance favors length and breach checks over composition rules; the pattern illustrates lookaheads.)
Example: Prices After a Currency Symbol
<?php
preg_match_all('/(?<=₹)\d+/u', 'Pen ₹20, Book ₹250, Bag 999', $m);
print_r($m[0]);
Output
Array
(
[0] => 20
[1] => 250
)
The lookbehind requires ₹ before the digits but does not include it in the match. 999 has no ₹ and is not matched. The u modifier is needed because ₹ is a multi-byte UTF-8 character.
Unicode Patterns
By default, \w and [a-z] cover only ASCII. With the u modifier, patterns work on UTF-8 characters, and Unicode properties become useful:
| Property | Matches |
|---|---|
\p{L} | Any letter in any script |
\p{Lu} / \p{Ll} | Uppercase / lowercase letter |
\p{N} | Any number |
\p{M} | Combining marks (for example, vowel signs in Indian scripts) |
\p{Tamil}, \p{Devanagari}, … | Characters of a specific script |
<?php
preg_match_all('/\w+/', 'Café São Paulo', $ascii);
preg_match_all('/\p{L}+/u', 'Café São Paulo', $unicode);
echo implode(' | ', $ascii[0]) . "\n";
echo implode(' | ', $unicode[0]) . "\n";
Output
Caf | S | o | Paulo
Café | São | Paulo
Without Unicode support, accented letters break words apart. For scripts such as Tamil or Hindi, which use combining vowel signs, match words with [\p{L}\p{M}]+ so that the signs stay attached to their letters.
Practical Example: Parsing a Log Line
<?php
$line = '2026-10-09 10:16:45 [ERROR] Payment failed for order #501';
$pattern = '/^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) \[(?<level>[A-Z]+)\] (?<message>.+)$/';
if (preg_match($pattern, $line, $m) === 1) {
$entry = array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY);
print_r($entry);
if (preg_match('/#(\d+)/', $entry['message'], $order) === 1) {
echo "Order ID: {$order[1]}\n";
}
}
Output
Array
(
[date] => 2026-10-09
[time] => 10:16:45
[level] => ERROR
[message] => Payment failed for order #501
)
Order ID: 501
Step-by-Step Explanation
- Named groups capture each part of the line: date, time, level, and message.
0and1match literal square brackets around the level..+captures the rest of the line as the message.array_filter(..., 'is_string', ARRAY_FILTER_USE_KEY)keeps only the named keys, removing the duplicate numeric ones.- A second, smaller pattern extracts the order number from the message.
Splitting the work into two simple patterns is easier to read and maintain than one complex pattern.
Quick Reference
| Category | Syntax |
|---|---|
| Any character | . |
| Character class | [abc], [a-z], [^abc] |
| Shorthands | \d \D \w \W \s \S |
| Quantifiers | * + ? {n} {n,} {n,m} — add ? for lazy |
| Anchors | ^ $ \A \z \b \B |
| Alternation | `a |
| Groups | (...), (?:...), (?<name>...) |
| Backreference | \1, \k<name> |
| Lookarounds | (?=...) (?!...) (?<=...) (?<!...) |
Unicode (with u) | \p{L}, \p{N}, \p{M}, \p{Tamil} |
Common Mistakes
Unescaped Dots
Effect: . matches any character, so /gocourse.in/ matches gocourseXin.
Fix: 0 for a literal dot.
Greedy Matching Too Much
Effect: .+ or .* runs to the last possible match.
Fix: Use lazy quantifiers or negated classes like [^>]+.
Missing Word Boundaries
Effect: Matches inside longer words.
Fix: Use \b around whole words.
ASCII-Only Classes for International Text
Effect: Names with accents or Indian-language text fail validation.
Fix: Use the u modifier with \p{L} and \p{M}.
Overly Complex Patterns
Effect: Hard-to-maintain code and performance problems.
Fix: Split the task into simpler patterns, use named groups, and document complex patterns (the x modifier allows comments).
Best Practices
- Escape metacharacters you mean literally.
- Anchor validation patterns with
^/\Aand\z. - Prefer specific classes and lazy or negated patterns over broad
.*. - Use named groups for readability.
- Enable the
umodifier whenever input may contain non-ASCII text.
Official reference: https\://www.php.net/manual/en/reference.pcre.pattern.syntax.php