Chapter 10: Real-World Patterns in PHP

Goal: Apply everything you’ve learned to common, practical use cases.


Why Real-World Patterns Are Different

Tutorial patterns are simple. Production patterns must:

  • Validate user input
  • Handle edge cases
  • Be secure (no ReDoS)
  • Be maintainable
  • Be documented

This chapter shows battle-tested patterns with explanations.


Email Validation

The Pattern

$pattern = '/^[a-z0-9]+(?:[._%+-][a-z0-9]+)*+@[a-z0-9-]+(?:\.[a-z0-9-]+)*+$/i';

Explanation

Start of string
  Local part:
    One or more alphanumeric characters
    Zero or more of:
      (literal . _ % + -) followed by one or more alphanumeric
  @
  Domain:
    One or more alphanumeric or hyphen
    Zero or more of:
      (literal .) followed by one or more alphanumeric or hyphen
End of string (case-insensitive)

Structure summary

  • Local part: [a-z0-9]+
  • Separator: @
  • Domain: [a-z0-9-]+ with dot-separated segments
  • TLD: [a-z0-9-]+

Usage

use PHPRegex\Toolkit\Regex;

$regex = Regex::create();

// Validate the pattern itself before using it
$result = $regex->validate($pattern);
if (!$result->isValid) {
    echo "Invalid pattern: " . $result->error;
    return;
}

// Check for potential ReDoS risk
$analysis = $regex->redos($pattern);
if ($analysis->severity->value !== 'safe') {
    throw new RuntimeException("Pattern has potential ReDoS risk");
}

// Use safely
if (preg_match($pattern, $userInput)) {
    echo "Valid email";
}

Why This Pattern?

Feature Benefit
^...$ Anchored - validates entire string
*+ Possessive - no backtracking
[a-z0-9] No special characters in local part
Case-insensitive Accepts any case

Date Validation (YYYY-MM-DD)

The Pattern

$pattern = '/^(?<year>\d{4})-(?<month>0[1-9]|1[0-2])-(?<day>0[1-9]|[12][0-9]|3[01])$/';

Explanation

Start of string
  Year: 4 digits (named 'year')
  Literal: -
  Month: 01-12 (named 'month')
    0[1-9]  = 01-09
    |1[0-2] = 10-12
  Literal: -
  Day: 01-31 (named 'day')
    0[1-9]   = 01-09
    |[12][0-9] = 10-29
    |3[01]   = 30-31
End of string

Usage

preg_match($pattern, '2024-01-15', $matches);

$year = $matches['year'];   // "2024"
$month = $matches['month']; // "01"
$day = $matches['day'];     // "15"

Note

This validates format, not validity. “2024-02-30” passes but isn’t a real date. For real dates, combine with PHP’s checkdate():

if (preg_match($pattern, $input, $m)) {
    if (checkdate((int)$m['month'], (int)$m['day'], (int)$m['year'])) {
        echo "Valid date!";
    } else {
        echo "Invalid calendar date";
    }
}

Phone Numbers (US Format)

The Pattern

$pattern = '/^\+?1?\s*\(?([0-9]{3})\)?\s*-?[0-9]{3}\s*-?[0-9]{4}$/';

Explanation

Start of string
  Optional: +1 (country code)
  Optional: whitespace
  Optional: (
  Area code: 3 digits (captured as group 1)
  Optional: )
  Optional: whitespace or hyphen
  Prefix: 3 digits
  Optional: whitespace or hyphen
  Line number: 4 digits
End of string

Usage

preg_match($pattern, '(555) 123-4567', $matches);
$areaCode = $matches[1];  // "555"

URLs (HTTP/HTTPS)

The Pattern

$pattern = '/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/';

Explanation

Start of string
  http:// or https://
  Optional: www.
  Domain: 1-256 characters (alphanumeric, @, %, _, ~, #, =)
  .
  TLD: 1-6 alphanumeric or parentheses
  Word boundary
  Optional path/query: any characters
End of string

Usage

if (preg_match($pattern, $url)) {
    echo "Valid URL format";
}

A Note on Risk

PHPRegex rates this pattern medium — “Polynomial backtracking, degree 2 (proven)”. A quadratic pattern turns 10× the input into 100× the work; the backtrack limit does not catch it. If URLs come from untrusted sources and can be long, tighten the pattern (the two character classes overlap on many characters) or cap the input length first. Check yours the same way:

vendor/bin/regex debug '/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/'

Log Parsing

The Pattern

$pattern = '/^(?<level>INFO|WARN|ERROR|DEBUG)\s+(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(?<message>.+)$/';

Explanation

Start of string
  Level: INFO, WARN, ERROR, or DEBUG (named 'level')
  Whitespace
  Timestamp: YYYY-MM-DD HH:MM:SS (named 'timestamp')
  Whitespace
  Message: rest of line (named 'message')
End of string

Usage

$logLine = 'INFO 2024-01-15 10:30:45 User logged in successfully';

preg_match($pattern, $logLine, $matches);

echo $matches['level'];      // "INFO"
echo $matches['timestamp'];  // "2024-01-15 10:30:45"
echo $matches['message'];    // "User logged in successfully"

Tags (HTML-like)

The Pattern

$pattern = '/^<([a-z][a-z0-9]*)([^>]*)>(.*?)<\/\1>$/i';

Explanation

Start of string
  Opening tag:
    <letter followed by letters/numbers>
    Zero or more attributes (not >)
  >
  Content: any characters (lazy)
  Closing tag: </same opening tag>
End of string (case-insensitive)

Usage

preg_match($pattern, '<div class="container">Content</div>', $matches);

echo $matches[1];   // "div"
echo $matches[2];   // ' class="container"'
echo $matches[3];   // "Content"

Password Strength

The Pattern

$pattern = '/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/';

Explanation

Start of string
  Lookahead: at least one uppercase
  Lookahead: at least one lowercase
  Lookahead: at least one digit
  Lookahead: at least one special char (@$!%*?&)
  Main: 8+ characters from allowed set
End of string

Usage

if (preg_match($pattern, $password)) {
    echo "Password is strong";
} else {
    echo "Password doesn't meet requirements";
}

Comparison Table

Verdicts generated with $regex->redos() on the exact patterns above:

Use Case Pattern from this chapter Anchored ReDoS verdict Capture
Email /^[a-z0-9]+(?:[._%+-][a-z0-9]+)*+@[a-z0-9-]+(?:\.[a-z0-9-]+)*+$/i Yes safe (proven) none
Date /^(?<year>\d{4})-(?<month>0[1-9]\|1[0-2])-(?<day>0[1-9]\|[12][0-9]\|3[01])$/ Yes safe (proven) named (year, month, day)
URL /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/ Yes medium — quadratic (proven) none
Phone /^\+?1?\s*\(?([0-9]{3})\)?\s*-?[0-9]{3}\s*-?[0-9]{4}$/ Yes safe (proven) numbered (1: area code)
Password /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ Yes safe (proven) none

The URL row is the one to remember: a pattern can look reasonable, anchor everything, and still carry a proven quadratic cost — the verdict column is why you run the analysis instead of eyeballing it.


Exercise: Build and Test a Pattern

Challenge

Create a pattern to validate a GitHub username:

  • Starts with letter or number
  • Contains letters, numbers, hyphens
  • Cannot start or end with hyphen
  • Max 39 characters

Solution

use PHPRegex\Toolkit\Regex;

$pattern = '/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/';

// Explanation
// ^                Start of string
// [a-zA-Z0-9]     Must start with letter or number
// (?:...)?        Optional tail (allows a single character)
// [a-zA-Z0-9-]{0,37} Middle: 0-37 chars
// [a-zA-Z0-9]     Must end with letter or number
// $                End of string

// Test with PHPRegex
$regex = Regex::create();
echo $regex->explain($pattern);

Key Takeaways

  1. Production patterns need anchors (^...$) for exact matching
  2. Use named groups for clarity and maintainability
  3. Validate format first, then validate logic separately
  4. Always check ReDoS risk before using patterns — with a tool, not by eye
  5. Lookaheads are great for validation without consuming

You’re a Regex Master!

Tutorial summary:

  1. Basics - Your first patterns
  2. Character Classes - Matching sets
  3. Anchors - Controlling position
  4. Quantifiers - Controlling repetition
  5. Groups - Structuring patterns
  6. Lookarounds - Context matching
  7. Backreferences - Self-reference
  8. Performance - Avoiding ReDoS
  9. Testing - Debugging patterns
  10. Real-World - Practical patterns

Next Steps

  • Cookbook - More pattern examples
  • ReDoS Guide - Deep dive on risk and mitigation
  • API Reference - API documentation
  • Apply what you’ve learned in your own project

Tutorial finished.

Edit on GitHub