Chapter 4: Quantifiers & Greediness

Goal: Say how many times something should match.


1) What are Quantifiers?

A quantifier applies to the element right before it. It tells the engine: “match this thing this many times”.

We’ve already seen them in passing, now we’ll look at them properly.

Quantifier Meaning Example
* Zero or more a* matches “”, “a”, “aa”, …
+ One or more a+ matches “a”, “aa”, … (at least one)
? Zero or one a? matches “” or “a” (optional)
{n} Exactly n a{3} matches “aaa”
{n,} At least n a{2,} matches “aa”, “aaa”, …
{n,m} Between n and m a{2,4} matches “aa”, “aaa”, “aaaa”

They work on a single character, a character class, or a group (we’ll see groups next chapter).


2) The Basics

* — zero or more

preg_match('/ab*/', 'a', $matches);    // matches "a" (zero 'b's)
echo $matches[0], PHP_EOL;
preg_match('/ab*/', 'ab', $matches);   // matches "ab"
echo $matches[0], PHP_EOL;
preg_match('/ab*/', 'abbb', $matches); // matches "abbb"
echo $matches[0], PHP_EOL;

+ — one or more

preg_match('/ab+/', 'a'); // no match (needs at least one 'b')
echo preg_match('/ab+/', 'a'), PHP_EOL; // 0
preg_match('/ab+/', 'ab', $matches);   // matches "ab"
echo $matches[0], PHP_EOL;
preg_match('/ab+/', 'abbb', $matches); // matches "abbb"
echo $matches[0], PHP_EOL;

? — zero or one (optional)

preg_match('/ab?/', 'a', $matches);   // matches "a"
echo $matches[0], PHP_EOL;
preg_match('/ab?/', 'ab', $matches);  // matches "ab"
echo $matches[0], PHP_EOL;
preg_match('/ab?/', 'abb', $matches); // matches "ab" (only takes one 'b')
echo $matches[0], PHP_EOL;

Combining with character classes

This is where quantifiers become really useful.

preg_match('/\d+/', 'Order 123', $matches);      // matches "123"
echo $matches[0], PHP_EOL;
preg_match('/[a-z]+/', 'Hello World', $matches); // matches "ello"
echo $matches[0], PHP_EOL;
preg_match('/\s*/', 'abc', $matches);            // matches "" (zero spaces)
echo '[' . $matches[0] . ']', PHP_EOL; // [] — an empty match

3) Exact Ranges with {}

When you need precision, use curly braces.

preg_match('/\d{3}/', '123', $matches);     // matches "123"
echo $matches[0], PHP_EOL;
preg_match('/\d{3}/', '12');                // no match
echo preg_match('/\d{3}/', '12'), PHP_EOL;       // 0
preg_match('/\d{3}/', '12345', $matches);   // matches "123" (first 3)
echo $matches[0], PHP_EOL;
preg_match('/\d{2,4}/', '12345', $matches); // matches "1234" (up to 4)
echo $matches[0], PHP_EOL;
preg_match('/\d{2,}/', '1');                // no match (needs at least 2)
echo preg_match('/\d{2,}/', '1'), PHP_EOL;       // 0
preg_match('/\d{2,}/', '123', $matches);    // matches "123"
echo $matches[0], PHP_EOL;

A common example: a 3-digit code: /^\d{3}$/ with anchors to match the whole string.


4) Greedy by Default

By default, quantifiers are greedy. That means: “take as much as possible while still allowing the rest of the pattern to match”.

$text = "<p>hello</p>";

preg_match('/<.+>/', $text, $matches);
echo $matches[0]; // Output: "<p>hello</p>"

The .+ could stop earlier, but greedy makes it go all the way to the last >.

How does it actually work?

The engine tries to match greedily first. If later parts of the pattern fail, it backtracks: it gives back one character at a time and tries again.

This is normal behaviour. Backtracking only becomes problematic in very specific cases with nested quantifiers — we’ll touch on that briefly at the end and go deeper in Chapter 8.


5) Lazy (Non-Greedy) Matching

To make a quantifier lazy, add ? after it: *?, +?, ??, {n,m}?

A lazy quantifier does the opposite: “take as little as possible”.

$text = "<p>hello</p>";

preg_match('/<.+?>/', $text, $matches);
echo $matches[0]; // Output: "<p>"

Greedy vs Lazy side by side

$text = "<div>test</div>";

preg_match('/<.+>/',  $text, $m1);  // greedy
preg_match('/<.+?>/', $text, $m2);  // lazy

echo $m1[0]; // <div>test</div>
echo $m2[0]; // <div>

Extracting content between tags

To get just the inside of the first tag, lazy is perfect:

$text = "<p>hello</p><p>world</p>";

preg_match('/<p>(.+?)<\/p>/', $text, $matches);
echo $matches[1]; // "hello"

If you want all matches, use preg_match_all().

$text = "<p>hello</p><p>world</p>";

preg_match_all('/<p>(.+?)<\/p>/', $text, $matches);
print_r($matches[1]); // ["hello", "world"]

6) A Note on Possessive Quantifiers

You might see ++, *+, ?+. These are possessive quantifiers: they never give back characters if something goes wrong. They can help in specific performance cases, but they’re not something you need to reach for on day one.

For now, just know they exist. We’ll cover when they matter in Chapter 8: Performance & ReDoS.


7) Exercises

Use only what you’ve learned so far (Chapters 1-3).

Exercise 1: Zero or more

Write a pattern that matches go, goo, gooo, etc. using *.

Solution
'/go*/'

Exercise 2: One or more

Write a pattern using \d+ that matches one or more digits.

Solution
'/\d+/'

Exercise 3: Optional letter

Write a pattern that matches color or colour (the u is optional).

Solution
'/colou?r/'

Exercise 4: Exact count

Write a pattern that matches exactly 4 digits.

Solution
'/\d{4}/'

Exercise 5: Greedy vs lazy

Given <b>bold</b>, write two patterns:

  1. A greedy one that matches the whole thing
  2. A lazy one that matches only <b>
Solution
// 1) Greedy
'/<.+>/' 

// 2) Lazy
'/<.+?>/'

Key Takeaways

  • Quantifiers control how many times to repeat: *, +, ?, {n,m}
  • * = 0 or more, + = 1 or more, ? = 0 or 1
  • Use {n}, {n,}, {n,m} for precise counts
  • Greedy (default) = match as much as possible
  • Lazy (? after quantifier) = match as little as possible
  • Mix with character classes and anchors for powerful patterns

Common Pitfalls

  • Forgetting + needs at least one — /\d+/ won’t match an empty string
  • Greedy going too far — use lazy +?/*? when you want the first match between delimiters
  • Not anchoring when validating — /\d+/ finds digits anywhere. If you want only digits, use /^\d+$/

Optional: Tools to Inspect Patterns

If you’d like to visualize greedy vs lazy behavior, these tools are optional:

vendor/bin/regex explain '/<.+?>/'
vendor/bin/regex diagram '/\d{2,4}/'
use PHPRegex\Toolkit\Regex;

$regex = Regex::create();
echo $regex->explain('/a+?/');

Next: Chapter 5: Groups & Alternation

Edit on GitHub