preg_match() / preg_replace()
debt(d7/e3/b3/t7)
Closest to 'only careful code review or runtime testing' (d7) — phpstan/psalm can flag some loose return checks but the false-vs-0 distinction and ReDoS risk typically require code review; semgrep rules can catch patterns but aren't default.
Closest to 'simple parameterised fix' (e3) — quick_fix is to replace if (preg_match(...)) with === 1 checks and add backtrack_limit; repeated pattern fix across call sites but each is small.
Closest to 'localised tax' (b3) — regex usage tends to cluster in validation/parsing code; doesn't shape the whole system but each call site carries the return-value gotcha.
Closest to 'serious trap' (t7) — misconception is explicit: three return values (1/0/false) where the falsy check conflates no-match with error, contradicting most other languages' regex APIs that throw on error.
Also Known As
TL;DR
Explanation
preg_match() and preg_replace() are PHP's primary regular expression functions using the PCRE library. Common pitfalls: catastrophic backtracking (ReDoS) occurs when a pattern with nested quantifiers matches a long string that ultimately fails — the engine retries exponentially many paths. Always set a backtrack limit and use pcre.backtrack_limit in php.ini. For untrusted patterns, validate the regex itself before executing it. preg_replace() with the /e modifier (deprecated PHP 5.5, removed 7.0) executed replacement strings as PHP code — a critical vulnerability.
Common Misconception
Why It Matters
Common Mistakes
- Checking if (preg_match(...)) where a pattern error returns false — same check for match and error.
- Not limiting input length before matching complex patterns — ReDoS on unbounded input.
- Using preg_match when str_contains or str_starts_with is sufficient — regex has overhead.
- Not using the PREG_OFFSET_CAPTURE flag when needing match position — re-running the regex to find position.
Code Examples
// Return value not checked properly:
if (preg_match('/^[a-z]+$/i', $input)) {
// preg_match returns 1 (match), 0 (no match), or FALSE (error)
// if() treats 0 and false the same — pattern errors silently fail
}
// Correct:
$result = preg_match('/^[a-z]+$/i', $input);
if ($result === false) throw new RuntimeException('Regex error: ' . preg_last_error_msg());
if ($result === 1) { /* matched */ }
// Basic match — returns 1 (match), 0 (no match), false (error)
if (preg_match('/^[a-z0-9_-]{3,20}$/i', $username)) {
// valid username
}
// Capture groups
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $m)) {
[$full, $year, $month, $day] = $m;
}
// Named captures
preg_match('/^(?P<year>\d{4})-(?P<month>\d{2})$/', $input, $m);
$year = $m['year'];
// All matches
preg_match_all('/<a href="([^"]+)"/', $html, $matches);
$urls = $matches[1];
// Replace
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower($title));
// Check for regex errors
if (preg_last_error() !== PREG_NO_ERROR) { /* handle */ }