PHPackages                             meraki/form - 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. meraki/form

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

meraki/form
===========

A library that provides a flexible, UI-agnostic solution for defining, validating, and serializing form schemas that can be used across multiple rendering environments and formats.

v1.12.0-alpha(2d ago)00MITPHPPHP ^8.2

Since Sep 21Pushed 3w ago1 watchersCompare

[ Source](https://github.com/merakiframework/schema)[ Packagist](https://packagist.org/packages/meraki/form)[ RSS](/packages/meraki-form/feed)WikiDiscussions main Synced yesterday

READMEChangelog (8)Dependencies (7)Versions (15)Used By (0)

meraki/schema
=============

[](#merakischema)

A flexible, UI-agnostic library for **defining and validating** form schemas in PHP.

You describe a form once — its fields, their constraints, and the rules that wire fields together — and the schema validates input against that description. The core package is deliberately focused: it knows nothing about HTTP, HTML, or JSON. Those concerns live in sibling packages so the domain stays small and stable:

PackageResponsibility[`meraki/schema`](https://github.com/merakiframework/schema)Define + validate schemas (this package)[`meraki/schema-json`](https://github.com/merakiframework/schema-json)JSON serialization / deserialization[`meraki/schema-html`](https://github.com/merakiframework/schema-html)Render a schema as an HTML form + normalize request inputRequirements
------------

[](#requirements)

- PHP 8.4+

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

[](#installation)

```
composer require meraki/schema
```

Quick start
-----------

[](#quick-start)

```
use Meraki\Schema\Facade;

$schema = new Facade('contact_form');

$schema->addTextField('username')
    ->matches('/^[a-zA-Z0-9_]+$/')
    ->minLengthOf(3)
    ->maxLengthOf(20);

$schema->addNumberField('age')
    ->minOf(18)
    ->maxOf(120);

$result = $schema->validate([
    'username' => 'johndoe',
    'age'      => 25,
]);

if (!$result->anyFailed()) {
    // safe to proceed
}
```

`Facade` is the entry point. Each `addXField()` method appends a field and, when called without a configurator, returns the field itself so constraints can be chained fluently.

Reading validation results
--------------------------

[](#reading-validation-results)

`validate()` returns a `SchemaValidationResult` — an aggregate of one result per field. It is iterable, and rolling the per-field results up into a single verdict is left to you, via the granular predicates:

```
$result = $schema->validate($data);

$result->anyFailed();   // at least one field failed
$result->allPassed();   // every field passed (none skipped)
$result->anySkipped();  // at least one field was skipped
$result->anyPending();  // not yet validated

// e.g. "no errors" usually means: nothing failed and nothing is pending
$ok = !$result->anyFailed() && !$result->anyPending();

foreach ($result->getFailed() as $fieldResult) {
    foreach ($fieldResult->getFailed() as $failure) {
        // $failure->name  -> the constraint that failed (e.g. 'minLength', 'type')
        echo "\"{$failure->name}\" failed for field \"{$fieldResult->field->name}\"\n";
    }
}
```

Every result carries a `ValidationStatus`. On aggregate results it is a *computed*property derived on demand from the contained results, so it never goes stale:

```
use Meraki\Schema\ValidationStatus;

$fieldResult->status === ValidationStatus::Passed;
// Passed | Pending | Skipped | Failed
```

`validate()` is a pure query: it returns the result and stores nothing on the fields. Re-validating is safe and repeatable, and the result tree is yours to keep.

Each field is validated in two phases: first its **value/shape** (reported under the constraint name `type`), then its individual constraints. If the shape check fails, the remaining constraints are skipped rather than failed.

Optional fields and default values
----------------------------------

[](#optional-fields-and-default-values)

```
$schema->addBooleanField('subscribe')
    ->makeOptional()   // absent input is skipped, not failed
    ->prefill(false);  // resolved value when no input is given

$schema->addBooleanField('terms')->require(); // the default; explicit here
```

When a field is optional and no input is provided, all of its constraints are **skipped**. `prefill()` sets the value used in place of missing input.

Supplying input
---------------

[](#supplying-input)

`validate()` accepts an array or an object. Objects are read via their public properties **and** `__get()` accessors, so value objects work without exposing internals:

```
final class Input
{
    public function __construct(private array $data) {}
    public function __get(string $name): mixed { return $this->data[$name] ?? null; }
}

$schema->validate(new Input(['username' => 'johndoe', 'age' => 25]));
```

You can also stage input separately from validation:

```
$schema->prefill($defaults); // default values
$schema->input($data);       // user input (applies rules)
$schema->validate($data);    // input + validate in one step
```

Field types
-----------

[](#field-types)

MethodFieldNotable constraints`addTextField``Text``minLengthOf`, `maxLengthOf`, `matches``addNameField``Name``minLengthOf`, `maxLengthOf``addNumberField``Number``minOf`, `maxOf`, `scaleTo`, `inIncrementsOf``addBooleanField``Boolean`—`addEnumField``Enum``allow` (set via constructor `$options`)`addDateField``Date``from`, `until` / `to`, `atIntervalsOf``addTimeField``Time``from`, `until`, `inIncrementsOf`, `precisionMode``addDateTimeField``DateTime``from`, `until`, `inIncrementsOf`, `precisionMode``addDurationField``Duration``minOf`, `maxOf`, `inIncrementsOf``addMoneyField``Money``allow`, `minOf`, `maxOf`, `inIncrementsOf``addEmailAddressField``EmailAddress``minLengthOf`, `maxLengthOf`, `allowDomain`, `disallowDomain``addPhoneNumberField``PhoneNumber`—`addUriField``Uri``minLengthOf`, `maxLengthOf``addUuidField``Uuid``restrictToVersion``addCreditCardField``CreditCard`—`addPasswordField``Password`length + `minNumberOf*`/`maxNumberOf*` (lowercase, uppercase, digits, symbols), `satisfyAnyOf``addPassphraseField``Passphrase`—`addFileField``File``atLeast`, `atMost`, `minFileSizeOf`, `maxFileSizeOf`, `allowTypes`, `disallowTypes`, `allowImages`, `allowVideos`, `allowDocuments`, `disallowScripts``addAddressField``Address` (composite)—`addVariantField``Variant`accepts any of several atomic field typesComposite fields (e.g. `Address`) group sub-fields; their values can be nested under either the local name or the fully-qualified name:

```
$schema->addMoneyField('price', ['AUD' => 2]);
$schema->validate(['price' => ['amount' => '1500', 'currency' => 'AUD']]);
```

A `Variant` field accepts a value that may match one of several atomic field types; the first matching type wins:

```
$schema->addVariantField('secret', [
    new Field\Password(new Property\Name('password')),
    new Field\Passphrase(new Property\Name('passphrase')),
]);
```

Conditional rules
-----------------

[](#conditional-rules)

Rules make one field's requirements depend on another field's value. Targets are referenced by scope path (`#/fields//value`):

```
$schema->addBooleanField('has_phone');
$schema->addTextField('phone')->makeOptional();

$schema->whenAllMatch(
    fn($rule) => $rule
        ->whenEquals('#/fields/has_phone/value', true)
        ->thenRequire('#/fields/phone')
);
```

- `whenAllMatch(...)` — all conditions must hold (`whenAnyMatch(...)` for any).
- Conditions: `whenEquals`, `andWhenEquals`, `orWhenEquals` (or pass a `Rule\Condition` to `when`/`andWhen`/`orWhen`).
- Outcomes: `thenRequire($scope)`, `thenMakeOptional($scope)`.

Rules are re-applied on each `input()`/`validate()` call, and each field is reset to its author-configured optionality first, so an outcome never lingers once its condition stops holding.

Design decisions
----------------

[](#design-decisions)

- **Single-purpose core.** Serialization and rendering are *not* in this package. JSON lives in `meraki/schema-json`; HTML rendering and request normalization live in `meraki/schema-html`. The core depends on neither and exposes a stable public API they both consume.
- **No `Property\Type`, no `Field\Factory`.** Earlier versions modelled a field's type as a `Property\Type` value object and built fields through a factory. Both were removed. A field's type *is* its class, and the shape check is a single `validateValue(mixed): bool` method each field implements. `Facade` constructs fields directly in its `addXField()` methods.
- **Immutable results, computed status.** `SchemaValidationResult` and the aggregated/field/constraint results are immutable; combinators like `getFailed()`, `add()`, and `merge()` return new instances. An aggregate's `status` is computed on demand rather than stored, so it can never drift from its contents.
- **Pure `validate()`.** Validation returns a result and stores nothing on the fields — no per-request state hangs off the schema definition. (HTML rendering threads the returned result through instead of reading it back off the field.)
- **The caller owns the roll-up.** Aggregate results expose granular predicates (`anyFailed()`, `allPassed()`, `anyPending()`, ...) rather than a single opinionated `passed()`/`failed()`. Whether "all passed", "no failures", or "nothing pending" counts as success is a decision the library leaves to you.
- **Composite input nests by local name.** Sub-field values are supplied nested under the composite (`['price' => ['amount' => ...]]`); fully-qualified flat keys are not accepted.
- **Skip vs. fail.** Missing input on an optional field skips its constraints; a failed shape check skips (rather than fails) the dependent constraints. This keeps error reports focused on the real problem.
- **camelCase keys.** Serialized field keys and constraint names use camelCase (e.g. `minLength`, `minCount`); `uri` is the canonical term for URL-style fields. (The serialized form itself is produced by `meraki/schema-json`.)

Examples
--------

[](#examples)

Runnable scripts live in [`examples/`](examples/):

- [`validate.php`](examples/validate.php) — basic field validation.
- [`validate-with-magic-input.php`](examples/validate-with-magic-input.php) — validating a `__get`-based value object.

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance97

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity46

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

Every ~49 days

Recently: every ~90 days

Total

14

Last Release

2d ago

Major Versions

v0.9.0-alpha → v1.10.0-alpha2026-06-03

### Community

Maintainers

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

---

Top Contributors

[![nbish11](https://avatars.githubusercontent.com/u/1518821?v=4)](https://github.com/nbish11 "nbish11 (359 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/meraki-form/health.svg)

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

###  Alternatives

[laravel/framework

The Laravel Framework.

34.8k543.8M20.1k](/packages/laravel-framework)[illuminate/validation

The Illuminate Validation package.

18838.2M1.7k](/packages/illuminate-validation)[illuminate/database

The Illuminate Database package.

2.8k54.9M11.6k](/packages/illuminate-database)[propaganistas/laravel-phone

Adds phone number functionality to Laravel based on Google's libphonenumber API.

3.0k39.7M145](/packages/propaganistas-laravel-phone)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)

PHPackages © 2026

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