Regular expressions7 min readExamples run on PHP 8.5.10

preg_match_all()

Learn how preg_match_all() finds every regex match in a string, how its ordering flags shape $matches, and which mistakes to avoid in PHP 8.4.

Signature

preg_match_all(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false

Purpose

preg_match_all() searches a string for every match of a regular expression, not only the first one. It returns how many full matches it found and fills a variable with the matched text. Use it when you want to collect all numbers, all key/value pairs, all tags or all words that fit a pattern. After each match, the next search continues from the end of the previous match, so matches do not overlap.

Signature

Parameters and defaults

  • $pattern: the regular expression as a string, including delimiters and any modifiers, such as '/\d+/'.
  • $subject: the string to search.
  • $matches: passed by reference. It is filled with a multi-dimensional array of results. It defaults to null, so you can omit it when you only need the count.
  • $flags: controls the layout of $matches. The default 0 behaves like PREG_PATTERN_ORDER. Other options are PREG_SET_ORDER, PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL. Combine compatible flags with |. Pattern order and set order do not make sense together.
  • $offset: the byte position where the search starts. The default is 0.

Return value and failure

The function returns the number of full pattern matches, which may be 0. It returns false on failure. If the pattern does not compile to a valid regular expression, PHP emits an E_WARNING. Always compare with false strictly when you need to tell "no matches" from "error", because 0 and false are both falsy.

Understanding the result layout

With the default pattern order, $matches[0] is a list of all full matches, $matches[1] is a list of everything captured by the first group, and so on. With PREG_SET_ORDER, each entry of $matches is one match, holding the full text and its groups. Named groups add string keys next to the numeric ones.

Example 1: all numbers in a string

Example
<?php
$count = preg_match_all('/\d+/', 'Items: 3, 14 and 159', $matches);
echo $count, "\n";
echo json_encode($matches, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
3
[["3","14","159"]]

Without a capture group, $matches holds one inner list: the full matches.

Example 2: set order with named groups

Example
<?php
$text = 'a=1; b=22; c=333';
$count = preg_match_all('/(?<key>\w+)=(?<value>\d+)/', $text, $matches, PREG_SET_ORDER);
$pairs = [];
foreach ($matches as $m) {
    $pairs[$m['key']] = (int) $m['value'];
}
echo $count, "\n";
echo json_encode($pairs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
3
{"a":1,"b":22,"c":333}

PREG_SET_ORDER is convenient for looping, because each iteration gives you one complete match with its named parts.

Example 3: offsets and a starting position

Example
<?php
$count = preg_match_all('/cat/', 'cat concat cat', $matches, PREG_OFFSET_CAPTURE, 1);
echo $count, "\n";
echo json_encode($matches, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
2
[[["cat",7],["cat",11]]]

The search begins at byte 1, so the first cat at position 0 is skipped. With PREG_OFFSET_CAPTURE, each match becomes a two-element array: the text and its byte offset in the original subject.

Edge cases

  • No matches: the return value is 0. In pattern order, $matches still has one empty list per group, so you can loop over it safely.
  • Byte offsets: both the $offset input and the offsets from PREG_OFFSET_CAPTURE count bytes, not characters. This matters for multibyte text.
  • Offset is not substr(): starting at $offset is not the same as searching substr($subject, $offset). Assertions such as ^ and lookbehinds like (?<=x) can still see the text before the offset.
  • Unmatched groups: a group that did not take part in a match is reported as an empty string, or as null if you pass PREG_UNMATCHED_AS_NULL.
  • Duplicate group names: with the (?J) modifier, only the rightmost group with that name is stored under the name key.

Common mistakes

  1. Expecting set order by default. The default groups results by pattern part, so $matches[0] is the list of all full matches, not the first match.
  2. Checking the result with a loose comparison. Use === false to detect errors, and treat 0 as a valid "nothing found".
  3. Forgetting to escape user input. If the pattern includes dynamic text, run it through preg_quote() first.
  4. Using it when one match is enough. preg_match() is simpler if you only need the first match.
  5. Assuming character offsets. Convert carefully if you work with non-ASCII text.
  6. Relying on the output array after a failure. Check the return value before using $matches.
  • preg_match() finds only the first match.
  • preg_replace() replaces matches instead of listing them.
  • preg_split() splits a string at matches.
  • preg_quote() escapes text for use inside a pattern.
  • preg_last_error() reports the code of the last PCRE error.

Recap

preg_match_all() counts and collects every non-overlapping match of a pattern. Choose PREG_PATTERN_ORDER for column-like lists or PREG_SET_ORDER for row-like matches. Add PREG_OFFSET_CAPTURE when you need byte positions. The function returns an integer count, or false with a warning when the pattern is invalid.

Tested examples

All numbers in a string
<?php
$count = preg_match_all('/\d+/', 'Items: 3, 14 and 159', $matches);
echo $count, "\n";
echo json_encode($matches, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
3
[["3","14","159"]]
Set order with named groups
<?php
$text = 'a=1; b=22; c=333';
$count = preg_match_all('/(?<key>\w+)=(?<value>\d+)/', $text, $matches, PREG_SET_ORDER);
$pairs = [];
foreach ($matches as $m) {
    $pairs[$m['key']] = (int) $m['value'];
}
echo $count, "\n";
echo json_encode($pairs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
3
{"a":1,"b":22,"c":333}
Offsets and a starting position
<?php
$count = preg_match_all('/cat/', 'cat concat cat', $matches, PREG_OFFSET_CAPTURE, 1);
echo $count, "\n";
echo json_encode($matches, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
2
[[["cat",7],["cat",11]]]