Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pcre Laravel Package

composer/pcre

Type-safe wrappers around PHP's preg_* functions that fail loudly instead of returning unexpected nulls. Enforces PREG_UNMATCHED_AS_NULL, simplifies return types for PHPStan and Psalm, and includes a PHPStan extension for better regex analysis.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it explicitly, even though it usually shows up in vendor/ already as a transitive dependency of composer/composer (which is typically a require-dev or global install, so it will not be there in a trimmed production build):

composer require composer/pcre

Where to look first

  1. src/Preg.php — a static, drop-in replacement for every preg_* call you already write. This is where 90% of your day-to-day usage lives.
  2. src/Regex.php + src/MatchResult.php, MatchAllResult.php, ReplaceResult.php — an alternative, result-object API for code where by-ref $matches arguments feel clunky.
  3. extension.neon — the PHPStan/Larastan extension. Read this early if you run static analysis; it's the single biggest reason to adopt the package in a Laravel app.

First use case

Find one preg_match in your app — a custom validation rule, a route/slug parser, a log-line parser in an artisan command — and convert it:

// before
if (preg_match('/^(\d{4})-(\d{2})$/', $value, $m)) {
    [$year, $month] = [$m[1], $m[2]];
}

// after
use Composer\Pcre\Preg;

if (Preg::isMatch('/^(\d{4})-(\d{2})$/', $value, $m)) {
    [$year, $month] = [$m[1], $m[2]];
}

The two immediate wins: isMatch returns a real bool (no more === 1), and a malformed pattern throws Composer\Pcre\PcreException instead of returning false/null and silently falling through your if.

Implementation Patterns

The mechanical swap. These are the one-to-one mappings you can apply across a codebase:

Native Preg::
preg_match match (int) / isMatch (bool)
preg_match + PREG_OFFSET_CAPTURE matchWithOffsets
preg_match_all matchAll / isMatchAll
preg_replace replace
preg_replace_callback replaceCallback
preg_replace_callback_array replaceCallbackArray
preg_grep grep
preg_split split / splitWithOffsets

Custom validation rules. This is the highest-value Laravel integration, because validation rules are exactly where a failed regex silently degrades into "input accepted":

class IsIsbn extends \Illuminate\Contracts\Validation\ValidationRule
{
    public function validate(string $attribute, mixed $value, \Closure $fail): void
    {
        if (!is_string($value) || !Preg::isMatch('/^\d{13}$/', $value)) {
            $fail("The {$attribute} is not a valid ISBN.");
        }
    }
}

Because the return is bool, there's no === 1 noise and PHPStan/Larastan can narrow $value correctly.

Result objects in services. When a method needs to return "did it match, and what did we get", Regex reads better than a by-ref out-parameter leaking through a method signature:

use Composer\Pcre\Regex;

$result = Regex::match('/^(?<name>[^<]+) <(?<email>[^>]+)>$/', $header);

if ($result->matched) {
    return new Contact($result->matches['name'], $result->matches['email']);
}

Because of PREG_UNMATCHED_AS_NULL, $result->matches always contains every named and numbered group — null for unmatched, string for matched. Named groups make this a genuinely pleasant way to parse fixed-format strings.

Strict groups for parser-shaped code. When you know the pattern has no optional branches, use matchStrictGroups / matchAllStrictGroups and you get a plain array<string|int, string> with no nullable members:

// Guaranteed strings for group 1 and 2, or an exception
Preg::matchStrictGroups('/^(\w+)@(\w+\.\w+)$/', $email, $m);

Filtering arrays. Preg::grep is a clean replacement for array_filter + preg_match, and is the documented workaround for the unsupported preg_filter:

$errorLines = Preg::grep('/^\[error\]/', $logLines);

Note it preserves keys (see gotchas), so array_values() before handing to Inertia/JSON.

Templating and normalisation with callbacks. replaceCallback is ideal for placeholder substitution and slugging where preg_replace with backreferences gets unreadable:

$slug = Preg::replace('~[^a-z0-9]+~i', '-', trim(Str::ascii($title)));
$out  = Preg::replaceCallback('/\{\{(\w+)\}\}/', fn ($m) => e($data[$m[1]] ?? ''), $template);

Handle exceptions at the boundary. PcreException is a runtime error, not a validation result. Catch it in your controller/middleware/form request and convert it into a 422 or a ValidationException, otherwise a user-supplied pattern (or a pattern from a DB/config value) becomes a 500:

try {
    $count = Preg::matchAll($dynamicPattern, $subject, $matches);
} catch (\Composer\Pcre\PcreException $e) {
    throw ValidationException::withMessages(['pattern' => 'Invalid pattern supplied.']);
}

Turn on the PHPStan extension with Larastan. If you don't use phpstan/extension-installer, add to phpstan.neon:

includes:
    - vendor/composer/pcre/extension.neon

With Larastan this gives you inferred $matches array shapes and, importantly, static validation of literal regexes — a typo like /(foo/ becomes a build failure rather than a production PcreException.

Gotchas and Tips

PREG_UNMATCHED_AS_NULL is always on (2.0+, including replaceCallback in 3.0+). This is the one that breaks existing code during migration. Given /(a)(b)*(c)(d)*/ against "ac":

  • Native PHP: [0 => 'ac', 1 => 'a', 2 => '', 3 => 'c'] — group 2 is '' whether it matched empty or nothing, and group 4 is absent entirely.
  • composer/pcre: [0 => 'ac', 1 => 'a', 2 => null, 3 => 'c', 4 => null].

So every isset($m[4]) check in your codebase is now wrong (the key always exists), and every $m[2] === '' check is now wrong too. Audit conversions for isset() on $matches and swap to !== null. The offset-based matchWithOffsets shape is [string, int]|null per group, not the flat native shape — destructuring code needs adjusting.

*StrictGroups throws on optional groups. It's safe when every subpattern is mandatory or can match empty ((.*) is fine — it yields ''). It will throw UnexpectedNullMatchException (a PcreException) the moment a (foo)? / (a|b) branch doesn't participate. Never use strict variants with user-supplied patterns; they're for patterns you own and have reasoned about.

Flags are gated by method, not accepted silently. match/matchAll reject PREG_OFFSET_CAPTURE — use matchWithOffsets/matchAllWithOffsets. split rejects PREG_SPLIT_OFFSET_CAPTURE — use splitWithOffsets. matchAll rejects PREG_SET_ORDER, and there is no replacement, so you must reshape the result yourself (transpose the matchAll output). PREG_SPLIT_NO_EMPTY and PREG_SPLIT_DELIM_CAPTURE are fine.

replace/replaceCallback refuse array subjects. Native preg_replace accepts string|array and returns the same shape, which is convenient for bulk-cleansing a collection of strings. composer/pcre deliberately narrows this to a single string. Use array_map with Preg::replace, or ->map(fn ($s) => Preg::replace(...)) on a collection.

preg_filter is not supported at all — its "return matches only, keep keys, null on no match" API isn't typeable. Compose Preg::grep + Preg::replace instead.

Preg::grep preserves keys. preg_grep semantics are retained. If the result goes into a JSON response, an Inertia prop, or implode, wrap in array_values() — otherwise you get a JSON object instead of an array.

Exceptions where you didn't expect them. Backtrack/recursion limit breaches and invalid UTF-8 subjects both throw now instead of returning false. On user-controlled input (search boxes, import files, uploaded CSVs) this is a real 500 risk. Either validate patterns at configuration time, or wrap the call site.

Pattern strings from config/database should be validated once at boot, e.g. in a service provider or a test asserting Preg::isMatch($pattern, '') doesn't throw. PcreException::getMessage() includes the PCRE error, but the pattern itself is the useful context — log it alongside.

Version pinning matters. 3.x requires PHP 7.4+ and applies PREG_UNMATCHED_AS_NULL to replaceCallback; 2.x does not. If you share regex helper code between a Laravel app and an older package, the callback $matches shape differs between majors. Also note 3.4.0 dropped PHPStan 1.x support — if a package in your graph pins PHPStan 1.x, pin composer/pcre below 3.4.

Don't force it for everything. It's a thin wrapper with no pattern caching, no named-group extraction sugar, and no "find and replace all with a builder" API. If you need readable composition, named-pattern extraction, or a fluent API, rawr/t-regx is the intended escape hatch per the README. For a handful of preg_match calls, the honest answer is often to leave them alone — adopt composer/pcre where the null return actually caused (or could cause) a bug, plus wherever you want the Larastan type narrowing.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel