String functions like str_contains and str_replace are perfect when you know the exact text you're looking for. But often you only know its shape: "a phone number", "a price with a pound sign", "a username of 3 to 15 letters and digits", "anything between square brackets". That's what regular expressions, or regex, are for.
A regular expression is a small pattern that describes text. PHP supports them through the preg_ functions, which use the same pattern style as JavaScript, Python and most other languages, so what you learn here works almost everywhere.
In this lesson you'll write patterns with delimiters, test and validate input with preg_match, pull out every match with preg_match_all, clean and reformat text with preg_replace, split on flexible separators with preg_split, and turn a blog title into a tidy web address.
Patterns and delimiters
In PHP, a pattern is a string that starts and ends with a delimiter, usually /, followed by optional flags:
$found = preg_match('/\d+/', 'Order 4521 shipped', $m);
echo $m[0]; // 4521'/\d+/'is the pattern\d+between two slashes. Use single quotes so PHP doesn't treat backslashes specially.preg_matchreturns 1 if it finds a match, 0 if not, and fills$mwith what matched.- Flags go after the closing delimiter:
iignores case,utreats the text as UTF-8,mmakes^and$work per line. - If your pattern contains
/, pick another delimiter, such as#:'#^https?://#'.
Building blocks
| Pattern | Matches |
|---|---|
\d / \w / \s | A digit / a letter, digit or underscore / whitespace |
. | Any character except a newline |
[abc] / [a-z] / [^0-9] | One of these / a range / anything except |
^ / $ | Start / end of the text |
* / + / ? | Zero or more / one or more / optional |
{3} / {2,5} | Exactly 3 / 2 to 5 |
( ) | A group that captures part of the match |
| | Either side: cat|dog |
To match a special character literally, escape it with a backslash: \. is a real full stop and \$ a real dollar sign.
Validating, extracting and replacing
- Validate: anchor the pattern with
^and$so the whole input must match:preg_match('/^[a-z0-9_]{3,15}$/', $username). - Extract:
preg_match_all($pattern, $text, $m)finds every match;$m[0]holds them all and$m[1]the first group of each. - Replace:
preg_replace($pattern, $replacement, $text)changes every match. Use$1,$2in the replacement to insert groups. - Split:
preg_split('/[\s,;]+/', $text)splits on any run of spaces, commas or semicolons.
Name a group with (?<name>...) and read it as $m['name'], which keeps longer patterns readable.
Groups in practice
preg_match('/(\d{2})\/(\d{2})\/(\d{4})/', 'Due 05/07/2024', $m);
// $m[0] = "05/07/2024", $m[1] = "05", $m[2] = "07", $m[3] = "2024"
echo preg_replace('/(\d{2})\/(\d{2})\/(\d{4})/', '$3-$2-$1', 'Due 05/07/2024');
// Due 2024-07-05Each pair of brackets captures part of the match, numbered from the left. In the replacement, $3-$2-$1 rearranges them, turning a UK date into the database format.
Greedy and lazy matching
Quantifiers such as + and * are greedy: they match as much text as possible while still allowing the whole pattern to succeed. That can catch more than you expect:
preg_match('/<.+>/', '<b>bold</b>', $m);
echo $m[0]; // <b>bold</b>
preg_match('/<.+?>/', '<b>bold</b>', $m);
echo $m[0]; // <b>Adding ? after a quantifier makes it lazy, so it matches as little as possible: .+? stops at the first >. A negated character class is often clearer still: <[^>]+> means "a <, then anything except >, then >". When a pattern matches too much, greediness is the first thing to check.
PHP Regex Syntax
preg functions
preg_match('/pattern/', $text, $m); // 1 or 0, first match in $m
preg_match_all('/pattern/', $text, $m); // count, all matches in $m
preg_replace('/pattern/', 'new $1', $text);
preg_split('/[\s,]+/', $text);
preg_quote($userText, '/'); // escape user text for a pattern
'/(?<year>\d{4})/' -> $m['year']PHP Regex Summary
- Patterns sit between delimiters, like '/pattern/i', with flags after the closing delimiter.
- preg_match finds the first match (returns 1 or 0); preg_match_all finds every match.
- Anchor validation patterns with ^ and $ so the whole input must match.
- preg_replace replaces matches, using $1, $2 for groups; preg_split splits on a pattern.
- Use the u flag for UTF-8 text and filter_var for emails and URLs.
PHP Regex Examples
Each example shows the code, the output it produces and what each part does.
Validating usernames and postcodes
<?php
$usernames = ['sam_lee', 'Al', 'codevra-fan', 'Mia2024'];
foreach ($usernames as $u) {
$ok = preg_match('/^[a-z][a-z0-9_]{2,14}$/i', $u);
echo str_pad($u, 12) . ($ok ? "valid" : "invalid") . "\n";
}
$postcodes = ['SW1A 1AA', 'm1 1ae', '12345'];
foreach ($postcodes as $p) {
$ok = preg_match('/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i', trim($p));
echo str_pad($p, 12) . ($ok ? "looks like a UK postcode" : "not a UK postcode") . "\n";
}sam_lee valid
Al invalid
codevra-fan invalid
Mia2024 valid
SW1A 1AA looks like a UK postcode
m1 1ae looks like a UK postcode
12345 not a UK postcodeLine-by-line explanation
- Line 4A letter, then 2 to 14 letters, digits or underscores; the i flag allows capitals.
- Line 4^ and $ make the whole username match, so "Al" (too short) and "codevra-fan" (has a dash) fail.
- Line 9An outward code of 1 or 2 letters and a digit, an optional space, then a digit and two letters.
Extracting prices and hashtags
<?php
$text = "Tea £2.50, cake £3.95 and a gift card for £20. #cafe #treats";
preg_match_all('/£(\d+(?:\.\d{2})?)/', $text, $m);
print_r($m[1]);
echo "Total: £" . number_format(array_sum($m[1]), 2) . "\n";
$count = preg_match_all('/#(\w+)/', $text, $tags);
echo "$count tags: " . implode(", ", $tags[1]) . "\n";Array
(
[0] => 2.50
[1] => 3.95
[2] => 20
)
Total: £26.45
2 tags: cafe, treatsLine-by-line explanation
- Line 4£, then digits, then optionally a dot and two digits. (?: ) groups without capturing.
- Line 5$m[1] holds just the numbers from the capturing group.
- Line 6The matched numbers are strings, but array_sum adds them as numbers.
- Line 8preg_match_all returns how many matches it found.
Cleaning text and making a slug
<?php
function slugify(string $title): string {
$slug = strtolower(trim($title));
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
return trim($slug, '-');
}
echo slugify(" 10 Tips for Learning PHP (Fast!) ") . "\n";
echo slugify("What's new in PHP 8.3?") . "\n";
$messy = "red, green;blue yellow";
print_r(preg_split('/[\s,;]+/', $messy));
echo preg_replace('/\s+/', ' ', "Too many spaces") . "\n";10-tips-for-learning-php-fast
what-s-new-in-php-8-3
Array
(
[0] => red
[1] => green
[2] => blue
[3] => yellow
)
Too many spacesLine-by-line explanation
- Line 4Any run of characters that aren't lowercase letters or digits becomes a single dash.
- Line 5trim with a second argument removes dashes from both ends.
- Line 12Split on any mix of spaces, commas and semicolons.
- Line 14Collapse runs of whitespace into one space.
Common PHP Regex Mistakes
Validating without anchors
preg_match('/\d{5}/', 'abc123456xyz'); // 1: matches part of itpreg_match('/^\d{5}$/', 'abc123456xyz'); // 0: the whole text must matchDouble quotes and backslashes
preg_match("/\bcat\b/", $text); // works by luck; "\b" can confuse readerspreg_match('/\bcat\b/', $text); // single quotes keep backslashes exactlyPutting user input straight into a pattern
preg_match('/' . $search . '/', $text); // "a.b" or "(" break itpreg_match('/' . preg_quote($search, '/') . '/', $text);preg_quote escapes every special character in the user's text.
PHP Regex in Real Projects
Regex in PHP projects
Content management systems create URL slugs from titles, form handlers check usernames, postcodes and phone numbers, log parsers pull dates and error codes out of text files, and import scripts clean messy data from spreadsheets. Routers in frameworks like Laravel and Symfony also turn route patterns such as /posts/{id} into regular expressions behind the scenes.
PHP Regex FAQ
What is the difference between preg_match and preg_match_all?
preg_match stops at the first match and returns 1 or 0. preg_match_all finds every match in the text and returns how many there were, with all the matches in the array you pass. Use the first to test or validate, and the second to extract lists such as every hashtag in a post.
How do I match accented letters and other alphabets?
Add the u flag so PHP reads the text as UTF-8, and use Unicode classes: \p{L} matches any letter in any language, so /^\p{L}+$/u accepts Zoë. Without the u flag, \w and character ranges work on bytes and treat accented letters as several separate characters.
How can I test and debug a regular expression?
Try it in an online tester such as regex101 with the PCRE flavour selected, which matches PHP's engine and explains each part of the pattern. In code, if a preg_ function returns false or null, call preg_last_error_msg() to see what went wrong, such as a malformed pattern or a backtracking limit.
Can a regular expression be too slow?
Most patterns are fast, but nested quantifiers such as (a+)+ can backtrack an enormous number of times on certain inputs. PHP stops at pcre.backtrack_limit and returns false. Keep patterns specific, avoid nested repetition, and limit the length of input before matching it.
PHP Regex Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the function that checks a pattern against text.
if (('/^\d{4}$/', $pin)) {
echo "Valid PIN";
}
What does this print?
echo preg_replace('/\d/', '*', 'Card 4821');
What does this print?
echo preg_match_all('/a/i', 'Banana Apple');
Which pattern checks that the whole input is 3 to 10 letters?
In preg_replace('/(\w+) (\w+)/', '$2 $1', 'Ada Lovelace'), what's the result?
PHP Regex Quiz
5 questions to check what you learned.
What does the i flag do in
/php/i?- Ignores upper and lower case
- Inverts the match
- Matches integers only
- Includes newlines
What does preg_match return when there's no match?
- false
- 0
- null
- An empty string
Which function splits text on a pattern?
- explode
- str_split
- preg_match_all
- preg_split
How should you validate an email address in PHP?
- A long custom regex
- str_contains($email, '@')
- filter_var($email, FILTER_VALIDATE_EMAIL)
- strlen($email) > 5
Why use preg_quote on user input inside a pattern?
- To make it lowercase
- To escape special characters so they're matched literally
- To speed up matching
- To remove spaces