Lesson 91 of 100
Regular expressions15 min5 checksPHP 8.5

Extract Values with Regex Captures

Use preg_match() with named capture groups to pull structured values out of text, and handle non-matches and optional groups explicitly.

01 · THE IDEAREAD → TRY → LEARN

Why captures?

A plain yes/no regex test tells you that text has a certain shape. Most real tasks need more: you want the year from a sentence, the host from a URL, or the numbers from a version string. Parentheses in a pattern create capture groups, and preg_match() copies whatever each group matched into an array you supply as the third argument.

The signature is preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false. It returns 1 for a match, 0 for no match, and false if matching failed, for example when the pattern is invalid or a backtracking limit is hit.

Numbered and named groups

Without names, $matches[0] holds the whole match, $matches[1] the first group, $matches[2] the second, and so on. Numbers are fragile: add a group at the start of the pattern and every index shifts. A named group such as (?<year>\d{4}) is written once and read as $matches['year']. PHP still fills in the numeric index too, so a named group appears under both keys.

Example
<?php
$text = 'order: 2008';
if (preg_match('/(?<label>[a-z]+): (?<number>\d+)/', $text, $m) === 1) {
    echo json_encode($m, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
Output
{"0":"order: 2008","label":"order","1":"order","number":"2008","2":"2008"}

Notice the order: the full match comes first, then each named group directly followed by its numeric twin.

Handle the non-match explicitly

When the pattern does not match, $matches is an empty array. Reading $m['year'] from it raises a warning about an undefined key and gives you null by accident. Check the return value first, and use the strict comparison === 1, because false and 0 are both falsy but mean different things. Then decide what your function reports for the failure case. A nullable return type such as ?int is a clear, honest choice.

Example
<?php
function read_year(string $text): ?int {
    $result = preg_match('/\b(?<year>\d{4})\b/', $text, $m);
    if ($result !== 1) {
        return null;
    }
    return (int) $m['year'];
}
echo json_encode([read_year('Released in 2024'), read_year('No year here')]), "\n";
Output
[2024,null]

Captured values are always strings, so cast them with (int) when you need numbers.

Optional groups and PREG_UNMATCHED_AS_NULL

A group followed by ? may take no part in a match. By default PHP handles that inconsistently: an unmatched group in the middle becomes an empty string, while unmatched trailing groups are left out of the array entirely. That means $m['patch'] might not exist at all. The PREG_UNMATCHED_AS_NULL flag makes the result uniform: every group is present, and unmatched ones are null. This also lets you tell an unmatched group apart from one that matched an empty string.

Example
<?php
preg_match('/(?<a>a)(?<b>b)?(?<c>c)?/', 'a', $plain);
preg_match('/(?<a>a)(?<b>b)?(?<c>c)?/', 'a', $nullable, PREG_UNMATCHED_AS_NULL);
echo json_encode([count($plain), array_key_exists('b', $plain), count($nullable), array_key_exists('b', $nullable), $nullable['b']]), "\n";
Output
[3,false,7,true,null]

The plain result has three entries and no b key, while the flagged result has seven entries and b is null.

Anchors matter

A pattern without ^ and $ can match anywhere inside the subject. If you want to validate a whole string, anchor both ends. Remember that $ also matches just before a final newline; the D modifier makes it match only at the very end.

Exercise contract

Write parse_version(string $text): ?array. It accepts a version string with an optional leading v, a major and minor number, and an optional patch number: v8.4.25, 8.4, 0.0.0.

  • On success, return an array with the integer keys major and minor, and patch as an integer, or null when the patch part is absent.
  • If the whole string does not have that shape, return null. Extra text before or after the version means no match, so version 8.4.25 returns null.
  • Use named groups, check preg_match() strictly against 1, and cast the captured strings to integers.

The tests compare the returned values, not printed output, and they include a missing patch, all zeros, surrounding text and an empty string.

Recap

  • Groups in parentheses are captured into $matches; name them with (?<name>...).
  • Check preg_match(...) === 1 before touching $matches.
  • Captures are strings; cast them when you need other types.
  • Use PREG_UNMATCHED_AS_NULL so optional groups always exist.
  • Anchor patterns when validating a whole string.
Before you start

Functions · Arrays · Types

You’ll practice
  • Use named captures and handle non-matches explicitly
  • Implement and test a deterministic PHP 8.4 function
Your task

Make parse_version() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.