Regular Expressions (preg)

Reviewed & published by Brayan K

A regular expression is a tiny pattern language for finding, extracting, and replacing text. PHP exposes it through the preg_* family. Learn the core syntax and four key functions, and validating an email or pulling every link from a page becomes a one-liner.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Testing with preg_match

preg_match($pattern, $subject, $matches) returns 1 if the pattern is found, 0 if not. The pattern is a string wrapped in delimiters (usually /.../). The building blocks: \d a digit, \w a word character, \s whitespace, + one-or-more, ^ start, $ end, \b word boundary. A trailing i makes it case-insensitive.

<?php
// preg_match tests whether a pattern is found in a string.
// Returns 1 (match), 0 (no match), or false (error).
// The pattern is a string wrapped in DELIMITERS — usually slashes /.../.

$text = 'Order #4821 confirmed';

if (preg_match('/\\d+/', $text, $m)) {
    // \d  = a digit,  +  = one or more.
    // $m[0] holds the whole matched text.
    echo "Found: " . $m[0] . "\n";       // Found: 4821
}

// \b = word boundary; the i flag at the end makes it case-insensitive.
var_dump(preg_match('/\\bphp\\b/i', 'I love PHP'));   // int(1)
var_dump(preg_match('/^Order/', $text));               // int(1)  ^ = start
var_dump(preg_match('/confirmed$/', $text));           // int(1)  $ = end
?>

2️⃣ Capture Groups

Wrap part of a pattern in parentheses ( ... ) to capture it — pull that piece out of the match. Captures land in the matches array: index 0 is the whole match, 1, 2, … are the groups left-to-right. Even better, name a group with (?<name>...) and read it as $m['name'] — far clearer than counting parentheses.

<?php
// CAPTURE GROUPS ( ... ) pull pieces OUT of a match.
// They appear in $m at index 1, 2, 3 ... (index 0 is the whole match).

$date = '2026-06-18';

preg_match('/(\\d{4})-(\\d{2})-(\\d{2})/', $date, $m);

echo "Whole: " . $m[0] . "\n";   // 2026-06-18
echo "Year:  " . $m[1] . "\n";   // 2026
echo "Month: " . $m[2] . "\n";   // 06
echo "Day:   " . $m[3] . "\n";   // 18

// NAMED groups (?<name>...) are clearer for real code.
preg_match('/(?<user>\\w+)@(?<host>\\w+\\.\\w+)/', 'ada@example.com', $e);
echo $e['user'] . " at " . $e['host'] . "\n";   // ada at example.com
?>

3️⃣ Finding Every Match

preg_match_all doesn't stop at the first hit — it collects them all. Its results array is organised by group, so $m[1] is an array of every first-group capture. Note .*? below: the ? makes it lazy, matching as little as possible so each <a> tag is captured separately rather than one giant greedy span.

<?php
// preg_match_all finds EVERY match, not just the first.
$html = '<a>Home</a> <a>About</a> <a>Contact</a>';

// .*? is a LAZY match: grab as little as possible between the tags.
preg_match_all('/<a>(.*?)<\\/a>/', $html, $m);

// $m[0] = all full matches, $m[1] = all first-group captures.
echo "Count: " . count($m[1]) . "\n";       // Count: 3
echo implode(', ', $m[1]) . "\n";           // Home, About, Contact
?>

4️⃣ Replacing & Splitting

preg_replace($pattern, $replacement, $subject) swaps every match. In the replacement string, $1, $2 … are back-references to your capture groups — perfect for reformatting. And preg_split breaks a string wherever the pattern matches, ideal for splitting on "any run of spaces or commas".

<?php
// preg_replace swaps every match for a replacement string.
$msg = 'Call 555-1234 or 555-9876 today';

// Replace each phone number with [redacted].
echo preg_replace('/\\d{3}-\\d{4}/', '[redacted]', $msg) . "\n";

// Use $1 in the replacement to reference a capture group (back-reference).
$name = 'Smith, John';
echo preg_replace('/(\\w+), (\\w+)/', '$2 $1', $name) . "\n";   // John Smith

// preg_split breaks a string on a pattern (here: any run of whitespace/commas).
$tags = preg_split('/[\\s,]+/', 'php, regex   patterns');
echo implode('|', $tags) . "\n";            // php|regex|patterns
?>

Now you try — fill in each ___ using the 👉 hint, then run it and check against the Output panel.

<?php
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.

// 1) Match a 5-digit US ZIP code. \d is a digit; {5} means exactly five.
$ok = preg_match('/^\\d___$/', '90210');   // 👉 the quantifier for "exactly 5"
echo $ok . "\n";                           // should print: 1

// 2) Capture the area code from a phone number.
preg_match('/\\((\\d{3})\\)/', '(415) 555-0000', $m);
echo $m[___] . "\n";                       // 👉 the group index for the captured digits
                                            // should print: 415

// ✅ Expected output:
//    1
//    415
?>

🧩 Reorder Challenge

These lines extract and print the year from a date — but they're scrambled. Put them in the order that prints 2026.

B → D → C → A

Set the subject string (B), optionally initialise $m (D), run preg_match which fills $m (C), then echo the first capture group (A). You can't read $m[1] before the match populates it.

🧠 Quick Recall — Predict the Output

0 — the anchors ^ and $ require the entire string to be digits, but '12a' ends in a letter, so there's no match.

2. What does this output?

r*g*x — every vowel in the character class [aeiou] is replaced with an asterisk.

3. How many elements are in $m[0]?

3 — there are three separate runs of digits (1, 22, 333), so preg_match_all finds three matches.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — preg & Syntax

Token / FunctionExampleMeaning
\d \w \s\d+Digit / word char / whitespace
^ $ \b^abc$Start / end / word boundary
+ * ? {n}\d{3}Quantifiers (count)
( ) (?<name>)(?<y>\d{4})Capture group
preg_matchpreg_match($p, $s, $m)First match (0/1)
preg_match_allpreg_match_all($p, $s, $m)Every match
preg_replacepreg_replace($p, $r, $s)Find & replace
preg_splitpreg_split($p, $s)Split into array

Mini-Challenge: Slug Maker

No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments.

<?php
// 🎯 MINI-CHALLENGE: a simple slug maker.
// No code is filled in — work from the steps below, then run it.
//
// 1. Start with $title = 'Hello, World! PHP 8 Rocks'.
// 2. strtolower() it.
// 3. Use preg_replace to turn any run of NON-alphanumeric characters
//    into a single dash:   pattern /[^a-z0-9]+/   replacement '-'
// 4. Use preg_replace again (or trim) to strip a leading/trailing dash:
//    pattern /^-|-$/  replacement ''
// 5. echo the result.
//
// ✅ Expected output:
//    hello-world-php-8-rocks

// your code here
?>

🎉 Lesson Complete!

Practice quiz

What does echo preg_match('/^\d+$/', '12a'); print?

  • 1
  • false
  • 0
  • 3

Answer: 0. The ^ and $ anchors require the whole string to be digits, but '12a' ends in a letter, so there is no match (0).

What does echo preg_replace('/[aeiou]/', '*', 'regex'); print?

  • r*g*x
  • *****
  • regex
  • rgx

Answer: r*g*x. Every vowel in the character class [aeiou] is replaced with an asterisk: r*g*x.

After preg_match_all('/\d+/', 'a1b22c333', $m); what is count($m[0])?

  • 6
  • 1
  • 0
  • 3

Answer: 3. There are three separate runs of digits (1, 22, 333), so preg_match_all finds three matches.

In a capture-group match, what does index 0 of the matches array hold?

  • The first capture group
  • The whole matched text
  • The last capture group
  • Always null

Answer: The whole matched text. $m[0] is the entire match; $m[1], $m[2], ... are the capture groups left to right.

What does the i flag at the end of a pattern like /php/i do?

  • Makes matching case-insensitive
  • Treats the pattern as Unicode
  • Makes . match newlines
  • Anchors to the line start

Answer: Makes matching case-insensitive. The i modifier makes the pattern case-insensitive, so /php/i matches 'PHP'.

What makes .*? different from .* in a pattern?

  • The ? makes it match more
  • The ? makes it case-insensitive
  • The ? makes it lazy — matches as little as possible
  • It is a syntax error

Answer: The ? makes it lazy — matches as little as possible. Appending ? makes a quantifier lazy, grabbing as little as possible instead of being greedy.

What does preg_replace('/(\w+), (\w+)/', '$2 $1', 'Smith, John') return?

  • Smith John
  • John Smith
  • Smith, John
  • $2 $1

Answer: John Smith. $1 and $2 are back-references to the capture groups, so the names are swapped to 'John Smith'.

Which function should you use to split a string on a pattern?

  • preg_match
  • preg_replace
  • preg_match_all
  • preg_split

Answer: preg_split. preg_split breaks a string wherever the pattern matches and returns the pieces as an array.

Why prefer a named group like (?<year>\d{4}) over a numbered one?

  • It matches faster
  • It is self-documenting and survives reordering
  • It allows more characters
  • Numbered groups are not supported in PHP

Answer: It is self-documenting and survives reordering. Named groups read clearly ($m['year']) and don't break when you insert or reorder groups.

When should you NOT use a regular expression?

  • To validate a fixed pattern shape
  • To extract a list of matches
  • To parse structured formats like HTML, XML, or JSON
  • To find and replace with variation

Answer: To parse structured formats like HTML, XML, or JSON. Use a real parser (DOMDocument, json_decode) for structured formats; nesting and edge cases defeat regex.

Continue this course

Frequently asked questions

What are the slashes in a PHP regex like '/\d+/' for?

Those are delimiters — they mark where the pattern begins and ends, and everything after the closing delimiter is for flags. Slashes are conventional, but any non-alphanumeric character works, which is handy when your pattern itself contains slashes: matching a URL is cleaner with '#https?://#' than with escaped slashes. The trailing letters (like the i in '/php/i') are pattern modifiers: i for case-insensitive, m for multiline, s for dot-matches-newline, u for UTF-8.

What's the difference between preg_match and preg_match_all?

preg_match stops at the first match and fills the matches array with that single result (full match in [0], capture groups in [1], [2], ...). preg_match_all keeps going and finds every match in the string; its matches array is organised by group, so $m[0] is an array of all full matches and $m[1] is an array of every first-group capture. Use preg_match to test or grab one thing; use preg_match_all to extract a list, like every link in some HTML.

Why should I use named capture groups?

Numbered groups ($m[1], $m[2]) break the moment you insert or reorder a group, and they're hard to read. A named group (?<year>\d{4}) lets you write $m['year'], which is self-documenting and survives refactoring. The pattern is slightly longer, but for anything beyond a throwaway script the clarity is well worth it — future-you will know exactly what each captured piece means.

When should I NOT use a regular expression?

Don't parse structured formats like HTML, XML, or JSON with regex — use a real parser (DOMDocument, json_decode), because nesting and edge cases will defeat any pattern you write. For simple fixed-string work, plain functions are faster and clearer: str_contains, str_replace, explode, and str_starts_with handle 'does it contain', 'swap this text', and 'split on a comma' without a pattern at all. Reach for preg_* when you genuinely need flexible pattern matching — validation, extraction, or find-and-replace with variation.

Related lessons