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

Validation Composite Laravel Package

illuminatech/validation-composite

Illuminatech Validation Composite lets Laravel developers build custom validation rules by composing existing rules into one reusable rule. Define nested rules and messages, keep validation logic modular, and use the result like any standard Laravel validation rule.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package via Composer:

composer require illuminatech/validation-composite

There is no service provider or config to publish — it is a thin library of abstract validation classes. The first place to look is the Illuminatech\Validation\Composite namespace (specifically CompositeRule and the dynamic-rule variant). Skim the source of CompositeRule once: it is small and shows exactly how the internal Validator is wired up.

First use case — consolidate a field's rules that you keep copy-pasting. Instead of repeating required|string|min:3|max:64|regex:/^[a-z0-9_-]+$/ across FormRequests, create a rule class:

namespace App\Rules;

use Illuminatech\Validation\Composite\CompositeRule;

class UsernameRule extends CompositeRule
{
    public function rules(): array
    {
        return [
            'required',
            'string',
            'min:3',
            'max:64',
            'regex:/^[a-z0-9_-]+$/',
            'unique:users,username',
        ];
    }
}

Use it anywhere Laravel accepts a rule — FormRequest, Validator::make, $request->validate():

$validator = Validator::make($request->all(), [
    'username' => [new UsernameRule()],
]);

That's the entire onboarding curve. Everything else is variations on parameterizing and composing these classes.

Implementation Patterns

1. Reuse across a codebase. Keep composite rules in app/Rules (or a dedicated Validation namespace) and reference them by class name. This is the primary payoff: the rule definition lives in one place, and changing a policy (e.g. bumping max:64 to max:32) updates every consumer.

2. Parameterized rules. When the same shape needs different bounds, take constructor arguments and inject them into the rule strings:

class PriceRule extends CompositeDynamicRule
{
    public function __construct(protected float $min = 0, protected float $max = 100000)
    {
    }

    public function rules(): array
    {
        return [
            'required',
            'numeric',
            "between:{$this->min},{$this->max}",
            'decimal:0,2',
        ];
    }
}

Usage: 'price' => [new PriceRule(10, 500)].

3. Nesting composites. Composites compose. A PhoneRule can be embedded inside a broader ContactRule:

public function rules(): array
{
    return [
        'nullable',
        new PhoneRule(),
        new CountryCodeRule(),
    ];
}

Nesting keeps rules granular and testable without duplicating the inner logic.

4. Custom messages and attribute names. Override messages() / attributes() on the composite to give domain wording, using the underlying rule keys (regex, unique, etc.):

public function messages(): array
{
    return [
        'regex' => 'Usernames may only contain lowercase letters, digits, dashes and underscores.',
        'unique' => 'That username is already taken.',
    ];
}

5. Inline reuse in $request->validate(). Composites work in the array-syntax of validate() and in Rule:: chains, so they slot naturally into controllers that use short-form validation. The composite instance is passed by reference — no need to serialize to a string.

6. Data-aware behavior. For cross-field rules (e.g. "must match password_confirmation"), either keep the comparison rule at the parent level and only bundle the field-local ones in the composite, or implement DataAwareRule on the composite. Avoid hiding cross-field dependencies inside a composite — it makes the outer FormRequest harder to reason about.

7. Composition with bail and sometimes. bail outside a composite still aborts the whole chain on the first failure inside the composite, which is usually what you want. Use sometimes/nullable at the call site rather than baking them into a widely reused composite — presence requirements vary by form.

8. Testing. Treat each composite as a unit. Instantiate it with Validator::make(['field' => $value], ['field' => [new TheRule()]]) and assert passes()/fails() and the message bag. This gives fast, isolated coverage without booting a HTTP request.

Gotchas and Tips

  • Order matters just like normal rules. ['required', 'string', ...] still short-circuits on empty values before string runs. If a composite is used on an optional field, required inside it will silently break the call site's intent — parameterize presence instead of hard-coding it.
  • Message keys use the underlying rule name, not the composite class. 'UsernameRule' => '...' will not match; use 'regex', 'unique', etc. (or namespace them with the attribute, e.g. 'username.unique'), which matters when two composites share the same field.
  • Don't return closures from rules() if anything caches or serializes. Composite instances are objects; if you cache validation config or serialize the rules array, closure rules break. Stick to string rules and rule-object instances.
  • Nested composites produce a sub-Validator each time. That's fine for normal request counts, but avoid instantiating the same composite in a loop over thousands of rows — hoist the instance or the string rule instead.
  • A composite is not a closure and not a string. Passing it into a config array that later gets implode('|', ...) will fail (object-to-string). If an integration or package flattens rule arrays, keep composites at the outermost layer.
  • Debugging: dd((new UsernameRule())->rules()) is the fastest way to confirm what is actually being run. When a composite fails "mysteriously," get the inner Validator's MessageBag — the composite forwards it, but check that $validator->errors()->first('field') isn't shadowed by a same-named rule at the parent level.
  • Extension points: override rules(), messages(), attributes(), or passes(). If you need request-scoped context (the current user, a tenant id) inside unique:..., inject it via the constructor (Rule::unique('users')->where(...)) rather than reaching into request() from inside the composite — it makes the class unit-testable.
  • Version pinning: the package is small and lightly maintained (NOASSERTION license), so pin the major version in composer.json and run Laravel's own test suite on upgrade. The abstract class is stable API, but internal validator plumbing can track Laravel changes.
  • Prefer Laravel's built-ins when they suffice. If your rule is a single string, a composite class adds indirection without benefit. Reach for composites when a field genuinely has 3+ rules or when the rule is used in 3+ places — that is where the package earns its keep.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console