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-dev

The 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
Email /^[^\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


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

Edit on GitHub