Patterns instead of fixed text
String functions such as str_contains() look for exact text. Often you need something looser: "six digits", "an order number like ORD-1234", "anything that looks like a date". A regular expression (regex) describes such a pattern. PHP uses the PCRE library, the same pattern syntax as JavaScript, Python and most editors, through functions whose names start with preg_.
A pattern is written as a string with delimiters, usually slashes, around it. Flags go after the closing delimiter:
<?php
var_dump(preg_match('/keyboard/', 'Mechanical keyboard'));
var_dump(preg_match('/keyboard/', 'Mechanical Keyboard'));
var_dump(preg_match('/keyboard/i', 'Mechanical Keyboard'));int(1)
int(0)
int(1)preg_match() returns 1 when the pattern matches, 0 when it does not (and false if the pattern itself is broken). The i flag makes the match case-insensitive. Always write patterns in single quotes, so PHP does not treat backslashes in them as its own escape sequences.
The building blocks
| Pattern | Matches |
|---|---|
abc |
the text "abc" |
. |
any one character (except a new line) |
\d / \D |
a digit / not a digit |
\w |
a letter, digit or underscore |
\s |
whitespace |
[aeiou] |
one of the listed characters |
[a-z0-9] |
one character in the ranges |
[^0-9] |
one character that is not listed |
^ and $ |
start and end of the string |
\b |
a word boundary |
\. |
a literal dot (escape special characters) |
Quantifiers say how many times the previous item repeats:
| Quantifier | Meaning |
|---|---|
? |
zero or one (optional) |
* |
zero or more |
+ |
one or more |
{6} |
exactly 6 |
{2,4} |
between 2 and 4 |
Validating a whole value
To check that a value is something, rather than contains it, anchor the pattern with ^ and $. An Indian PIN code is exactly six digits and does not start with 0:
<?php
function isPinCode(string $value): bool
{
return preg_match('/^[1-9][0-9]{5}$/', $value) === 1;
}
foreach (["110001", "011000", "1100011", "11000a"] as $pin) {
echo $pin . ": " . (isPinCode($pin) ? "valid" : "invalid") . "\n";
}110001: valid
011000: invalid
1100011: invalid
11000a: invalidWithout the anchors, "1100011" would match, because it contains six digits that fit. For email addresses and URLs, do not write your own pattern: PHP's filter_var() does it better, as shown in Validating and escaping form input.
Capturing parts of a match
Round brackets create a capture group. Pass a variable as the third argument and preg_match() fills it with the whole match at index 0 and each group after it:
<?php
$line = "Order ORD-2024-0042 placed on 2024-06-03";
if (preg_match('/ORD-(\d{4})-(\d+)/', $line, $matches)) {
print_r($matches);
echo "Year {$matches[1]}, number " . (int) $matches[2] . "\n";
}Array
(
[0] => ORD-2024-0042
[1] => 2024
[2] => 0042
)
Year 2024, number 42Named groups, (?<name>...), make the result easier to read:
<?php
$date = "2024-06-03";
if (preg_match('/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/', $date, $m)) {
echo "Day {$m['day']} of month {$m['month']}, {$m['year']}\n";
}Day 03 of month 06, 2024Finding every match
preg_match_all() finds all matches and returns how many there were:
<?php
$text = "Call 98100 12345 or 022-2345-6789. Prices: 89.50, 18.75 and 199.99.";
$count = preg_match_all('/\d+\.\d{2}/', $text, $matches);
echo "$count prices\n";
print_r($matches[0]);
preg_match_all('/#(\w+)/', "Loving #php and #mysql today #webdev", $tags);
echo implode(", ", $tags[1]) . "\n";3 prices
Array
(
[0] => 89.50
[1] => 18.75
[2] => 199.99
)
php, mysql, webdev$tags[1] holds the first capture group of every match, without the #.
Replacing by pattern
preg_replace() replaces every match. In the replacement, $1, $2 refer to the capture groups:
<?php
echo preg_replace('/\s+/', ' ', "Too many spaces\there") . "\n";
echo preg_replace('/\D/', '', "+91 98100-12345") . "\n";
echo preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', "Shipped 2024-06-03") . "\n";
echo preg_replace('/\b(\d{4})\d{8}(\d{4})\b/', '$1 **** **** $2', "Card 4111111111111111") . "\n";Too many spaces here
919810012345
Shipped 03/06/2024
Card 4111 **** **** 1111The second line keeps only digits by removing everything that is not a digit. For changes that need logic, preg_replace_callback() calls a function for each match.
Splitting by pattern
preg_split() is explode() with a pattern, useful when separators vary:
<?php
$input = "php, mysql;html css,,laravel";
print_r(preg_split('/[\s,;]+/', $input, -1, PREG_SPLIT_NO_EMPTY));Array
(
[0] => php
[1] => mysql
[2] => html
[3] => css
[4] => laravel
)UTF-8 text
Without a flag, patterns work on bytes, like strlen(). Add the u flag whenever the text may contain non-English characters, so that . and character classes match whole characters, and use \p{L} for "any letter in any alphabet":
<?php
$name = "Zoë Müller";
var_dump(preg_match('/^[a-z ]+$/i', $name));
var_dump(preg_match('/^[\p{L} ]+$/u', $name));
var_dump(preg_match('/^\p{L}+$/u', "नमस्ते"));int(0)
int(1)
int(0)The last one fails because Hindi vowel signs are not letters (\p{L}) but combining marks (\p{M}). /^[\p{L}\p{M}]+$/u accepts it. This is a good reason to keep name validation loose.
When not to use a regex
Regular expressions are powerful but hard to read, and a wrong one can be very slow. Use plain functions when they are enough: str_starts_with() instead of /^abc/, str_replace() for fixed text, ctype_digit() to check "only digits", filter_var() for emails, URLs and numbers. Comment any non-trivial pattern, or build it with the x flag, which allows spaces and comments inside it.
Common mistakes
- Forgetting ^ and $ when validating, so a value only has to contain the pattern.
- Writing patterns in double quotes, where PHP eats the backslashes first.
- Forgetting the u flag for UTF-8 text.
- Not escaping special characters such as
.,?,(and+when you mean them literally.preg_quote()escapes user text that you insert into a pattern.
Exercise
Extract every product code like SKU- followed by 3 letters and 4 digits from the text below, then print them in uppercase, one per line: "Restock sku-abc-1234 and SKU-KBD-0042, not SKU-12-34 or SKU-MUGS-1."
Show one possible answer
<?php
$text = "Restock sku-abc-1234 and SKU-KBD-0042, not SKU-12-34 or SKU-MUGS-1.";
preg_match_all('/\bSKU-[a-z]{3}-\d{4}\b/i', $text, $matches);
foreach ($matches[0] as $code) {
echo strtoupper($code) . "\n";
}SKU-ABC-1234
SKU-KBD-0042Key points
- A regular expression is a pattern written between delimiters, for example '/^[0-9]{6}$/'.
- preg_match tests once and can capture groups; preg_match_all finds every match.
- preg_replace replaces by pattern, using $1 for captured groups; preg_split splits on a pattern.
- Use the u flag for UTF-8 text, and prefer simple string functions when no pattern is needed.