dseguy/regex-class

Regexes as classes for PHP.

Maintainers

Package info

github.com/dseguy/regexClass

Language:C

Type:php-ext

Ext name:ext-regexclass

pkg:composer/dseguy/regex-class

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-07-23 15:43 UTC

This package is auto-updated.

Last update: 2026-07-23 16:11:33 UTC


README

License: PHP-3.01 PHP PCRE2

A PHP extension that exposes PCRE2 through an object-oriented API — no more &$matches reference parameters, false-on-failure return values, or global preg_last_error() state. Compile a pattern once into an immutable Regex object, then match, replace, split, and grep against it with typed, exception-driven results.

Targets PHP 8.5+ (PHP 8.6 feature target), built on PCRE2 ≥ 10.30. Originated as a PHP RFC proposing this API for ext/pcre; this repo is the reference implementation.

Why

The preg_* functions have been part of PHP since PHP 3 and show their age:

  • Return types are a moving target: int|false, string|string[]|null, false — the exact shape depends on flags and outcome.
  • Match data comes back through a &$matches out-parameter, not a return value.
  • Errors surface as a false/null return plus a global preg_last_error() you have to remember to check.
  • Checking whether a pattern even compiles means suppressing an E_WARNING.
  • Interpolating a literal into a pattern requires a separate preg_quote() call.

This extension replaces that surface with typed objects (RegexMatch, RegexMatchGroup, RegexSplitResult) and exceptions (InvalidRegexException, RegexExecutionException) instead of sentinel return values and global state. See Mapping from preg_* below for the direct translation.

Installing

This extension isn't packaged yet — build it from source (see Building from source below). PIE/PECL packaging is tracked as future work; contributions welcome.

Classes

Class Role
Regex Immutable compiled pattern. Factory: Regex::of($pattern) or Regex::compile($pattern).
RegexMatch Single match result. Implements ArrayAccess (group access via $m[0], $m['name']).
RegexMatchGroup One capture group inside a RegexMatch.
RegexMatchIterator Lazy iterator over all matches. Returned by matchEach().
RegexSplitResult Result of split(). Provides parts() and separators().
RegexException Base exception (extends RuntimeException).
InvalidRegexException Thrown on compilation failure.
RegexExecutionException Thrown on runtime PCRE2 error.

Quick start

// Compile once, use many times
$r = Regex::of('/^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})$/');

// Single match — returns RegexMatch or null
$m = $r->match('2026-06-01');
echo $m->value;                  // "2026-06-01"
echo $m->group('year')->value;   // "2006"
echo $m->group(1)->offset;       // byte offset of group 1

// All matches — returns RegexMatch[]
$matches = Regex::of('/\d+/')->matchAll('a1 b22 c333');
foreach ($matches as $m) {
    echo $m->value . ' at ' . $m->offset . "\n";
}

// Test (true/false)
var_dump(Regex::of('/^\d+$/')->test('42'));          // bool(true)
var_dump(Regex::of('/^\d+$/')->testStrict('42 '));   // bool(false)

// Replace
echo Regex::of('/\b\w/')->replace('hello world', strtoupper($m->value));
// Callback variant
echo Regex::of('/\b\w/')->replaceWith('hello world', fn($m) => strtoupper($m->value));

// Split
$res = Regex::of('/,\s*/')->split('a, b, c');
var_dump($res->parts());       // ["a", "b", "c"]
var_dump($res->separators());  // [RegexMatch(","), RegexMatch(", ")]

// Grep
$urls = ['http://a.com', 'ftp://b.org', 'https://c.net'];
var_dump(Regex::of('/^https?:/i')->grep($urls));         // http + https
var_dump(Regex::of('/^https?:/i')->grepInvert($urls));   // ftp only

Pattern format

Patterns use the same /inner/flags delimiter syntax as PHP's preg_* functions.

Bracket-style delimiters are supported: {...}, [...], (...), <...>.

Any non-alphanumeric, non-backslash character may be used as a delimiter.

Supported flags

Flag PCRE2 option Meaning
i PCRE2_CASELESS Case-insensitive
m PCRE2_MULTILINE ^/$ match per line
s PCRE2_DOTALL . matches newline
x PCRE2_EXTENDED Ignore unescaped whitespace
u PCRE2_UTF Unicode (UTF-8) mode
n PCRE2_NO_AUTO_CAPTURE Suppress unnamed captures
U PCRE2_UNGREEDY Make quantifiers ungreedy
D PCRE2_DOLLAR_ENDONLY $ only at true end
A PCRE2_ANCHORED Anchor at start
J PCRE2_DUPNAMES Allow duplicate group names

API reference

Regex

Regex::of(string $pattern): Regex          // Compile; throw InvalidRegexException on failure
Regex::compile(string $pattern): Regex     // Alias of of()
Regex::withLiteral(string $literal): Regex // Append literal (auto-escaped) to inner pattern
Regex::withPattern(string $fragment): Regex // Append raw regex fragment to inner pattern

match(string $subject, int $offset = 0): ?RegexMatch
matchAll(string $subject, int $offset = 0): RegexMatch[]
matchEach(string $subject): RegexMatchIterator
test(string $subject): bool                // true if any match exists
testStrict(string $subject): bool          // true only if the entire string matches

replace(string $subject, string $replacement): string       // Replace all; $1/${1} backrefs
replaceFirst(string $subject, string $replacement): string  // Replace first only
replaceWith(string $subject, callable $fn): string          // Callback receives RegexMatch
replaceFirstWith(string $subject, callable $fn): string

split(string $subject, int $limit = -1): RegexSplitResult

grep(array $subjects): array          // Preserves original keys
grepInvert(array $subjects): array

getPattern(): string                   // Full pattern string including delimiters+flags
getCaptureGroupCount(): int            // Count of all capture groups
getNamedGroups(): array                // Group names in order of appearance
__toString(): string                   // Same as getPattern()

RegexMatch

readonly string $value       // Full matched text
readonly int    $offset      // Byte offset in subject
readonly int    $charOffset  // UTF-8 codepoint offset

group(int|string $index): ?RegexMatchGroup
namedGroups(): array<string, string>   // name => value for all named groups

// ArrayAccess: $m[0] = $m->value, $m[1] = group(1)->value, $m['name'] = group('name')->value

RegexMatchGroup

readonly string $value
readonly int    $offset
readonly int    $charOffset
readonly string $name    // '' for unnamed groups
readonly int    $index   // 1-based capture group number

RegexSplitResult

parts(): string[]                // Text between separators (count = separators + 1)
separators(): RegexMatch[]       // Matched separator objects (with offset info)

// Countable: count() = number of parts
// IteratorAggregate: foreach iterates parts

Building from source

cd regex/
phpize
./configure --enable-regex
make -j$(nproc)
# Test
php run-tests.php -n -d extension=modules/regex.so -q tests/
php run-tests.php -n -d extension=modules/regex.so -q tests/pcre/

Requires PCRE2 development headers (libpcre2-dev / pcre2-devel). On macOS with Homebrew: brew install pcre2.

Tests

  • tests/ — 17 self-contained unit tests covering the full API surface
  • tests/pcre/ — 17 tests ported from PHP's ext/pcre/tests/, demonstrating API equivalence with preg_* functions

Run all 34 tests:

php run-tests.php -n -d extension=modules/regex.so -q tests/ tests/pcre/

Mapping from preg_*

Old API New API
preg_match($p, $s, $m) $m = Regex::of($p)->match($s)
preg_match($p, $s, $m, PREG_OFFSET_CAPTURE) Same; $m->offset, $m->group(n)->offset always available
preg_match($p, $s, $m, 0, $offset) $m = Regex::of($p)->match($s, $offset)
preg_match_all($p, $s, $m) $matches = Regex::of($p)->matchAll($s)
preg_replace($p, $r, $s) Regex::of($p)->replace($s, $r) (use ${n} for backrefs)
preg_replace_callback($p, $fn, $s) Regex::of($p)->replaceWith($s, $fn) (callback gets RegexMatch, not array)
preg_split($p, $s) Regex::of($p)->split($s)->parts()
preg_split($p, $s, -1, PREG_SPLIT_NO_EMPTY) array_filter(Regex::of($p)->split($s)->parts(), fn($p) => $p !== '')
preg_split($p, $s, -1, PREG_SPLIT_DELIM_CAPTURE) Interleave ->parts() and ->separators() manually
preg_grep($p, $arr) Regex::of($p)->grep($arr)
preg_grep($p, $arr, PREG_GREP_INVERT) Regex::of($p)->grepInvert($arr)
preg_quote($s, $delim) Regex::withLiteral($s) (auto-uses the pattern's delimiter)

Contributing

Bug reports and pull requests are welcome via the issue tracker.

  • Add or update a .phpt test in tests/ (or tests/pcre/ for preg_* equivalence cases) for any behavior change.
  • Run the full suite (php run-tests.php -n -d extension=modules/regex.so -q tests/ tests/pcre/) and confirm zero compiler warnings from make before opening a PR.
  • Keep new public API consistent with the existing style: immutable value objects, typed properties, exceptions instead of sentinel returns.
  • Discussion of the underlying language proposal belongs on the PHP RFC / internals@lists.php.net, not this repo's tracker.

License

PHP License 3.01, matching ext/pcre and the rest of PHP core — see php.net/license/3_01.txt.