composer/pcre: making PHP's preg functions fail loudly
PCRE wrapping library that offers type-safe preg_* replacements.
At a glance
- What is it?
- composer/pcre wraps PHP's preg family so a failed match throws instead of returning false or null, and it does that by forcing PREG_UNMATCHED_AS_NULL so unmatched groups are consistently null. The trade is a set of restrictions on which flags you may pass, and a PHPStan extension that understands the patterns.
- Who is it for?
- Adopt composer/pcre if you are a PHP developer who wants static analysis to catch the null returns that preg_match and preg_replace can hand back, since the whole library exists to make those failures visible and typed. Keep away if you need the flag combinations it forbids, because PREG_OFFSET_CAPTURE, PREG_SET_ORDER, preg_filter and array subjects are all rejected by design.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 22 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem is the null, not the regex
composer/pcre is a thin wrapper around PHP's preg functions that offers type-safe replacements, and the README is precise about why it exists. It gives you a way to ensure the preg functions do not fail silently, returning unexpected nulls that may not be handled.
That is the whole problem statement, and it is a narrower one than you might expect. The regex engine is not the issue and the library does not replace it. The issue is that a failed preg_match returns false, a failed preg_replace can return null, and a partially matched pattern gives you an array with null holes in it. None of that is a bug in PHP, and all of it is annoying for a static analyser to reason about.
The fix is that if anything fails to match or replace, it throws a Composer\Pcre\PcreException instead of returning null, or false in some cases. The practical consequence is that you can use the return values safely, relying on the fact that they can only be strings for replace, ints for match, or arrays for grep and split.
The library is by Composer, the maintainer of the PHP package tooling, and it is MIT licensed. The primary language is PHP, the default branch is main, the last push was on 2026-10-01, and the release history shows 3.4.0 on 2026-06-11, 3.3.2 on 2024-11-12 and 2.3.2 the same day, so the 3.x line has been quiet for a while.
The PHP floor is documented per major version: PHP 7.4.0 for the 3.x versions, 7.2.0 for 2.x, and 5.3.2 for 1.x.
Installing it, and what a first change looks like
Installation is a Composer require, and the library has no other runtime dependency worth mentioning because it is a wrapper around functions that are already in PHP:
$ composer require composer/pcreThe first change to a codebase is usually mechanical too, because the method names are the same as the built-in functions. A line that read if (preg_match('{fo+}', $string, $matches)) becomes if (Preg::match('{fo+}', $string, $matches)), with the import added, and the behaviour change is that a failure now throws.
That last part is the part to plan for rather than discover. Code that relied on a false return to mean something, and that now has an exception thrown in its place, will fail differently, and the fix is to catch the PcreException or to use the isMatch variant where a boolean is what you actually wanted.
The PHP version floor is the other thing to check before you require it. The 3.x line needs PHP 7.4 or newer, so a project still on PHP 7.2 or 7.3 is pinned to the 2.x line, which behaves differently in one respect: the library always used PREG_UNMATCHED_AS_NULL for matching from 2.0, but the callback replacements only got the flag in 3.0.
The repository is laid out like a small library with a real toolchain around it, with a src/ directory, a tests/ directory, a composer.json, the PHPStan configuration with its extension registration and baseline, a phpunit configuration, and a CONTRIBUTING.md.
The same calls, on a Preg class
The migration is mechanical, because the API mirrors the built-in functions one for one. The list of built-in calls is the list of replacements:
if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);and the same set on the Preg class:
use Composer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);Note the one name that changed rather than just moving namespaces. The offset capture flag becomes its own method, matchWithOffsets, rather than a flag argument. That is the pattern the whole library follows, and the restrictions section makes it explicit: you cannot pass the flag to match or matchAll.
There are also two extra method families that the built-ins do not have. The isMatch variants return a bool rather than an int, for when the number of matches is not useful:
use Composer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // bool
if (Preg::isMatchAll('{fo+}', $string, $matches)) // booland the strict groups variants, which guarantee that every group is present and therefore non-nullable, so a subpattern that does not match throws instead of leaving a hole in the array.
PREG_UNMATCHED_AS_NULL, and what it changed
The behavioural change that matters most is not the exception, it is the flag.
As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for all match and isMatch functions, and as of 3.0 the flag is also set for replaceCallback and replaceCallbackArray. The README describes the effect as much saner and more predictable results, and the reasoning is about consistency: without the flag, an unmatched trailing group is simply absent from the matches array, and whether that is true depends on which PCRE features the pattern happens to use. With it, an unmatched group is present and null, so the shape of the array does not vary with the pattern.
That sounds like a small detail and it is the opposite. It is the difference between a matches array whose shape you can write a type for and one you cannot, and it is why the strict group variants can make the non-nullable promise at all.
The strict group methods are:
use Composer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throw
if (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))And the caveat is stated carefully, because it is where people get surprised. The general rule is that this is safe as long as you do not have optional subpatterns, such as a group with a question mark or a star, or branches with a pipe that result in some groups not being matched at all.
There is a subtlety worth reading twice. A subpattern that can match an empty string, like a group with a dot-star, is not optional, and it will be present as an empty string in the matches. A non-matching subpattern, even an optional one, is not present in the matches either, so that is also not a problem to use these with the strict groups methods.
So the rule is not about optionality in the abstract, it is about whether a group ends up absent, and the library draws the line at groups that can be skipped entirely.
The Regex class, for people who prefer result objects
There is a second interface for anyone who finds the by-reference arguments a bit terse. The Regex class replaces them with result objects, and the README calls this a slightly more verbose usage.
use Composer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched
// as it returns a bool instead of int(1/0) for match
$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;The result object carries a matched flag, the matches, a count for the all variant, and a result property on the replace calls, so the return value is reached through the object rather than returned directly.
One asymmetry is worth noting. preg_grep and preg_split are only callable through the Preg class, because they do not have complex return types warranting a specific result object. That is a sensible rule: the result object exists where there is something to say, and the two functions that return a plain list do not need one.
The result classes are in the src/ directory and the README links them individually, so the shape of each object is in the source rather than in the documentation, which is the usual state of affairs for a library this small.
The restrictions, and the PHPStan extension
The restrictions section exists because type safety and flexibility pull in different directions, and the library resolves the conflict in favour of the types.
Matching with PREG_OFFSET_CAPTURE is available through matchWithOffsets and matchAllWithOffsets, and you cannot pass the flag to match or matchAll. Preg::split rejects PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead. matchAll rejects PREG_SET_ORDER, since it changes the shape of the returned matches, and there is no alternative provided because the README says you can fairly easily code around it.
preg_filter is not supported at all, the reasoning being that it has a rather crazy API and you should rather use Preg::grep in combination with some loop and Preg::replace. The replace, replaceCallback and replaceCallbackArray methods do not support an array subject, only simple strings.
So the set of things you lose is specific: a flag here, preg_filter there, and array subjects. For most code none of that matters, and for the code where it does, the library points you at a different tool. The README says that if you are looking for a richer API to handle regular expressions, have a look at rawr/t-regx instead, which is the sensible answer for a library whose own scope is a thin wrapper.
The other half of the value is static analysis. The library makes it easier to work with tools like PHPStan or Psalm because it simplifies and reduces the possible return values from all the preg functions, which are quite packed with edge cases. As of version 2.2.0 and 3.2.0, it also comes with a PHPStan extension for parsing regular expressions and giving even better output types, so the analyser understands what your pattern captures rather than treating matches as a flat array of nullable strings.
The repository carries the pieces you would expect for a library with that claim: a phpstan.neon.dist configuration, an extension.neon file for registering the extension, and a phpstan-baseline.neon with the known issues filtered out, plus a phpunit.xml.dist and a tests/ directory.
Editorial conclusion
Adopt composer/pcre if you are a PHP developer who wants static analysis to catch the null returns that preg_match and preg_replace can hand back, since the whole library exists to make those failures visible and typed. Keep away if you need the flag combinations it forbids, because PREG_OFFSET_CAPTURE, PREG_SET_ORDER, preg_filter and array subjects are all rejected by design. Before you migrate, check which of your patterns use optional subpatterns, because the strict group variants are only safe when no group can silently be absent, and note the PHP floor of 7.4 for the 3.x line with 3.4.0 released on 2026-06-11.
Frequently asked questions
What does composer/pcre actually change about PHP's preg functions?
Failures throw a Composer\Pcre\PcreException instead of returning false or null, and the library always sets PREG_UNMATCHED_AS_NULL so unmatched groups are consistently present and null. The return values are therefore narrow: strings for replace, ints for match, arrays for grep and split, which is what makes static analysis practical.
When is it safe to use the StrictGroups methods in composer/pcre?
As long as your pattern has no optional subpatterns, such as a group with a question mark or a star, or branches with a pipe where some groups are never matched. A group that can match an empty string is not optional and will be present as an empty string, and a non-matching group is simply absent, so neither is a problem.
Which preg flags can I not use with composer/pcre?
PREG_OFFSET_CAPTURE cannot be passed to match or matchAll, you use matchWithOffsets and matchAllWithOffsets instead. Preg::split rejects PREG_SPLIT_OFFSET_CAPTURE in favour of splitWithOffsets, matchAll rejects PREG_SET_ORDER, preg_filter is not supported at all, and the replace methods only accept a simple string subject.
What PHP version does composer/pcre 3.x need?
PHP 7.4.0 is required for the 3.x versions, 7.2.0 for 2.x and 5.3.2 for 1.x, according to the README requirements section. The library also ships a PHPStan extension, added in versions 2.2.0 and 3.2.0, which parses your patterns and gives more precise output types.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/composer-pcre)Was this useful?