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

Human Regex Laravel Package

mpociot/human-regex

Human Regex lets you build regular expressions in PHP with readable, fluent method calls instead of cryptic syntax. Chain start(), then(), capture(), and end() to compose patterns that are easier to write, review, and maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

Install into a branch, not main.

composer require mpociot/human-regex

This is a single-maintainer package with zero dependents. Nothing upstream will catch a breakage for you — pin the exact version in composer.lock and note it in your dependency risk register the same day. No service provider, no config file, no migrations: the footprint is one directory under vendor/.

Read the source before you trust the README. vendor/mpociot/human-regex/src is small enough to finish in fifteen minutes. You need three answers:

  1. The full method vocabulary (the README is a worked-examples list, not a specification — the source is the authority).
  2. Whether a raw()/passthrough escape hatch exists.
  3. What ->get() returns: a delimited pattern (/^...$/) or a bare body. This determines your entire calling convention.

Round-trip one chain in tinker before writing anything real.

$pattern = HumanRegex::create()
    ->startOfLine()
    ->then('hello')
    ->whitespace()
    ->endOfLine()
    ->get();

dump($pattern);                      // inspect the compiled string, not the chain
preg_match($pattern, 'hello world'); // confirm the delimiter/flag contract

If ->get() hands back an undelimited body, you own delimiter choice at every call site — decide that once, in a wrapper, before it spreads. Also check the package's PHP constraint against your actual runtime rather than the README's claim.

Look at your own code first. Skip the README's examples and grep app/ and tests/ for preg_match, preg_replace, and regex:. Pick the three ugliest patterns you already own: one Laravel regex validation rule, one log-line parser, one extraction with a capture group. Those three are your whole evaluation. If fewer than two convert cleanly, stop here — that's your answer and it cost you an afternoon.

First real use case: a FormRequest rule. Lowest blast radius, most reviewers, easiest to sanity-check without context:

// Before
'postal_code' => ['required', 'regex:/^[A-Z]{2}[0-9]{4}$/'],

// After
'postal_code' => ['required', 'regex:' . Pattern::postalCode()],

Ship it with a test asserting both the compiled string and positive/negative fixtures. If the chain and the original don't produce equivalent ->get() output, stop and find out why before merging — that diff is the exercise.

Implementation Patterns

The wrapper is the pattern; everything else is a detail. Put the package behind one class and make it the only import site in the codebase:

namespace App\Support;

use HumanRegex;

final class Pattern
{
    public static function postalCode(): string
    {
        return HumanRegex::create()
            ->startOfLine()
            ->exactly(2)->uppercaseLetters()   // verify exact method names against vendor source
            ->exactly(4)->digits()
            ->endOfLine()
            ->get();
    }
}

This turns a dormant third-party dependency into an internal seam. If the package is abandoned, the cost is "edit one class," not "migration project." Nobody outside App\Support should ever use HumanRegex — enforce that with a code-review convention or a PHPStan rule, not hope.

Always name the pattern. Pattern::postalCode() reads better at the call site than the chain does, is greppable, and gives you a single place to change delimiters or add the u flag. Anonymous inline chains in a controller defeat the entire purpose.

Two tests per pattern, both cheap.

it('compiles the postal code pattern deterministically', function () {
    expect(Pattern::postalCode())->toBe('/^[A-Z]{2}[0-9]{4}$/');
});

it('matches valid postal codes', function () {
    expect(preg_match(Pattern::postalCode(), 'AB1234'))->toBe(1);
    expect(preg_match(Pattern::postalCode(), 'ab1234'))->toBe(0);
    expect(preg_match(Pattern::postalCode(), 'AB12345'))->toBe(0);
});

The string assertion catches a silent package upgrade or a refactor that changed quantifier binding. The fixture test catches a chain that reads correctly and matches too broadly. You need both — the string test alone will happily lock in a wrong pattern.

Extraction with capture groups. Where the DSL supports grouping, use it for parsers; where it doesn't, take the escape hatch rather than contorting the chain:

// 2024-05-01 12:00:00 ERROR Something broke
$pattern = HumanRegex::create()
    ->startOfLine()
    ->group(fn ($g) => $g->digits(4)->then('-')->digits(2)->then('-')->digits(2))
    ->whitespace()
    ->then('ERROR')
    ->whitespace()
    ->group(fn ($g) => $g->oneOrMore()->anything())  // verify semantics against source
    ->get();

preg_match($pattern, $line, $m);

Verify that oneOrMore()->anything() does not match the empty string when you need "at least one character." That single mistake is the most common way a readable chain silently becomes a permissive pattern.

Keep a raw() seam in the wrapper. Even if the upstream passthrough composes awkwardly, expose your own:

public static function raw(string $pattern): string
{
    return $pattern;
}

A mixed vocabulary then becomes a deliberate, visible decision rather than an accident — and moving off the package entirely is a reimplementation of methods that already have call sites.

Laravel integration points, ordered by payoff:

  • FormRequest validation — highest value, lowest risk, most readers. Convert opportunistically as you touch validators.
  • Str:: helpers and preg_replace scrubbers in service classes — second-best, because the pattern sits next to the business rule it encodes.
  • Console commands and importers (CSV, log ingestion, fixture loaders) — the "only one person can read this" problem is worst here, since these scripts are touched rarely and by whoever is on call.
  • Seeders and factories — free wins; nobody argues about readability in a seeder.

Rollout sequence: wrapper + contract tests → new code only → opportunistic backfill when you touch a file for other reasons. Do not schedule a rewrite; that converts a two-week pilot into a project and burns the goodwill that made the pilot cheap to approve in the first place.

Gotchas and Tips

Readability is not safety, and this package makes the confusion easier. A fluent chain looks reviewed. oneOrMore()->anything() still backtracks catastrophically, and nesting quantifiers is exactly as dangerous as it was in hand-written regex. Generated patterns get the same review, the same input-length guard, and the same ReDoS scrutiny as anything hand-written. Add preg_last_error() checks — or a set_time_limit guard — anywhere user input reaches a compiled pattern in a request path.

Never approve a chain without the compiled output. Require the ->get() string in the PR description for every converted pattern. Without this discipline you have swapped opaque regex for opaque method chains and gained precisely nothing. Make it a review-checklist item, not a suggestion.

ASCII semantics. letters() and digits() are almost certainly ASCII-oriented. If a field accepts names, addresses, or any free text — anything not a machine-generated code — assume the DSL is wrong and use your raw seam with explicit Unicode property classes. Confirm u modifier handling before trusting any non-ASCII test.

Quantifier binding is the subtle one. Does ->maybe() bind to the last token or the last group? Does ->oneOrMore() after a group quantify the group or the preceding literal? Get this wrong and the pattern compiles, matches something, and fails in production on an edge case your fixtures missed. Write the fixture set before you write the chain — it forces you to answer the question explicitly rather than by assumption.

The DSL has a hard ceiling. No lookarounds, no backreferences, no conditionals, no named captures, no atomic groups. Plan for a subset of patterns to stay hand-written; treat "inexpressible" as an expected result, not a failure. If more than roughly a third of your sampled patterns don't fit, that's your kill signal — you'd end up maintaining two regex vocabularies with neither authoritative.

User-supplied text in ->then(). Confirm whether strings passed to the builder are preg_quote-escaped or interpolated literally. If literal, never interpolate a variable into a chain — build with the raw seam and preg_quote($input, '/') yourself. Getting this wrong is a regex-injection bug with a completely innocuous-looking call site.

Delimiter and escaping ownership. ->get() may return a body rather than a complete pattern. If so, every call site decides the delimiter, and a / inside a URL-matching pattern blows up at runtime inside a conditional — the worst possible failure mode. Decide once in the wrapper, and add a test that a pattern containing a slash compiles and matches.

Free-spacing (x modifier) is the real competition. PCRE's x flag plus named groups gives you most of the readability with zero dependencies. If your first reaction to a converted pattern is "this isn't much shorter than the regex," that's genuine signal. The package earns its place on semantics — explicit quantifier binding, self-documenting vocabulary — not on line count.

Performance is a non-issue; construction overhead isn't zero. Building is string concatenation and compiled patterns land in PCRE's own cache. But don't construct chains inside tight loops — hoist them into Pattern static methods so you build once and reuse the string.

Debugging a chain that won't match. Print ->get() first, always. Feed the string to a PCRE tester or preg_match in tinker against the actual failing input. You never debug the chain; you debug the compiled string. If the string looks correct and matching still fails, suspect the delimiter and flags before you suspect the DSL.

Own the documentation. There is no vendor, no SLA, and the README is a worked-examples format rather than a spec. Copy the handful of patterns you actually standardize on into your own onboarding docs and name a single owner for App\Support\Pattern. Otherwise the wrapper becomes an orphan, the next developer reads the chain, doesn't trust it, and writes a fresh hand-rolled regex beside it — which leaves you strictly worse off than when you started.

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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle