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

Reverse Regex Laravel Package

ilario-pierbattista/reverse-regex

ReverseRegex generates random text strings that match a given regular expression, making it easy to create test data for web forms, databases, and regex testing. A fork of icomefromthenet/reverse-regex with lexer, parser, and scope-based generation.

View on GitHub
Deep Wiki
Context7

Getting Started

ReverseRegex inverts the usual regex flow: instead of matching a string, it builds one that matches. For Laravel work that means you stop hand-writing plausible-looking '0412 345 678' strings in factories and let the pattern itself be the spec.

Install (PHP 8.1+ is a hard requirement since 0.6.0; 0.8.0 keeps the 8.1/8.5 matrix):

composer require ilario-pierbattista/reverse-regex

0.8.0 widened the runtime dependency ranges (doctrine/lexer ^3, doctrine/collections ^2|^3), so on a modern Laravel app you are less likely to hit a version conflict than you were on 0.6.x. Previously supported versions still work.

Minimal working call — three objects, one pipeline:

use ReverseRegex\Generator\Scope;
use ReverseRegex\Lexer;
use ReverseRegex\Parser;
use ReverseRegex\Random\SimpleRandom;

$lexer  = new Lexer('[a-z]{10}');
$parser = new Parser($lexer, new Scope(), new Scope());
$random = new SimpleRandom(10007);   // seed → deterministic

$result = '';
$parser->parse()->getResult()->generate($result, $random);

echo $result;   // e.g. "jmceohykoa"

Note the signature: generate() takes the output variable by reference and appends to it, so always pass a freshly initialised ''. As of 0.8.0 a null result is normalised to an empty string, so null no longer corrupts the buffer — but '' is still the clearer idiom and keeps static analysis happy.

Where to look first

  1. The "Regex Support" table in README.md — this is your real API surface. Read it before writing any pattern; it tells you what the lexer will and won't accept (\p{...} and lookarounds are out, \X{####} and \xFF ranges are in).
  2. examples/ — ausphone.php, auspostcode.php, mobilenumbers.php. These are the closest thing to real-world patterns, double as smoke tests for your local install, and are now executed in the package's CI, so they stay trustworthy.
  3. src/ReverseRegex/ — Lexer → Parser/* strategies → Generator/Scope tree, plus Random/ for the RNG. You rarely touch this, but knowing the parse tree exists helps enormously when a pattern silently produces nothing. The PSR-0 → PSR-4 switch in 0.8.0 preserved namespaces and layout, so this path is unchanged.
  4. The tests are no longer shipped in the package archive (0.8.0 moved them from src/ReverseRegex/Test/ to a root tests/ directory and excluded them from the archive). If you want the authoritative, pinned-down answer on edge cases like \W and \S, clone the GitHub repo rather than looking in vendor/.

First use case worth wiring up: a single factory field whose validation rule you already have, e.g. an Australian mobile number. Replace the hardcoded sample with a generated one and you immediately get variety without invalidity — the thing Faker can't do for bespoke patterns.

Implementation Patterns

1. Wrap it once, inject everywhere

Do not scatter new Lexer(...) through your test suite. One small service, bound in the container, pays for itself:

namespace App\Support;

use ReverseRegex\Generator\Scope;
use ReverseRegex\Lexer;
use ReverseRegex\Parser;
use ReverseRegex\Random\SimpleRandom;

final class RegexFaker
{
    /** @return list<string> */
    public function generate(string $pattern, int $count = 1, ?int $seed = null): array
    {
        $out = [];

        for ($i = 0; $i < $count; $i++) {
            $random = new SimpleRandom($seed === null ? random_int(1, PHP_INT_MAX) : $seed + $i);

            $buffer = '';
            (new Parser(new Lexer($pattern), new Scope(), new Scope()))
                ->parse()
                ->getResult()
                ->generate($buffer, $random);

            $out[] = $buffer;
        }

        return $out;
    }

    public function one(string $pattern, ?int $seed = null): string
    {
        return $this->generate($pattern, 1, $seed)[0];
    }
}
// AppServiceProvider::register()
$this->app->singleton(RegexFaker::class);

Because the package has effectively one maintainer and 0 dependents, this wrapper is also your escape hatch: if you ever swap to a hand-rolled generator or a different fork, only one class changes.

2. Model factories — regex where it matters, Faker everywhere else

Don't try to replace Faker. Use the pattern-driven generator for the constrained fields and leave names, addresses and prose to Faker:

public function definition(): array
{
    return [
        'name'         => fake()->name(),
        'mobile'       => app(RegexFaker::class)->one('\0?4[0-9]{2} ?[0-9]{3} ?[0-9]{3}'),
        'postcode'     => app(RegexFaker::class)->one('[0-9]{4}'),
        'external_ref' => app(RegexFaker::class)->one('REF-[A-Z]{3}[0-9]{6}'),
    ];
}

The win is that the regex is the same rule you wrote in FormRequest validation, copied verbatim. When the rule changes, the factory changes with it and your tests stop drifting.

3. Seeded determinism for CI

SimpleRandom accepts a seed, which is the whole reason this package is safe to use in a regression suite. Pick a pattern:

// Pest beforeEach or a PHPUnit setUp()
$this->seed = 424242;
$phone = app(RegexFaker::class)->one('[0-9]{10}', $this->seed);

Or, better, derive the seed from the test name so each test is stable but distinct:

$seed = crc32($this->name());

Never seed from time() and never assume unseeded output is stable — it isn't, and that's exactly the flakiness this package lets you avoid.

4. Validation tests: generate pairs, not singles

The highest-leverage pattern is asserting the contract in both directions inside one test:

it('accepts and rejects postcodes correctly', function () {
    $faker = app(RegexFaker::class);

    foreach ($faker->generate('[0-9]{4}', 50, 999) as $valid) {
        expect(Validator::make(['pc' => $valid], ['pc' => 'regex:/^[0-9]{4}$/'])->passes())->toBeTrue();
    }

    foreach (['123', '12345', 'ABCD', ''] as $invalid) {
        expect(Validator::make(['pc' => $invalid], ['pc' => 'regex:/^[0-9]{4}$/'])->fails())->toBeTrue();
    }
});

A Pest dataset works too, generated at collection time:

dataset('valid_mobiles', fn () => app(RegexFaker::class)->generate('\0?4[0-9]{2}[0-9]{3}[0-9]{3}', 25, 7));

Pair each generated string with a preg_match sanity assertion (expect($value)->toMatch('/^...$/')) — that catches the case where you wrote a pattern ReverseRegex accepts but PCRE interprets differently.

5. Seeders and bulk payload generation

For database seeding or API load simulation, generate in bulk from an Artisan command and chunk the inserts:

$rows = collect(app(RegexFaker::class)->generate('[A-Z]{2}-[0-9]{8}', 5000, 1))
    ->map(fn (string $ref) => ['reference' => $ref, 'created_at' => now()])
    ->chunk(500);

foreach ($rows as $chunk) {
    DB::table('transactions')->insert($chunk->values()->all());
}

Keep the chunk(); the generation loop is cheap but a 5k-row single insert is not.

6. Localisation / Unicode strings

For internationalised forms where Faker gives you nothing useful, use explicit codepoint ranges:

$greek = $faker->one('[\X{03B1}-\X{03C9}]{8}');   // α–ω
$emoji = $faker->one('\X{1F600}');                // 😀
$hex   = $faker->one('[\xFF-\xFF]');

This is the package's genuine differentiator over naive str_shuffle-style helpers — but see the gotchas about \p.

Gotchas and Tips

Upgrading from 0.6.x to 0.8.0 is low-risk unless you subclass. Native return types were added to Scope, Node::contains(), Parser and the Parser\Quantifier/Parser\Unicode strategies, and the Parser and Quantifier properties are now typed. Public interfaces are unchanged, so normal usage (constructing Lexer/Parser/Scope and calling generate()) needs no edits. But if you extended any of those classes — a common trick for a custom output constraint — you must add compatible return types or PHP will fatal. Grep your app for extends Scope, extends Parser and extends Node before bumping.

generate() appends, and the buffer can hold more than one result. The README example emits a dozen strings for a single [a-z]{10} pattern. Always pass a fresh '', and after generating, assert what you got (strlen, preg_match) rather than assuming exactly one match. If you get a concatenation of several candidates, that's the Scope tree returning multiple branches — trim or re-parse per call. The 0.8.0 null-normalisation fix means an abandoned branch no longer poisons the buffer with a null, but it does not guarantee one result per call.

Quantifier binding is the #1 source of wrong output. ab{5} may not mean what you think; the docs are inconsistent about whether quantifiers bind to the "left most" or "last" token. When in doubt, group explicitly: (ab){5}. Always verify a new pattern by generating 20 samples and eyeballing them before committing it to a factory.

+ and * expand to a possible maximum of PHP_INT_MAX. Even with the 0.6.0 fix that lets SimpleRandom cope with unbounded quantifiers, an unbounded quantifier in a pattern used for bulk seeding is a memory hazard. Rewrite intent as bounded: [a-z]+ → [a-z]{1,20}. If you genuinely need a long tail, cap it yourself in the pattern.

No \p{...}, no lookarounds, no backreferences, no conditionals. This is a tree-walking generator, not a PCRE engine. If your validation rule uses \p{L} or (?=...), ReverseRegex will either reject the pattern or silently mis-generate. Fall back to a \X{####} codepoint range or keep that field on Faker/manual fixtures.

\W and \S deserve suspicion. The README's own table shows \W yielding j — a word character — for a negated class. Generate a batch of 50 and verify the negated shorthands behave as advertised before relying on them; if they don't, spell the class out ([^a-zA-Z0-9_]).

. is documented as "all ASCII characters". That likely includes control characters and newlines, which will corrupt CSV fixtures and log lines. Prefer an explicit class ([ -~]) over . in anything that ends up in a file or a request body.

Escape meta-characters exactly as you would in PCRE. Need a literal (? Write \(. This is the most common cause of a Lexer exception on first use — the pattern works fine in preg_match because that context is different, or vice versa.

Seeds are deterministic but not cryptographic. SimpleRandom is fine for fixtures and load tests. Never reuse it to generate real tokens, invite codes, or anything that ships to production users. Use Str::random() / random_int() for that.

Reuse the parse or re-parse — measure before choosing. Building a Parser per string is simple and obviously correct. If you're generating tens of thousands of rows, try hoisting parse()->getResult() out of the loop and calling generate() repeatedly on the same result object; if output repeats or degrades, go back to re-parsing. Don't optimise this speculatively.

A silent empty string means the pattern didn't parse into anything usable. Debug by dumping the tree rather than guessing:

dump((new Parser(new Lexer($pattern), new Scope(), new Scope()))->parse()->getResult());

Then reduce the pattern to a minimal fragment that does generate, and add pieces back. The examples/ files are good known-good starting points to diff against — and since 0.8.0 runs them in CI, they are a better reference than anything in the package archive now that the tests are excluded from it.

Pin the version in composer.json. This is a 3-star fork with a single active maintainer and zero dependents — it will not be fixed quickly if a future PHP release breaks it. 0.8.0 is a modest confidence signal (PSR-4, strict_types=1, PHPStan level 6 with no baseline, 90% coverage minimum, Dependabot, an 8.1/8.5 matrix), but it is still one person's project. A minor constraint (^0.8) plus your RegexFaker wrapper means an upstream stall is an afternoon of work, not a rewrite. Note the package has no SemVer stability guarantee in practice: 0.6.0 and 0.8.0 both shipped behaviour changes in a "minor" bump.

The package's own tooling (PHPStan level 6, Facile.it CS, Composer scripts) does not apply to your app. They're development dependencies of the package, not transitive code quality for your project. Use them as a reference for how you could configure phpstan in your own repo, but don't expect them to run from your Laravel install. Likewise, strict_types=1 in the package source governs the package's internal calls only — it does not change how your (non-strict) files call into it.

Extension point: swap the RNG. The generators take a random source object, so you can implement your own wrapper around random_int() (or a recorded/replayable fake for snapshot testing) and pass it where SimpleRandom goes. 0.8.0's docblock tightening on ContextInterface::generate() (declaring the by-reference $result as string|null in and string out) makes a custom implementation friendlier to Psalm/PHPStan. This is the cheapest way to get reproducible fixtures without changing any pattern code — and it's the extension point that did not break in 0.8.0.

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.
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode