# Pre-RFC: adding a Regex\CompiledRegex class for compiling regular expressions

- Thread: https://externals.io/message/132609
- Original archive: https://news-web.php.net/php.internals/132609
- Started by Gina P. Banyard on 2026-09-23T19:01:54+00:00
- Messages: 19
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #132609 — Gina P. Banyard — 2026-09-23T19:01:54+00:00

URL: https://externals.io/message/132609#132609

Hello internals,

I spent the day prototyping an alternative to the PREG_THROW_ON_ERROR RFC as I'm not fully a fan of the approach.
It can be found as the following PR: https://github.com/php/php-src/pull/23868

The basic idea is to add a new Regex\CompiledRegex class that takes a pattern, and some boolean modifiers, to compile a regular expression before passing it to the preg_* functions.
I've currently landed on the following concept design:

```
namespace Regex {
	class CompilationError extends Exception {}

	/**
	 * @strict-properties
	 * @not-serializable
	 */
	final class CompiledRegex {
		public function __construct(
			string $pattern,
			bool $caseSensitive = true,
			bool $greedy = true,
			bool $anchor = false,
			bool $multiLine = false,
			bool $dotMatchesNewLine = false,
			bool $ignoreWhitespace = false,
			bool $captureOnlyNamedGroups = false,
			bool $allowDuplicateSubPatternNames = false,
		) {}
	}
}
```
The $pattern parameter removes the need to use a delimiter, and a potential call to `preg_quote()`, and modifiers that would be specified after the ending delimiter are boolean flags.
I've made a few opinionated choices in the prototype in the sense that the pattern *must* be a UTF-8 pattern, and the PCRE2_DOLLAR_ENDONLY compilation option is always on (i.e. the 'D' modifier) as those seem to be sensible starting conditions.

I initially thought of something like this:
public function __construct(string $pattern, int $modifiers = 0) {}

with all the different PCRE2_* modifiers exposed as class constants but it felt clunky and seems hard to pre-validate the flag combinations.

For the prototype I've only added support to use this new class to the `preg_split()` and `preg_grep()` functions as they were easy to tweak.

This class could lay the foundation on which to build a new nice OO API (such as match returning a bool type rather than integer 0/1 and false on failure), but I'm not currently pursuing the idea of trying to figure out all the API details needed to handle sub pattern matches, etc.

Would love to hear what everyone's opinion on this direction is.


Best regards,

Gina P. Banyard

---

## #132627 — Sjoerd Langkemper — 2026-09-25T06:56:01+00:00

URL: https://externals.io/message/132609#132627 · In reply to: #132609

> This class could lay the foundation on which to build a new nice OO API

Nice. I am in favor of improving the API, instead of slapping more flags onto the existing one.

> the pattern *must* be a UTF-8 pattern

Does this also apply to the subject? I recently had a reason to remove the u modifier from a regex: if the input is not valid UTF8, preg_match silently fails without a match. I think I was scraping web pages and discovered that some webpages don't contain valid UTF8, and that made it impossible to use preg_match with UTF8 on it.

> returning a bool type

Would it make more sense to return a Match object, with captured groups?

The proposed API would result in calls like this: 
new CompiledRegex(".*", false, false, false, true, true, true, false, true)
where it is hard to determine what all the true/false parameters mean. Would it be better to pass enums instead? Or is this solved by editor hints nowadays?

Regards,

Sjoerd Langkemper

---

## #132632 — Gina P. Banyard — 2026-09-25T10:43:01+00:00

URL: https://externals.io/message/132609#132632 · In reply to: #132627

> 
>
> > […]
>
> Nice. I am in favor of improving the API, instead of slapping more flags onto the existing one.
>
> > […]
>
> Does this also apply to the subject? I recently had a reason to remove the u modifier from a regex: if the input is not valid UTF8, preg_match silently fails without a match. I think I was scraping web pages and discovered that some webpages don't contain valid UTF8, and that made it impossible to use preg_match with UTF8 on it.

It is possible to set the PCRE2_MATCH_INVALID_UTF flag which allows to match part of an invalid UTF-8 subject, details of this are described at the end of https://www.pcre.org/current/doc/html/pcre2unicode.html in the "MATCHING IN INVALID UTF STRINGS" section.
However it does seem to have some limitations and one may indeed need to offer a way to disable UTF-8 mode altogether.

> > […]
>
> Would it make more sense to return a Match object, with captured groups?

How would this Match object work? What is it's API? And if you don't care about captured groups why would you need to instantiate an (or multiple) object when a boolean value would do just fine.
That's why I said I don't want to spend time on designing an API as this frankly needs multiple methods and is going to be complicated.
There are also new PCRE2 features not exposed to userland where it may make sense to do so in a greenfield API.

> The proposed API would result in calls like this:
> new CompiledRegex(".*", false, false, false, true, true, true, false, true)
> where it is hard to determine what all the true/false parameters mean. Would it be better to pass enums instead? Or is this solved by editor hints nowadays

Arguably we have named parameters to just toggle the options that are different from the default, so I wouldn't be writing a call to it like this nowadays anyway.
But Nora did suggest an array of enum RegexOptions on the PR, and my reply was that if PHP had a way to pass an Enum set this would be the best approach as you'd only have one parameter.
In any case I don't have strong feelings about this part (or maybe I should spend some time and figure out a way to make enum sets a reality before)

Best regards,
Gina P. Banyard

---

## #132633 — Alexander Egorov — 2026-09-25T10:46:39+00:00

URL: https://externals.io/message/132609#132633 · In reply to: #132627

> 
>
> This class could lay the foundation on which to build a new nice OO API

I also like the idea. Not exactly because I have something against the
original proposal of PREG_THROW_..., but because your suggestion can
(possibly?) increase performance for those apps who use regexps a lot.



> The proposed API would result in calls like this:
> new CompiledRegex(".*", false, false, false, true, true, true, false, true)
> where it is hard to determine what all the true/false parameters mean. Would it be better to pass enums instead? Or is this solved by editor hints nowadays?

That is also my concern. Since people have been using regexps in PHP
for ages in a way they are, I believe it should be possible to make an
instance of CompiledRegex from a usual pattern-string. In my practice,
I don't use many different modifiers, but those which I do use often
(mostly "i" and "s") - I type them automatically when writing a
pattern. Like a muscle-memory. In other words, it is easier for me to
still input them as part of the pattern that supplying them as
parameters. Besides, such a possibility will simplify the process of
migration to this class and possible future API: you just copy an
existing pattern without bothering to extract the modifiers from it.

Regards,

Alexandr Egorov.

---

## #132638 — Jordi Kroon — 2026-09-25T15:08:27+00:00

URL: https://externals.io/message/132609#132638 · In reply to: #132609

> Hello internals,
> 
> I spent the day prototyping an alternative to the PREG_THROW_ON_ERROR RFC as I'm not fully a fan of the approach.
Hi Gina,

Thank you for sharing this idea. As this would get rid of the ugly 
delimiters, which means that I can finally copy-paste regexes from the 
Regex app I use on a regular basis without having to consider slashes, 
pipes or anything what comes to mind.

> I initially thought of something like this:
> public function __construct(string $pattern, int $modifiers = 0) {}
> 
> with all the different PCRE2_* modifiers exposed as class constants but it felt clunky and seems hard to pre-validate the flag combinations.

Agreed, but having a constructor that takes 9 parameters doesn't feel 
great as well IMHO. To the eye of a developer, who usually doesn't fully 
understand PCRE2 most of these parameters will be unfamiliar to them. If 
we do add these to the constructor, could we at least try to match the 
PRCE constants? E.g greedy would then become ungreedy (PCRE_UNGREEDY).

> For the prototype I've only added support to use this new class to the `preg_split()` and `preg_grep()` functions as they were easy to tweak.

Is it by design that this class does not have a `__toString()` method. 
And would you consider adding it? It would potentially allow the stubs 
for the exiting functions to stay unchanged. This would be similar to 
what the Uri API is doing, however it would require some sort delimiter 
generation I suppose.

And if we are going that direction, how about we add a `::fromString()`, 
to make it easier to migrate to this new API? And make use of it's 
Validation ability.

-- 
Regards,

Jordi Kroon

---

## #132639 — Larry Garfield — 2026-09-25T16:19:42+00:00

URL: https://externals.io/message/132609#132639 · In reply to: #132632

> On Friday, 25 September 2026 at 07:59, Sjoerd Langkemper 
> <sjoerd-php@linuxonly.nl> wrote:
> > […]

I also like the idea of an OOP Regex API.  Though I would ask that we just call it Regex, not CompiledRegex.  The "Compiled" adds no relevant information that a developer cares about, but doubles the length of text they need to read and begs the question of how to make an "uncompiled regex" (which is not a thing).

> > […]
>
> How would this Match object work? What is it's API? And if you don't 
> care about captured groups why would you need to instantiate an (or 
> multiple) object when a boolean value would do just fine.
> That's why I said I don't want to spend time on designing an API as 
> this frankly needs multiple methods and is going to be complicated.
> There are also new PCRE2 features not exposed to userland where it may 
> make sense to do so in a greenfield API. 

That feels like two separate methods then.  match(): bool and matchCapture(): MatchResult, or something like that.

> > […]
>
> Arguably we have named parameters to just toggle the options that are 
> different from the default, so I wouldn't be writing a call to it like 
> this nowadays anyway.
> But Nora did suggest an array of `enum RegexOptions` on the PR, and my 
> reply was that if PHP had a way to pass an Enum set this would be the 
> best approach as you'd only have one parameter.
> In any case I don't have strong feelings about this part (or maybe I 
> should spend some time and figure out a way to make enum sets a reality 
> before) 

If we can make sets a native type with good operator usage (I have designs for this), then enum sets would fall out naturally.

Baring that, I assumed this would always be called with sparse named arguments, which I am fine with.  (As someone who rarely uses regexes, admittedly.)

--Larry Garfield

---

## #132640 — Casper Langemeijer — 2026-09-25T16:29:49+00:00

URL: https://externals.io/message/132609#132640 · In reply to: #132609

> to add a new Regex\CompiledRegex class that takes a pattern, and some boolean modifiers, to compile a regular expression 

Very nice. Don't like the name though. From my perspective as a user, I don't care if the regexp is compiled or not. I don't fully understand what compiled means in this context. But I know it's a regular expression into a value object doing some magic to ensure it is valid, same as all my user-space value objects.

Please consider something like:
Regexp\Expression
Regexp\Pattern

Greetings, Casper

---

## #132641 — Jordi Kroon — 2026-09-25T16:56:18+00:00

URL: https://externals.io/message/132609#132641 · In reply to: #132640

> 

> Please consider something like:
> Regexp\Expression
> Regexp\Pattern
> 

+1 for this class naming. PHP has is primarly using the shorthand 
"Regex" in the documentation, not Regexp (minor nitpicking).

Additionally I believe we should add PCRE(2?) to the namespace.
  - Regex\Pcre\...
-- 
Regards,

Jordi Kroon

---

## #132642 — Juris Evertovskis — 2026-09-25T17:03:32+00:00

URL: https://externals.io/message/132609#132642 · In reply to: #132609

> I've currently landed on the following concept design:
> 
> ```
> namespace Regex {
> 	class CompilationError extends Exception {}
> 
> 	/**
> 	 * @strict-properties
> 	 * @not-serializable
> 	 */
> 	final class CompiledRegex {
> 		public function __construct(
> 			string $pattern,
> 			bool $caseSensitive = true,
> 			bool $greedy = true,
> 			bool $anchor = false,
> 			bool $multiLine = false,
> 			bool $dotMatchesNewLine = false,
> 			bool $ignoreWhitespace = false,
> 			bool $captureOnlyNamedGroups = false,
> 			bool $allowDuplicateSubPatternNames = false,
> 		) {}
> 	}
> }
> ```

Hi!

Great idea!

I've got some stupid questions though:
- Why `final`? Let me add methods while there's nothing in the core.
- Why `@not-serializable`? Serialization would seem as straightforward 
as for a plain DTO.
- Why not `readonly public` for all params? Inspecting the setup would 
be useful to build onto this in userland.

Regarding points brought up by otters
- +1 for the `Regex` name. And in that case `Pcre\Regex` might be nicer 
to avoid duplication and mixups.
- I would also need `fromString` or similar, but no problem to add it 
myself for now, as long as it's not fainal.
- I don't buy the objections about the number of the args. These days 
there's no problem to write `new Regex($pattern, multiline: true)`. 
Fluent builders, enums, constants — those are just workarounds in the 
absence of such syntax.

BR,
Juris

---

## #132647 — Osama Aldemeery — 2026-09-26T04:03:01+00:00

URL: https://externals.io/message/132609#132647 · In reply to: #132609

Hi Gina,

> Hello internals,
>
> I spent the day prototyping an alternative to the PREG_THROW_ON_ERROR RFC as I'm not fully a fan of the approach.
> It can be found as the following PR: https://github.com/php/php-src/pull/23868
>
> The basic idea is to add a new Regex\CompiledRegex class that takes a pattern, and some boolean modifiers, to compile a regular expression before passing it to the preg_* functions.

I really like the idea, but I am not quite sure it fully replaces the
`PREG_THROW_ON_ERROR`  flag.

Compilation errors are only half of the error story...the other half
happens at match time, on patterns that compiled just fine.

Consider this:

```
$r = preg_match('/(a+)+$/', str_repeat('a', 20) . '!');

var_dump($r); // bool(false)
var_dump(preg_last_error_msg()); // string(25) "Backtrack limit exhausted"
```

As far as I understand, this class of errors
(`PREG_BACKTRACK_LIMIT_ERROR`, `PREG_RECURSION_LIMIT_ERROR`,
`PREG_JIT_STACKLIMIT_ERROR`, ...etc) depends on the subject, not the
pattern, so no amount of validation at construction time can catch it.

I think the proposal, though, removes a good part of the motivation
for `PREG_THROW_ON_ERROR`, so now whether PHP should have a regex
object that fails on compilation errors only and works with `preg_*`
functions, or have a flag that guarantees the next line after a
`preg_*` call only executes if the call left no error behind (or
both?), is up to you and the people on this list to decide. I
personally see the two as complementary rather than alternatives.

That said, I really like and am in support of having a class that
represents a compiled regex (for what my entirely non-voting opinion
is worth), whether it replaces the flag or not.

Best,
Osama

---

## #132649 — tim@bastelstu.be — 2026-09-26T10:43:42+00:00

URL: https://externals.io/message/132609#132649 · In reply to: #132638

Hi


> > […]
> 
> Is it by design that this class does not have a `__toString()` method.

Similarly to backed enums, I also consider `__toString()` to be a 
mistake, because there is generally not a single obvious scalar / string 
representation, which you also acknowledge yourself: Delimiters would 
need to be invented out of thin air - and modifiers are not necessarily 
portable across programming languages or regex engines.

> And would you consider adding it? It would potentially allow the stubs
> for the exiting functions to stay unchanged. This would be similar to
> what the Uri API is doing, however it would require some sort delimiter
> generation I suppose.

I don't follow the reference to the URI API. It specifically doesn't 
have a __toString() implementation.

> And if we are going that direction, how about we add a `::fromString()`,
> to make it easier to migrate to this new API? And make use of it's
> Validation ability.

That however would make sense to me, but it would likely need a better 
name, because it's a *specific* string format that is expected.

Best regards
Tim Düsterhus

---

## #132650 — tim@bastelstu.be — 2026-09-26T10:51:42+00:00

URL: https://externals.io/message/132609#132650 · In reply to: #132609

Hi


> 		public function __construct(
> 			string $pattern,
> 			bool $caseSensitive = true,
> 			bool $greedy = true,
> 			bool $anchor = false,
> 			bool $multiLine = false,
> 			bool $dotMatchesNewLine = false,
> 			bool $ignoreWhitespace = false,
> 			bool $captureOnlyNamedGroups = false,
> 			bool $allowDuplicateSubPatternNames = false,
> 		) {}

Similar to what the others mentioned, I'm not happy with the 
constructor, for two reasons:

1. I feel that named constructors work much better if there are multiple 
equally valid ways of constructing a value object, which likely is the 
case here with “undelimited pattern” and “delimited pattern including 
modifiers” and possible future additions. Some of the flags could 
potentially even be built into the constructors name (switching between 
Unicode / Bytestring support).

2. The order of the flags is pretty much arbitrary, making 
discoverability hard because IDEs typically suggest parameters in the 
order they appear in.

We likely want the most common flags first, which then would become ugly 
if later adding additional flags which are expected to be commonly used.

Since the parameters are intended to be used as “named parameter only”, 
we should likely first have some mechanism to prevent parameters from 
being used positionally, similarly to what I think Python supports.

Best regards
Tim Düsterhus

---

## #132651 — tim@bastelstu.be — 2026-09-26T10:54:02+00:00

URL: https://externals.io/message/132609#132651 · In reply to: #132647

Hi


> I really like the idea, but I am not quite sure it fully replaces the
> `PREG_THROW_ON_ERROR`  flag.
> 
> Compilation errors are only half of the error story...the other half
> happens at match time, on patterns that compiled just fine.

Using the class as the pattern could just imply the PREG_THROW_ON_ERROR 
behavior.

Best regards
Tim Düsterhus

---

## #132652 — tim@bastelstu.be — 2026-09-26T11:02:30+00:00

URL: https://externals.io/message/132609#132652 · In reply to: #132642

Hi


> - Why `final`? Let me add methods while there's nothing in the core.

This is a question that comes up for every new internal (value) class 
and has been extensively discussed during the design of ext/uri. To give 
a short summary:

- Allowing inheritance allows to break the assumptions of internal 
classes, particularly those that store internal state (since you can 
just bypass the parent constructor).
- It is impossible to correctly construct child classes, particularly 
those that add additional properties, breaking the expectations for 
“with-er” style methods.
- Adding additional methods becomes a possible breaking change.
- Liskov Substitution applies not just to the actual API, but also to 
the expected behavior. For value classes, the expected contract is “what 
the class does”, making it effectively impossible to override anything.

> - Why `@not-serializable`? Serialization would seem as straightforward
> as for a plain DTO.
> - Why not `readonly public` for all params? Inspecting the setup would
> be useful to build onto this in userland.

These two are probably a consequence of the initial design of an 
“opaque” resource object. I agree that it would be reasonable to lift it 
(when a proper API is designed around the proposed class).

Best regards
Tim Düsterhus

---

## #132656 — Jordi Kroon — 2026-09-26T14:24:57+00:00

URL: https://externals.io/message/132609#132656 · In reply to: #132649

> I don't follow the reference to the URI API. It specifically doesn't 
> have a __toString() implementation.

Hi Tim,

My apologies. It doesn't have a __toString() implementation. However it 
does have a non-magic toString(). Which in the case of URI recomposes to 
a normalized string.

-- 
Regards,

Jordi Kroon

---

## #132657 — tim@bastelstu.be — 2026-09-26T14:31:23+00:00

URL: https://externals.io/message/132609#132657 · In reply to: #132656

Hi


> My apologies. It doesn't have a __toString() implementation. However it
> does have a non-magic toString(). Which in the case of URI recomposes to
> a normalized string.

Yes, but the non-magicness is an important distinction here: It requires 
an explicit and deliberate call to the method, allowing the users to 
make an explicit choice of what recomposition is desired (normalized or 
raw for RFC 3986 and Ascii or Unicode for WHATWG).

Similarly, Time\Instant (https://wiki.php.net/rfc/time_instant_class) 
will not be implementing `__toString()`.

Best regards
Tim Düsterhus

---

## #132658 — Ayesh Karunaratne — 2026-09-26T14:39:27+00:00

URL: https://externals.io/message/132609#132658 · In reply to: #132609

> ```
> namespace Regex {
>         class CompilationError extends Exception {}
>
>         /**
>          * @strict-properties
>          * @not-serializable
>          */
>         final class CompiledRegex {
>                 public function __construct(
>                         string $pattern,
>                         bool $caseSensitive = true,
>                         bool $greedy = true,
>                         bool $anchor = false,
>                         bool $multiLine = false,
>                         bool $dotMatchesNewLine = false,
>                         bool $ignoreWhitespace = false,
>                         bool $captureOnlyNamedGroups = false,
>                         bool $allowDuplicateSubPatternNames = false,
>                 ) {}
>         }
> }
> ```
> The $pattern parameter removes the need to use a delimiter, and a potential call to `preg_quote()`, and modifiers that would be specified after the ending delimiter are boolean flags.
> I've made a few opinionated choices in the prototype in the sense that the pattern *must* be a UTF-8 pattern, and the PCRE2_DOLLAR_ENDONLY compilation option is always on (i.e. the 'D' modifier) as those seem to be sensible starting conditions.
>
> I initially thought of something like this:
> public function __construct(string $pattern, int $modifiers = 0) {}
>
> with all the different PCRE2_* modifiers exposed as class constants but it felt clunky and seems hard to pre-validate the flag combinations.
>

Hi Gina,

Do you plan to add a `CompiledRegex::fromRegexp(string $pattern)`
method that can accept a delimited regex with flags? It can
significantly improve the migration path from existing code bases.
Libraries that build regexes from a user input (like a router
accepting a route with a regex pattern, for example) can easily pass a
user-provided regex and validate it immediately.

Echoing what others mentioned, I think the constructor is not that
intuitive without IDE parameter hints. This is still PCRE library's
domain, so when we update the PCRE2 library, we will have to change
the class signature to add new flags. If PCRE were to remove any of
the flags, we would essentially have a no-op that parameter. None of
which is ideal. It is also possible to configure the build with a
different PCRE2 library version, so the signature depends not only on
the PHP version but also on the PCRE version.

I'm definitely in favor of having compiled valid Regexps as class
objects, but I wish it would be a thin wrapper (similar to classes
that replace legacy `resource` objects), with a lot of logic and
validation handled at PCRE layer.

Thank you,
Ayesh.

---

## #132661 — Morgan — 2026-09-27T00:23:04+00:00

URL: https://externals.io/message/132609#132661 · In reply to: #132650

> 
> 1. ... possible future additions.
 >
Such as methods for constructing a regex algebraically (e.g. an 
alternatives method taking (objects representing) "a", "b", and "c", and 
returning "a|b|c").

The thing is that all four of those Regex objects will have flags 
set/unset and it's not immediately clear how the alternation's flags 
relate to the alternatives' flags. You can already set/unset flags on 
portions of a regular expression e.g., "/((?i)caseless)|caseful/", so 
the settings could be retained for the alternatives, but that doesn't 
say what the settings would be for their combination.

---

## #132670 — Jordi Boggiano — 2026-09-28T07:51:03+00:00

URL: https://externals.io/message/132609#132670 · In reply to: #132639

Hey,

Overall very positive about this, the preg_ functions are full of foot 
guns.

I've worked on https://github.com/composer/pcre#basic-usage which adds 
some type safety / strictness to the whole thing, and could maybe be of 
some inspiration for the API although some things I had to do because of 
limitations of the underlying or phpstan at the time, so it's not all 
perfect by any means.


> > […]
> That feels like two separate methods then.  match(): bool and matchCapture(): MatchResult, or something like that.

FWIW composer/pcre has isMatch(): bool and match(): MatchResult (see 
https://github.com/composer/pcre/blob/main/src/MatchResult.php)

> > […]
> If we can make sets a native type with good operator usage (I have designs for this), then enum sets would fall out naturally.
>
> Baring that, I assumed this would always be called with sparse named arguments, which I am fine with.  (As someone who rarely uses regexes, admittedly.)

I'd really like to still have a way to pass a string with modifiers like 
JS has `new RegExp("ab+c", "i")`, because these modifier are hardwired 
in many people's brains, but it could also be an alternative factory 
method rather than overloading the second param.

And finally to correct a point from the OP Gina:

 > The $pattern parameter removes the need to use a delimiter, and a 
potential call to `preg_quote()`

I do not think the latter is true. We still need to quote if you pass 
untrusted input, or just are otherwise unsure what's in a string. And 
having Regex::escape() would make sense I guess.

Best, Jordi

-- 
Jordi Boggiano
@seldaek -https://seld.be
