PHP Regular Expressions

Search, validate, extract and replace text by pattern with PHP's preg functions: delimiters, character classes, quantifiers, groups and flags.

Lesson 14 of 86Intermediate17 min read

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:

PHP
$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_match returns 1 if it finds a match, 0 if not, and fills $m with what matched.
  • Flags go after the closing delimiter: i ignores case, u treats the text as UTF-8, m makes ^ and $ work per line.
  • If your pattern contains /, pick another delimiter, such as #: '#^https?://#'.

Building blocks

PatternMatches
\d / \w / \sA 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, $2 in 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

PHP
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-05

Each 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:

PHP
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.

Example 1

Validating usernames and postcodes

PHP
<?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";
}
Output
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 postcode
Line-by-line explanation
  1. Line 4A letter, then 2 to 14 letters, digits or underscores; the i flag allows capitals.
  2. Line 4^ and $ make the whole username match, so "Al" (too short) and "codevra-fan" (has a dash) fail.
  3. Line 9An outward code of 1 or 2 letters and a digit, an optional space, then a digit and two letters.
Example 2

Extracting prices and hashtags

PHP
<?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";
Output
Array
(
    [0] => 2.50
    [1] => 3.95
    [2] => 20
)
Total: £26.45
2 tags: cafe, treats
Line-by-line explanation
  1. Line 4£, then digits, then optionally a dot and two digits. (?: ) groups without capturing.
  2. Line 5$m[1] holds just the numbers from the capturing group.
  3. Line 6The matched numbers are strings, but array_sum adds them as numbers.
  4. Line 8preg_match_all returns how many matches it found.
Example 3

Cleaning text and making a slug

PHP
<?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";
Output
10-tips-for-learning-php-fast
what-s-new-in-php-8-3
Array
(
    [0] => red
    [1] => green
    [2] => blue
    [3] => yellow
)
Too many spaces
Line-by-line explanation
  1. Line 4Any run of characters that aren't lowercase letters or digits becomes a single dash.
  2. Line 5trim with a second argument removes dashes from both ends.
  3. Line 12Split on any mix of spaces, commas and semicolons.
  4. Line 14Collapse runs of whitespace into one space.

Common PHP Regex Mistakes

Validating without anchors

Don't
preg_match('/\d{5}/', 'abc123456xyz');      // 1: matches part of it
Do
preg_match('/^\d{5}$/', 'abc123456xyz');    // 0: the whole text must match

Double quotes and backslashes

Don't
preg_match("/\bcat\b/", $text);    // works by luck; "\b" can confuse readers
Do
preg_match('/\bcat\b/', $text);    // single quotes keep backslashes exactly

Putting user input straight into a pattern

Don't
preg_match('/' . $search . '/', $text);           // "a.b" or "(" break it
Do
preg_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.

Fill in the blankseasy

Complete the function that checks a pattern against text.

if (('/^\d{4}$/', $pin)) {
    echo "Valid PIN";
}
Predict the outputeasy

What does this print?

echo preg_replace('/\d/', '*', 'Card 4821');
Predict the outputmedium

What does this print?

echo preg_match_all('/a/i', 'Banana Apple');
Multiple choicemedium

Which pattern checks that the whole input is 3 to 10 letters?

Multiple choicehard

In preg_replace('/(\w+) (\w+)/', '$2 $1', 'Ada Lovelace'), what's the result?

PHP Regex Quiz

5 questions to check what you learned.

  1. What does the i flag do in /php/i?

    • Ignores upper and lower case
    • Inverts the match
    • Matches integers only
    • Includes newlines
  2. What does preg_match return when there's no match?

    • false
    • 0
    • null
    • An empty string
  3. Which function splits text on a pattern?

    • explode
    • str_split
    • preg_match_all
    • preg_split
  4. 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
  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