preg_match
What Is preg_match() ? In simple terms: preg_match() checks whether a piece of text matches a regular expression. It can also give you the matched text and the parts captured by groups — for example,
What Is preg_match()?
In simple terms: preg_match() checks whether a piece of text matches a regular expression. It can also give you the matched text and the parts captured by groups — for example, the year, month, and day from a date.
Technically: preg_match() searches the subject string for the first match of the pattern. It returns 1 if a match is found, 0 if not, and false if an error occurs. When a third argument is passed, it is filled with the match and its captured groups. Its companion, preg_match_all(), finds all matches.
Syntax
preg_match(
string $pattern,
string $subject,
array &$matches = null,
int $flags = 0,
int $offset = 0
): int|false
| Parameter | Meaning |
|---|---|
$pattern | The regular expression, with delimiters |
$subject | The text to search |
$matches | Filled with the results (passed by reference) |
$flags | PREG_OFFSET_CAPTURE and/or PREG_UNMATCHED_AS_NULL |
$offset | Byte position where the search starts |
| Return Value | Meaning |
|---|---|
1 | A match was found |
0 | No match |
false | An error occurred (invalid pattern, invalid UTF-8 with u, backtracking limit) |
Testing for a Match
Example
<?php
function isValidUsername(string $username): bool
{
return preg_match('/^[a-z][a-z0-9_]{2,19}$/', $username) === 1;
}
foreach (['asha_dev', 'A', '9lives', 'bala.k', 'charu2026'] as $name) {
echo str_pad($name, 10) . (isValidUsername($name) ? 'valid' : 'invalid') . "\n";
}
Output
asha_dev valid
A invalid
9lives invalid
bala.k invalid
charu2026 valid
How It Works
The pattern requires: a lowercase letter first, then 2–19 lowercase letters, digits, or underscores — 3 to 20 characters in total.
Ais uppercase and too short.9livesstarts with a digit.bala.kcontains a dot, which is not allowed.
The comparison === 1 turns the result into a clean boolean and treats false (an error) as "not valid" instead of letting it pass unnoticed.
Capturing Matches
<?php
preg_match('/(\d+)\s*(kg|g)\b/', 'Package weight: 25 kg', $m);
print_r($m);
Output
Array
(
[0] => 25 kg
[1] => 25
[2] => kg
)
| Index | Contains |
|---|---|
0 | The full text matched by the pattern |
1 | The first capturing group: the number |
2 | The second capturing group: the unit |
If there is no match, $matches becomes an empty array.
Named Groups
<?php
$gstin = '33ABCDE1234F1Z5';
$pattern = '/^(?<state>\d{2})(?<pan>[A-Z]{5}\d{4}[A-Z])(?<entity>[1-9A-Z])Z(?<check>[0-9A-Z])$/';
if (preg_match($pattern, $gstin, $m) === 1) {
echo "State code: {$m['state']}\n";
echo "PAN: {$m['pan']}\n";
echo "Entity number: {$m['entity']}\n";
}
var_dump(preg_match($pattern, '33ABCDE1234F1X5'));
Output
State code: 33
PAN: ABCDE1234F
Entity number: 1
int(0)
An Indian GSTIN consists of a 2-digit state code (33 is Tamil Nadu), the business's 10-character PAN, an entity number, the letter Z, and a check character. The pattern checks the format and extracts the parts by name. (It does not verify the check character's checksum or that the number is actually registered — that requires additional logic or an official API.)
Flags
PREG_OFFSET_CAPTURE — Where Was It Found?
<?php
preg_match('/world/', 'Hello world', $m, PREG_OFFSET_CAPTURE);
print_r($m);
Output
Array
(
[0] => Array
(
[0] => world
[1] => 6
)
)
Each entry becomes a pair: the matched text and its byte offset in the subject. For UTF-8 text, convert byte offsets to character positions with mb_strlen(substr($subject, 0, $offset)) if needed.
PREG_UNMATCHED_AS_NULL — Consistent Results for Optional Groups
<?php
$pattern = '/(\d+)(?:\.(\d+))?/';
preg_match($pattern, 'Version 8', $plain);
preg_match($pattern, 'Version 8', $withNulls, PREG_UNMATCHED_AS_NULL);
var_dump(count($plain));
var_dump($withNulls);
Output
int(2)
array(3) {
[0]=>
string(1) "8"
[1]=>
string(1) "8"
[2]=>
NULL
}
The optional minor-version group did not participate in the match. Without the flag, trailing unmatched groups are simply missing from the array (so $plain[2] would cause a warning), and unmatched groups in the middle become empty strings. With PREG_UNMATCHED_AS_NULL, every group is present, and unmatched ones are null — easy to test with ?? or === null.
The $offset Parameter
<?php
preg_match('/\d+/', 'a1b22c333', $m, 0, 3);
echo $m[0] . "\n";
Output
22
The search starts at byte 3, so the 1 at position 1 is skipped. Note that ^ still refers to the start of the whole string, not the offset; use \G to anchor at the offset.
preg_match_all() — Finding All Matches
preg_match_all(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false
It returns the number of matches (or false on error).
Default Order: PREG_PATTERN_ORDER
<?php
$text = 'Pen ₹20, Book ₹249.50, Bag ₹1299';
$count = preg_match_all('/₹(\d+(?:\.\d{2})?)/u', $text, $m);
echo "$count prices\n";
print_r($m[1]);
echo "Total: ₹" . array_sum($m[1]) . "\n";
Output
3 prices
Array
(
[0] => 20
[1] => 249.50
[2] => 1299
)
Total: ₹1568.5
With the default order, $m[0] holds all full matches and $m[1] holds all values of group 1 — convenient when you need one list per group. (The total is shown without formatting; use number_format() for display.)
PREG_SET_ORDER — One Array per Match
<?php
$header = 'name=Asha; city=Chennai; role=admin';
preg_match_all('/(\w+)=(\w+)/', $header, $matches, PREG_SET_ORDER);
foreach ($matches as [$full, $key, $value]) {
echo "$key: $value\n";
}
Output
name: Asha
city: Chennai
role: admin
With PREG_SET_ORDER, each element contains one complete match with its groups — convenient for processing matches one by one, and ideal for destructuring.
| Flag | Structure | Best For |
|---|---|---|
PREG_PATTERN_ORDER (default) | $m[group][matchIndex] | Collecting all values of a group |
PREG_SET_ORDER | $m[matchIndex][group] | Processing each match as a unit |
preg_match() vs Other Tools
| Task | Recommended |
|---|---|
| Does the text contain a fixed substring? | str_contains() |
| Does the text start/end with fixed text? | str_starts_with() / str_ends_with() |
| Is this a valid email, URL, IP, or integer? | filter_var() |
| Does the text follow a custom format (codes, IDs, plates)? | preg_match() |
| Extract parts of a formatted string | preg_match() with groups |
| Find all occurrences of a pattern | preg_match_all() |
Common Mistakes
Using the Result Loosely
if (preg_match($pattern, $input)) { ... }
Effect: Errors (false) and "no match" (0) are treated the same, so a broken pattern silently fails.
Fix: Check === 1, and handle false explicitly where errors are possible.
Expecting All Matches From preg_match()
Effect: Only the first match is returned.
Fix: Use preg_match_all().
Reading Groups That May Be Missing
Effect: "Undefined array key" warnings for optional groups.
Fix: Use PREG_UNMATCHED_AS_NULL and ??.
Validation Without Anchors
Effect: /[a-z]+/ matches "abc!!!" because it finds abc inside.
Fix: Use ^...$ with D, or \A...\z.
Treating Offsets as Character Positions
Effect: Wrong positions for multi-byte text.
Fix: Remember offsets are in bytes; convert with mb_strlen() when needed.
Best Practices
- Compare the result with
=== 1and treatfalseas an error. - Use named groups for readable extraction.
- Use
PREG_UNMATCHED_AS_NULLwhen patterns contain optional groups. - Choose
PREG_SET_ORDERwhen processing each match individually. - Prefer string and filter functions for fixed text and standard formats.
Official reference: https\://www.php.net/manual/en/function.preg-match.php