Chapter 9: Testing & Debugging

Goal: Build a practical workflow to test, understand, and fix patterns.


1) Start Simple with PHP

You don’t need any extra tools to test patterns. PHP’s built-in functions are more than enough for learning.

$pattern = '/\d{3}/';
$text = 'Order 123 and 456';

if (preg_match($pattern, $text, $matches)) {
    echo "Found: " . $matches[0]; // "123"
}

To find all matches:

preg_match_all('/\d+/', 'a1 b22 c333', $matches);
print_r($matches[0]); // ["1", "22", "333"]

2) A Practical Testing Workflow

When a pattern doesn’t behave the way you expect, go step by step:

Step 1: Write a small test

Keep it minimal. Test on the exact string you’re trying to match.

$pattern = '/^hello$/';
var_dump(preg_match($pattern, 'hello')); // int(1)
var_dump(preg_match($pattern, 'hello!')); // int(0)

Step 2: Check anchors and scope

If it’s matching too much or too little, check anchors.

// Matches anywhere
preg_match('/cat/', 'category'); // matches "cat"
echo preg_match('/cat/', 'category'), PHP_EOL; // 1

// Match whole word only
preg_match('/\bcat\b/', 'category'); // no match
echo preg_match('/\bcat\b/', 'category'), PHP_EOL; // 0

Step 3: Check quantifiers

If it’s greedy when you want lazy, or vice versa, check quantifiers.

$text = '<b>hi</b>';

// Too much
preg_match('/<.+>/', $text, $m);  // <b>hi</b>
echo $m[0], PHP_EOL;

// Just right
preg_match('/<.+?>/', $text, $m); // <b>
echo $m[0], PHP_EOL;

Step 4: Check groups and alternation

If | is behaving oddly, it’s usually a grouping issue (Chapter 5).

// Wrong scope
preg_match('/jpg|png\.gif/', 'a.png.gif', $m); // matches "png.gif" but for wrong reason
echo $m[0], PHP_EOL;
// Better
preg_match('/(?:jpg|png)\.gif/', 'a.png.gif', $m);
echo $m[0], PHP_EOL;

3) Common Debugging Tricks

Use var_dump on matches

preg_match('/(\w+)@(\w+)\.(\w+)/', '[email protected]', $matches);
var_dump($matches);
// Shows full match + each captured group

Test with preg_match_all to see all matches

preg_match_all('/\b\w{4}\b/', 'cat dog bird fish', $matches);
print_r($matches[0]); // ["bird", "fish"]

Isolate pieces

If a long pattern fails, break it down. Test each part separately, then combine.

// Test parts
preg_match('/^\w+$/', 'user');      // yes
echo preg_match('/^\w+$/', 'user'), PHP_EOL; // 1
preg_match('/^@/', '@example');     // yes
echo preg_match('/^@/', '@example'), PHP_EOL; // 1
preg_match('/^\w+\.\w+$/', 'com');  // no — this part expects a dot
echo preg_match('/^\w+\.\w+$/', 'com'), PHP_EOL; // 0

Watch for escaping

Remember Chapter 1: in PHP strings, backslashes need care. In single quotes less so, but better to be aware.

// In PHP string
$pattern = '/\d+/'; // \d is fine in single quotes
echo preg_match($pattern, 'Order 123'), PHP_EOL; // 1

Check case sensitivity

preg_match('/hello/', 'HELLO');   // no match
echo preg_match('/hello/', 'HELLO'), PHP_EOL; // 0
preg_match('/hello/i', 'HELLO'); // match
echo preg_match('/hello/i', 'HELLO'), PHP_EOL; // 1

4) Testing Checklist

Before using a pattern anywhere important, ask yourself:

  • Does it match what I want? Test positive cases
  • Does it reject what I don’t want? Test negative cases
  • Does it match the whole thing or just part? Consider anchors
  • Is it too greedy? Try lazy if needed
  • Is it safe? Check for nested quantifiers
  • Does it handle edge cases? Empty string, single char, special chars
  • Is it readable? If it’s too complex, can you simplify it?

5) Exercises

Exercise 1: Debug a broken pattern

This pattern is supposed to match dates like 2025-10-23, but something’s wrong. Fix it.

$pattern = '/\d{4}\d{2}\d{2}/'; // broken
echo preg_match($pattern, '2025-10-23'), PHP_EOL; // 0 — it never matches a real date
Solution

Missing the dashes:

$pattern = '/\d{4}-\d{2}-\d{2}/';
echo preg_match($pattern, '2025-10-23'), PHP_EOL; // 1

Exercise 2: Greedy vs lazy

Extract just title from <title>My Page</title>. Use a lazy match.

Solution
preg_match('/<title>(.*?)<\/title>/', '<title>My Page</title>', $matches);
echo $matches[1]; // "My Page"

Exercise 3: Whole word only

Fix this so it matches cat only as a whole word, not in category.

$pattern = '/cat/';
echo preg_match($pattern, 'category'), PHP_EOL; // 1 — matches too much
Solution
$pattern = '/\bcat\b/';
echo preg_match($pattern, 'category'), PHP_EOL; // 0

Exercise 4: Build a test

Write a small test script that checks if an email-like string is valid using anchors and a simple pattern.

Solution
$pattern = '/^[\w.-]+@[\w.-]+\.[a-z]{2,}$/i';

$tests = ['[email protected]', 'bad-email', '[email protected]'];
foreach ($tests as $t) {
    echo $t . ': ' . (preg_match($pattern, $t) ? 'OK' : 'NO') . PHP_EOL;
}

Key Takeaways

  • Test small — Start with a tiny example and grow
  • Use preg_match/preg_match_all with $matches to inspect results
  • Break it down — Isolate complex patterns into parts
  • Think about scope — Anchors, word boundaries, and grouping fix most issues
  • Test negative cases — Making sure it doesn’t match is just as important

Optional: Extra Tools

If you’d like to explain or visualize patterns while debugging, there are optional tools available. You can skip these entirely and still follow everything above.

vendor/bin/regex explain '/\d{4}-\d{2}-\d{2}/'
vendor/bin/regex diagram '/(\w+)\s+\1/'
use PHPRegex\Toolkit\Regex;

$regex = Regex::create();
echo $regex->explain('/^[a-z]+$/i');

Next: Chapter 10: Real-World Patterns in PHP

Edit on GitHub