Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 30 of 54

Regular expressions with preg_ functions

Match, extract, replace and split text with regular expressions in PHP using preg_match, preg_match_all, preg_replace and preg_split, with practical patterns.

14 minStrings, dates and patternsUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonWrite simple regular expressions and use the preg_ functions to validate, extract and replace text.

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
<?php
var_dump(preg_match('/keyboard/', 'Mechanical keyboard'));
var_dump(preg_match('/keyboard/', 'Mechanical Keyboard'));
var_dump(preg_match('/keyboard/i', 'Mechanical Keyboard'));
result
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
<?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";
}
result
110001: valid
011000: invalid
1100011: invalid
11000a: invalid

Without 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
<?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";
}
result
Array
(
    [0] => ORD-2024-0042
    [1] => 2024
    [2] => 0042
)
Year 2024, number 42

Named groups, (?<name>...), make the result easier to read:

php
<?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";
}
result
Day 03 of month 06, 2024

Finding every match

preg_match_all() finds all matches and returns how many there were:

php
<?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";
result
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
<?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";
result
Too many spaces here
919810012345
Shipped 03/06/2024
Card 4111 **** **** 1111

The 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
<?php
$input = "php, mysql;html  css,,laravel";
print_r(preg_split('/[\s,;]+/', $input, -1, PREG_SPLIT_NO_EMPTY));
result
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
<?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', "नमस्ते"));
result
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
<?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";
}
result
SKU-ABC-1234
SKU-KBD-0042

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

Check yourself

1. What does ^ mean at the start of a pattern such as /^abc/?
2. What does \d+ match?
3. What does preg_match return when the pattern matches?
4. Which flag makes a pattern treat the subject as UTF-8?