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
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
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.
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.
returning a bool type
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
On Friday, 25 September 2026 at 07:59, Sjoerd Langkemper
sjoerd-php@linuxonly.nl wrote: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.
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).
returning a bool type
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.
That feels like two separate methods then. match(): bool and matchCapture(): MatchResult, or something like that.
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 nowadaysArguably 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 ofenum RegexOptionson 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
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.
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.
FWIW composer/pcre has isMatch(): bool and match(): MatchResult (see
https://github.com/composer/pcre/blob/main/src/MatchResult.php)
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 ofenum RegexOptionson 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.)
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 topreg_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
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.
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()andpreg_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
Hi
For the prototype I've only added support to use this new class to the
preg_split()andpreg_grep()functions as they were easy to tweak.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
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
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
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
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
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 publicfor all params? Inspecting the setup would
be useful to build onto this in userland.
Regarding points brought up by otters
- +1 for the
Regexname. And in that casePcre\Regexmight be nicer
to avoid duplication and mixups. - I would also need
fromStringor 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 writenew Regex($pattern, multiline: true).
Fluent builders, enums, constants — those are just workarounds in the
absence of such syntax.
BR,
Juris
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 publicfor 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
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/23868The 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
Hi
I really like the idea, but I am not quite sure it fully replaces the
PREG_THROW_ON_ERRORflag.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
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:
-
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). -
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
- ... 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.
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.