dseguy / regex-class
Regexes as classes for PHP.
Package info
Language:C
Type:php-ext
Ext name:ext-regexclass
pkg:composer/dseguy/regex-class
Requires
- php: >=8.2
This package is auto-updated.
Last update: 2026-07-23 16:11:33 UTC
README
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
&$matchesout-parameter, not a return value. - Errors surface as a
false/nullreturn plus a globalpreg_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 surfacetests/pcre/— 17 tests ported from PHP'sext/pcre/tests/, demonstrating API equivalence withpreg_*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
.phpttest intests/(ortests/pcre/forpreg_*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 frommakebefore 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.