PHPackages                             meritum/validation - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. meritum/validation

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

meritum/validation
==================

Validation library for the Meritum ecosystem

1.0.0(today)00MITPHPPHP ^8.4CI passing

Since Jun 10Pushed todayCompare

[ Source](https://github.com/MeritumIO/validation)[ Packagist](https://packagist.org/packages/meritum/validation)[ RSS](/packages/meritum-validation/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (2)Used By (0)

meritum/validation
==================

[](#meritumvalidation)

Validation library for the Meritum ecosystem. Provides a rule-agnostic engine, a set of 32 default rules, and kernel integration via `ValidationModule`.

Requirements
------------

[](#requirements)

- PHP 8.4+
- `georgeff/kernel` ^1.6

Installation
------------

[](#installation)

```
composer require meritum/validation
```

Usage
-----

[](#usage)

Inject `Validator` and call `validate()` with a schema and input array. The schema maps attribute names to a list of rules. Rules are plain strings; rules with parameters use the attribute name as the key and an array of parameters as the value.

```
use Meritum\Validation\Validator;

$result = $validator->validate(
    [
        'name'     => ['required', 'string', 'lengthMin' => [2], 'lengthMax' => [100]],
        'email'    => ['required', 'email'],
        'age'      => ['optional', 'integer', 'min' => [18]],
        'password' => ['required', 'string', 'lengthMin' => [8]],
        'password_confirmation' => ['required', 'sameAs' => 'password'],
    ],
    $input,
);

if ($result->passed()) {
    // proceed
}

foreach ($result->getErrors() as $attribute => $messages) {
    // $attribute => 'email', $messages => ['The email must be a valid email address']
}
```

### Optionality and nullability

[](#optionality-and-nullability)

RulesMissing`null`Present`['string']`failsfailsvalidated`['required', 'string']`fails (stops)fails (stops)validated`['optional', 'string']`passespassesvalidated`['nullable', 'string']`failspassesvalidated`['nullable', 'required', 'string']`fails (stops)passesvalidated- **`optional`** — field may be absent or null; if present and non-null, remaining rules run
- **`nullable`** — null is explicitly valid; if the value is null, remaining rules are skipped
- **`required`** — field must be present, non-null, and non-empty
- `nullable` + `required` expresses "must be present, but null is acceptable"
- Rule order matters — `optional` and `nullable` must appear before the rules they gate

### Dot notation and wildcards

[](#dot-notation-and-wildcards)

Nested attributes use dot notation. Wildcards validate each element of an array. Both can be combined and nested arbitrarily.

```
$result = $validator->validate(
    [
        'address.city'           => ['required', 'string'],
        'address.postcode'       => ['optional', 'string'],
        'items.*.name'           => ['required', 'string'],
        'items.*.price'          => ['required', 'numeric', 'min' => [0]],
        'items.*.variants.*.sku' => ['required', 'string'],
    ],
    $input,
);
```

Errors for wildcard attributes are keyed by the concrete path — `items.1.name`, `items.0.variants.2.sku`, etc. If the parent is absent or not an array, the wildcard path passes silently — combine with a separate rule on the parent attribute to enforce its presence.

Module registration
-------------------

[](#module-registration)

Register `ValidationModule` with the kernel to wire up the engine and all default rules.

```
use Meritum\Validation\ValidationModule;

$kernel->addModule(new ValidationModule());
```

The module binds `Validator::class` to `ValidationEngine` via `ValidationEngineFactory`, and tags all 32 default rules with `validation.rules`. The factory resolves tagged rules at boot time and spreads them into the engine.

Adding custom rules
-------------------

[](#adding-custom-rules)

Implement `RuleInterface` and register the class with the kernel tagged as `validation.rules`. The rule's `name()` return value is the string used in schemas.

```
use Meritum\Validation\RuleInterface;

final class Slug implements RuleInterface
{
    public function name(): string
    {
        return 'slug';
    }

    public function validate(mixed $value, mixed ...$params): bool
    {
        return is_string($value) && (bool) preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value);
    }

    public function message(string $attribute, mixed ...$params): string
    {
        return "The {$attribute} must be a valid slug";
    }
}
```

```
$kernel->define(Slug::class, fn() => new Slug())->tag('validation.rules');
```

```
$validator->validate(['handle' => ['required', 'slug']], $input);
```

### Overriding a default rule

[](#overriding-a-default-rule)

Register a new class that returns the same `name()` as the rule you want to replace. The last registration wins.

```
final class StrictEmail implements RuleInterface
{
    public function name(): string
    {
        return 'email'; // replaces the default Email rule
    }
    // ...
}

$kernel->define(StrictEmail::class, fn() => new StrictEmail())->tag('validation.rules');
```

### Stoppable rules

[](#stoppable-rules)

Implement `StoppableRuleInterface` to halt validation of remaining rules for an attribute when a condition is met. `shouldPropagationStop()` is called after `validate()` and receives the same value and params.

```
use Meritum\Validation\StoppableRuleInterface;

final class Bail implements StoppableRuleInterface
{
    public function name(): string { return 'bail'; }

    public function validate(mixed $value, mixed ...$params): bool { return true; }

    public function message(string $attribute, mixed ...$params): string { return ''; }

    public function shouldPropagationStop(mixed $value, mixed ...$params): bool
    {
        return true; // always stop — no further rules run after this
    }
}
```

### Field-referencing rules

[](#field-referencing-rules)

Implement `FieldReferencingRuleInterface` for rules that compare a value against another field in the input. `resolveParams()` receives the comparison field name (from the schema) and the full input, and returns the params passed to `validate()` and `message()`.

```
use Meritum\Validation\FieldReferencingRuleInterface;
use Meritum\Validation\Missing;

final class GreaterThan implements FieldReferencingRuleInterface
{
    public function name(): string { return 'greaterThan'; }

    public function resolveParams(string $attribute, array $input): array
    {
        return [$input[$attribute] ?? new Missing(), $attribute];
    }

    public function validate(mixed $value, mixed ...$params): bool
    {
        if ($params[0] instanceof Missing) { return false; }
        return is_numeric($value) && is_numeric($params[0]) && $value > $params[0];
    }

    public function message(string $attribute, mixed ...$params): string
    {
        $field = is_string($params[1]) ? $params[1] : '';
        return "The {$attribute} must be greater than {$field}";
    }
}
```

```
$validator->validate(['end_date' => ['required', 'date', 'greaterThan' => 'start_date']], $input);
```

Default rules
-------------

[](#default-rules)

### Presence and flow

[](#presence-and-flow)

RuleSchemaDescription`required``'required'`Must be present, non-null, and non-empty string`optional``'optional'`Absent or null passes; if present and non-null, remaining rules run`nullable``'nullable'`Null passes and remaining rules are skipped### Type

[](#type)

RuleSchemaDescription`string``'string'`Must be a string`integer``'integer'`Must be an integer`float``'float'`Must be a float (integers do not pass — use `numeric` for "can be cast to number")`boolean``'boolean'`Must be a boolean`array``'array'`Must be an array`numeric``'numeric'`Must be numeric — accepts integers, floats, and numeric strings### String

[](#string)

RuleSchemaDescription`alpha``'alpha'`Letters only (a–z, A–Z)`alphaNum``'alphaNum'`Letters and digits only`email``'email'`Valid email address`url``'url'`Valid URL`uuid``'uuid'`Valid UUID (any version)`ip``'ip'`Valid IPv4 or IPv6 address`ipv4``'ipv4'`Valid IPv4 address`ipv6``'ipv6'`Valid IPv6 address`regex``'regex' => ['/pattern/']`Matches the given regular expression`lengthMin``'lengthMin' => [4]`Minimum string length (multibyte-safe)`lengthMax``'lengthMax' => [255]`Maximum string length (multibyte-safe)`lengthBetween``'lengthBetween' => [4, 255]`Length between min and max inclusive### Numeric

[](#numeric)

RuleSchemaDescription`min``'min' => [0]`Value must be ≥ min`max``'max' => [100]`Value must be ≤ max`between``'between' => [1, 100]`Value must be between min and max inclusive### Comparison

[](#comparison)

RuleSchemaDescription`equals``'equals' => ['value']`Strict equality against a literal value`notEquals``'notEquals' => ['value']`Strict inequality against a literal value`in``'in' => ['a', 'b', 'c']`Value must be in the given list (strict)`notIn``'notIn' => ['a', 'b']`Value must not be in the given list (strict)### Field comparison

[](#field-comparison)

RuleSchemaDescription`sameAs``'sameAs' => 'other_field'`Must strictly equal another field's value`differentFrom``'differentFrom' => 'other_field'`Must strictly differ from another field's value### Date

[](#date)

RuleSchemaDescription`date``'date'`Valid calendar date string or `DateTimeInterface`; rejects relative strings like "next Tuesday"`dateFormat``'dateFormat' => ['Y-m-d']`Date string matching the given `date()` format exactlyLicense
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/c8147ce1d901e9fa2ec95d6408e5862025211f9ae60131e41fb115a5a0916ce4?d=identicon)[georgeff](/maintainers/georgeff)

---

Top Contributors

[![MikeGeorgeff](https://avatars.githubusercontent.com/u/6169468?v=4)](https://github.com/MikeGeorgeff "MikeGeorgeff (1 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/meritum-validation/health.svg)

```
[![Health](https://phpackages.com/badges/meritum-validation/health.svg)](https://phpackages.com/packages/meritum-validation)
```

###  Alternatives

[ziming/laravel-zxcvbn

Zxcvbn Password validation rule for Laravel

3064.3k](/packages/ziming-laravel-zxcvbn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
