Regex in PHP
This guide explains how PCRE works in PHP and how PHPRegex interprets the same syntax. Examples use full regex literals (/pattern/flags).
What Is PCRE?
PHP uses PCRE2 (Perl Compatible Regular Expressions) for regex operations: a reimplementation of Perl’s regular expression syntax, widely used and well-tested.
PCRE = Perl Compatible Regular Expressions
= The engine that powers preg_* functions
PHP Regex Functions
The Core Functions
| Function | What It Does | Returns |
|---|---|---|
preg_match() |
Find first match | 1 or 0 |
preg_match_all() |
Find all matches | count |
preg_replace() |
Find and replace | string |
preg_split() |
Split string | array |
preg_grep() |
Filter array by pattern | array |
Quick Examples
// Find first match
preg_match('/\d+/', 'Order 42', $matches);
echo $matches[0]; // "42"
// Find all matches
preg_match_all('/\d+/', 'Order 42, Price 99', $matches);
print_r($matches[0]); // Array ( [0] => "42", [1] => "99" )
// Replace
preg_replace('/\s+/', '-', 'hello world'); // "hello-world"
// Split
preg_split('/,\s*/', 'a, b, c'); // Array ( [0] => "a", [1] => "b", [2] => "c" )
// Filter
$users = ['admin', 'user1', 'guest', 'moderator'];
preg_grep('/^admin|moderator/', $users); // Array ( [0] => "admin", [3] => "moderator" )
Delimiters: The / Characters
Every PHP regex pattern needs delimiters - characters that mark the start and end:
Standard Delimiter
$pattern = '/hello/i'; // / is the delimiter
When Your Pattern Contains /
Use a different delimiter:
// Problem: / appears in the pattern
$pattern = '/https://example.com/'; // Error: unescaped delimiter
// Solution: Use # as delimiter
$pattern = '#https://example\.com#';
// Or ~ or any non-alphanumeric, non-backslash, non-whitespace ASCII character
$pattern = '~https://example\.com~';
$pattern = '%https://example\.com%';
Valid Delimiters
/ # ~ % , ; : ! ' " ( ) [ ] { } < > |
Delimiter Anatomy
Pattern: /^[a-z]+@[a-z]+\.[a-z]+$/i
Delimiter: /
Body: ^[a-z]+@[a-z]+\.[a-z]+$
Flags: i
Pattern Modifiers (Flags)
Add flags after the closing delimiter to change behavior:
Most Common Flags
| Flag | Name | Effect |
|---|---|---|
i |
Case-insensitive | /hello/i matches “HELLO” |
m |
Multiline | ^ and $ match line boundaries |
s |
Dot-all | . matches newlines too |
u |
Unicode | Unicode support |
x |
Extended | Ignore whitespace, allow comments |
Examples
// Case-insensitive
preg_match('/hello/i', 'HELLO'); // Match: yes
// Multiline (^ and $ match each line)
preg_match('/^error/m', "line1\nerror here"); // Match: yes
// Dot-all (. includes newlines)
preg_match('/hello.world/s', "hello\nworld"); // Match: yes
// Unicode
preg_match('/^\p{L}+$/u', 'café'); // Match: yes (Unicode letters)
// Extended mode (ignores spaces, allows comments)
$pattern = '/(
\d{3} # Area code
- # Separator
\d{4} # Local number
)/x';
Less Common Flags
| Flag | Effect |
|---|---|
A |
Anchor to start (like \A) |
D |
Dollar matches end only (like \z) |
U |
Ungreedy by default |
J |
Allow duplicate names in groups |
Common Use Cases with Code
1. Validate Email
$pattern = '/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i';
if (preg_match($pattern, $email)) {
echo "Valid email format";
}
2. Extract Data
// Get all email addresses from text
$text = "Contact us at [email protected] or [email protected]";
preg_match_all('/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i', $text, $matches);
print_r($matches[0]);
// Extract date parts
preg_match('/(\d{4})-(\d{2})-(\d{2})/', '2024-01-15', $matches);
$year = $matches[1]; // "2024"
$month = $matches[2]; // "01"
$day = $matches[3]; // "15"
3. Find and Replace
// Replace multiple spaces with single space
preg_replace('/\s+/', ' ', 'hello world'); // "hello world"
// Convert dates from YYYY-MM-DD to DD/MM/YYYY
preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', '2024-01-15'); // "15/01/2024"
4. Input Validation
// Validate US phone number
$pattern = '/^\(?[0-9]{3}\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}$/';
if (preg_match($pattern, $phone)) {
echo "Valid phone number";
}
// Validate password strength
$pattern = '/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/';
Important: preg_quote()
When inserting user input into patterns, always use preg_quote():
// DANGEROUS: User input directly in pattern!
$userInput = 'test+'; // + is a regex special character!
$pattern = "/$userInput/"; // Could fail or match unexpected things
// SAFE: Escape user input
$userInput = 'test+';
$pattern = '/'.preg_quote($userInput, '/').'/';
echo $pattern; // "/test\+/"
Syntax
preg_quote(string $str, ?string $delimiter = null): string
Error Handling
Check for Errors
preg_match('/invalid[/', 'test', $matches);
// Always check the result!
if (preg_last_error() === PREG_NO_ERROR) {
echo "Match attempted successfully";
} else {
echo "Error: " . preg_last_error_msg();
}
Error Codes
| Constant | Meaning |
|---|---|
PREG_NO_ERROR |
No error |
PREG_INTERNAL_ERROR |
PCRE internal error |
PREG_BACKTRACK_LIMIT_ERROR |
Backtrack limit reached |
PREG_RECURSION_LIMIT_ERROR |
Recursion limit reached |
PREG_BAD_UTF8_ERROR |
Malformed UTF-8 |
PREG_BAD_UTF8_OFFSET_ERROR |
UTF-8 offset doesn’t match |
Performance: PCRE Limits
For untrusted input, lower the limits to prevent runaway patterns. PHP’s defaults are pcre.backtrack_limit = 1000000 and pcre.recursion_limit = 100000 — setting them to their defaults protects nothing:
// Lower the limits (php.ini or runtime)
ini_set('pcre.backtrack_limit', '100000'); // down from the 1,000,000 default
ini_set('pcre.recursion_limit', '10000'); // down from the 100,000 default
// What happens when the backtrack limit is reached?
$result = preg_match('/(a+)+$/m', str_repeat('a', 1000000) . '!');
if ($result === false) {
echo "Pattern too complex or input too long";
echo "Error: " . preg_last_error_msg(); // Backtrack limit exhausted
}
Why Limits Matter
Without limits, malicious input can cause:
- CPU exhaustion (exponential backtracking)
- Memory exhaustion (deep recursion)
- Service denial (application hangs)
The limit only caps a single match attempt; the ReDoS guide explains the real cost model and what PHPRegex can prove about a pattern before it reaches production.
Common Mistakes and How to Fix Them
Mistake 1: Forgetting Anchors
// Unanchored pattern matches inside longer text
preg_match('/bar/', 'foobar'); // Match: yes (not desired)
// Anchors require a full-string match
preg_match('/^bar$/', 'bar'); // Match: yes
preg_match('/^bar$/', 'foobar'); // Match: no
Mistake 2: Using [A-z]
// [A-z] includes characters between Z and a: [ \ ] ^ _ `
// This matches underscores!
preg_match('/^[A-z]+$/', 'hello_world'); // Match: yes (unexpected)
// Correct: [A-Za-z]
preg_match('/^[A-Za-z]+$/', 'hello_world'); // Match: no
Mistake 3: Not Escaping Special Characters
// . matches any character!
preg_match('/file.txt/', 'fileXtxt'); // Match: yes (incorrect)
// Escape special characters
preg_match('/file\.txt/', 'file.txt'); // Match: yes
preg_match('/file\.txt/', 'fileXtxt'); // Match: no
Mistake 4: Greedy Matching
$html = '<p>Hello</p><p>World</p>';
// Greedy: matches everything to the last >
preg_match('/<p>.*<\/p>/', $html, $m);
echo $m[0]; // "<p>Hello</p><p>World</p>"
// Lazy: matches to the first >
preg_match('/<p>.*?<\/p>/', $html, $m);
echo $m[0]; // "<p>Hello</p>"
Where PHPRegex Helps
Installation
composer require --dev php-regex/php-regex:2.x-devThe examples below use the Regex facade; the Quick Start covers a first full analysis.
1. Validate Patterns Before Use
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
$result = $regex->validate($userPattern);
if (!$result->isValid) {
echo "Invalid pattern: " . $result->error;
echo "Hint: " . $result->getHint();
return;
}
2. Explain Complex Patterns
echo $regex->explain('/^(?<email>[^@]+)@(?<domain>[^@]+)$/');
Output:
Regex matches
Anchor: the beginning of a line
Capturing group (named: 'email')
Character Class: any character except [ '@' ] (one or more times)
End group
'@'
Capturing group (named: 'domain')
Character Class: any character except [ '@' ] (one or more times)
End group
Anchor: the end of a line
3. Detect ReDoS Vulnerabilities
$analysis = $regex->redos('/(a+)+$/');
if ($analysis->severity->value === 'critical') {
echo "DANGEROUS: This pattern can cause exponential backtracking!";
echo "Try: /a+$/ instead";
}
4. Generate Test Data
// Generate a valid email for testing
$email = $regex->generate('/^[a-z]+@[a-z]+\.[a-z]+$/i');
echo $email; // Example output: "[email protected]" (random each run)
Quick Reference
Regex to PCRE Translation
| Common Regex | PCRE Syntax |
|---|---|
| Start of string | ^ |
| End of string | $ |
| Any character | . |
| Word character | \w |
| Digit | \d |
| Whitespace | \s |
| One or more | + |
| Zero or more | * |
| Optional | ? |
| Group | (...) |
| Alternation | \| |
Common Patterns
| Task | Pattern |
|---|---|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
|
| URL | /^https?:\/\/[^\s]+$/ |
| Phone | /^\d{3}-?\d{3}-?\d{4}$/ |
| Date | /^\d{4}-\d{2}-\d{2}$/ |
| IP Address | /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ |
Learn More
- Regex Tutorial - Step-by-step guide
- Quick Start - Get productive in 5 minutes
- ReDoS Guide - Prevent catastrophic backtracking
- Cookbook - Ready-to-use patterns
Troubleshooting
| Problem | Solution |
|---|---|
| Pattern not matching | Add ^ and $ anchors |
| Partial matches | Use anchors to match entire string |
| Error “unknown modifier” | Check your delimiters |
| Unicode not working | Add u flag |
| Performance issues | Check for ReDoS, set limits |
| Complex pattern | Use PHPRegex to explain it |
Next: CLI Guide