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.
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.
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.
['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.'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.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.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.implode('|', ...) will fail (object-to-string). If an integration or package flattens rule arrays, keep composites at the outermost layer.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.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.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.How can I help you explore Laravel packages today?