hongxunpan/validator

Framework-agnostic validator core with extensible DSL keywords and adapter-friendly contracts

Maintainers

Package info

github.com/HongXunPan/php-validator

pkg:composer/hongxunpan/validator

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.3 2026-05-16 13:07 UTC

This package is auto-updated.

Last update: 2026-05-16 13:08:50 UTC


README

简体中文文档

hongxunpan/validator is a framework-agnostic validator core built around three ideas:

  • rules are public extension units and execute themselves;
  • consumers extend through validator subclass arrays instead of handler/source registries;
  • the kernel only orchestrates and pushes execution details into smaller collaborators.

Current Status

This repository is in pre-1.0 development.

For change history, see:

For contribution workflow, see:

For current performance notes, see:

For richer canonical usage scenarios, see:

  • [High-value Canonical Examples (Chinese)](./docs/高价值 canonical 示例.zh-CN.md)

Already in place:

  • Composer package skeleton;
  • PHP >=5.6 compatibility baseline;
  • RuleInterface + AbstractRule + KEY + of(...) public convention;
  • validator subclass extension configuration:
    • extraRules
    • ruleAliases
    • ruleMessages
  • canonical core validation pipeline.

Still in progress:

  • broader core canonical rule coverage;
  • backend adapter integration;
  • README examples and release hardening.

Public Extension Model

Default consumers only need a validator subclass:

class DemoValidator extends Validator
{
    protected static $extraRules = array(
        'trimTest' => TrimTestRule::class,
    );

    protected static $ruleAliases = array(
        'trimAlias' => 'trimTest',
    );

    protected static $ruleMessages = array(
        'trimTest' => '$paramName must be trimmed',
    );
}

When these maps become large, prefer two paths depending on the use case:

  • if you only want to split long static maps out of the validator class, prefer the provider-class constants:
    • EXTRA_RULES_PROVIDER_CLASS
    • RULE_ALIASES_PROVIDER_CLASS
    • RULE_MESSAGES_PROVIDER_CLASS
  • only override:
    • defineExtraRules()
    • defineRuleAliases()
    • defineRuleMessages() when you really need inherited merging or dynamic logic.

Rule lookup order is fixed:

  1. try the input rule key as a real rule;
  2. only when it does not exist, try alias mapping;
  3. resolve the final rule class;
  4. execute RuleClass::validate(RuleContext $context).

Public DSL Conventions

  • rule strings use ruleName[:argument];
  • each rule only splits on the first :;
  • public keyword classes expose:
    • KEY
    • key()
    • of(...)

Package Layout

src/
  Validator.php
  ValidationKernel.php
  Internal/
  Rule/
  Context/
  Support/
  Result/
  Output/
  Exception/
tests/

Installation

composer require hongxunpan/validator

30-second Quick Start

<?php

use HongXunPan\Validator\Validator;

class DemoValidator extends Validator
{
}

$result = DemoValidator::validateAndNormalize(
    array(
        'name' => '  Alice  ',
    ),
    array(
        'name:Name' => 'required|trim|minLength:2|maxLength:20',
    )
);

if ($result->isFailed()) {
    var_dump($result->errors());
    var_dump($result->detail());
    return;
}

var_dump($result->validatedData());
// array('name' => 'Alice')

This shows the default smallest path:

  • no custom rule yet;
  • only built-in canonical rules;
  • validateAndNormalize(...) returns ValidationResult;
  • successful output is read from validatedData().

Compatibility

  • PHP: >=5.6

Performance Trade-off

At the current stage, the package intentionally prioritizes:

  • clear public contract boundaries;
  • maintainable and testable execution flow;
  • measured optimization only after a real hotspot appears.

For the current performance stance and benchmark trigger conditions, see:

Public Contract and Stability Boundary

This repository is still in pre-1.0, but the public boundary is now being frozen.

The current stable public surface mainly includes:

  • Validator
  • ValidationKernel
  • ValidationResult
  • RuleInterface / AbstractRule / AbstractPresenceRule / AbstractValueRule
  • PresenceRuleInterface / ValueRuleInterface
  • ConditionalPresenceRuleInterface / ValueMaterializationRuleInterface / DependentValueRuleInterface
  • RuleContext / RuleValueReaderInterface / ValidationOptions / RuleResult
  • ValidatedDataWriterInterface / ArrayAccessValidatedDataWriter

The following are explicitly not part of the stable contract:

  • Internal/*
  • Rule\\CoreRules
  • undocumented internal assembly helpers

For the full contract notes, see:

Working with ValidationResult

validate(...), validateAndNormalize(...), and validateListAndNormalize(...) currently return:

  • HongXunPan\Validator\Result\ValidationResult

The most common methods are:

  • isPassed() / isFailed()
  • count()
  • errors()
  • detail()
  • validatedData()
  • toArray()

Example:

$result = DemoValidator::validate(
    array('name' => ''),
    array('name:Name' => 'required')
);

if ($result->isFailed()) {
    var_dump($result->count());
    var_dump($result->errors());
    var_dump($result->detail());
}

If an old project still needs the legacy array envelope, convert explicitly in the adapter layer:

$legacy = $result->toArray();

Smallest Custom Rule Example

<?php

use HongXunPan\Validator\Result\RuleResult;
use HongXunPan\Validator\Rule\AbstractValueRule;
use HongXunPan\Validator\Context\RuleContext;

class TrimNameRule extends AbstractValueRule
{
    const KEY = 'trimName';
    const MESSAGE = '$paramName must be string';

    public static function validate(RuleContext $context)
    {
        if (!is_string($context->value())) {
            return RuleResult::fail($context->value());
        }

        return RuleResult::pass(trim($context->value()));
    }
}

Then attach it in your validator subclass:

<?php

use HongXunPan\Validator\Validator;

class DemoValidator extends Validator
{
    protected static $extraRules = array(
        'trimName' => TrimNameRule::class,
    );
}

Call it:

$result = DemoValidator::validateAndNormalize(
    array('name' => '  Alice  '),
    array('name:Name' => 'trimName')
);

extraRules / ruleAliases / ruleMessages

class DemoValidator extends Validator
{
    protected static $extraRules = array(
        'trimName' => TrimNameRule::class,
    );

    protected static $ruleAliases = array(
        'trimAlias' => 'trimName',
        'lenMin' => 'minLength',
    );

    protected static $ruleMessages = array(
        'trimName' => '$paramName must be trimmed first',
        'minLength' => '$paramName is too short',
    );
}

Meaning:

  • extraRules: project-defined real rule keys;
  • ruleAliases: legacy or shorthand names mapped to final rule keys;
  • ruleMessages: message overrides by the final rule key.
  • When these maps grow large:
    • if you only want to split long arrays out of the validator class, prefer the provider-class constants:
      • EXTRA_RULES_PROVIDER_CLASS
      • RULE_ALIASES_PROVIDER_CLASS
      • RULE_MESSAGES_PROVIDER_CLASS
    • only override defineExtraRules() / defineRuleAliases() / defineRuleMessages() when you really need inherited merging or dynamic logic.

Rule lookup order is stable:

  1. try the input key as a real rule;
  2. only when the real rule does not exist, try alias lookup;
  3. execute the final resolved rule class.

List Validation Examples

Scalar list

$result = DemoValidator::validateListAndNormalize(
    array('  a  ', '  bb  '),
    'trim|minLength:1'
);

Object list

$result = DemoValidator::validateListAndNormalize(
    array(
        array('name' => ' Alice '),
        array('name' => ' Bob '),
    ),
    array(
        'name:Name' => 'required|trim|minLength:1',
    ),
    array(
        'field_prefix' => 'items',
    )
);

When one item is not an array in an object-list scenario, the failure detail path will look like:

  • items.2

More Canonical Examples

If the minimal README examples are already clear but you want a few more copy-friendly core scenarios, such as:

  • string normalization with length assertions;
  • default plus numeric normalization;
  • time formatting with cross-field comparison;
  • numeric comparison reading normalized dependent values;
  • list-rule composition;

see:

  • [High-value Canonical Examples (Chinese)](./docs/高价值 canonical 示例.zh-CN.md)

Adapter Layer for Legacy Projects

If an existing project still depends on:

  • array envelopes
  • validateOrThrow() returning payload arrays
  • project-local messages
  • legacy aliases or legacy rules
  • ORM rules such as unique / exists

prefer keeping those concerns in a project adapter layer instead of pushing them back into core.

Project-level adapter samples belong to the shared workspace collaboration layer and are not published as part of the package-facing docs.

Integration and Migration Notes

This package is not a seamless drop-in replacement for older validator helpers.

Core methods currently return objects:

  • validate(...) -> ValidationResult
  • validateAndNormalize(...) -> ValidationResult
  • validateListAndNormalize(...) -> ValidationResult

So if an existing project historically depends on:

  • an array envelope: count / errors / detail / validated_data
  • validateOrThrow() returning validated payload arrays
  • legacy aliases, legacy rules, or project-specific messages
  • old cross-field compare arguments such as fieldPath,label

that compatibility should be handled in the project-level adapter layer, not pushed back into the core package.

Recommended approach:

  • call ValidationResult::toArray() when an old array envelope must be preserved
  • keep project-local *OrThrow facade/helper methods if old signatures must remain stable
  • declare project-specific extraRules / ruleAliases / ruleMessages in a project validator subclass; if you only need to split large static maps, prefer the provider-class constants, and reserve the matching define*() overrides for inherited merging or dynamic logic
  • keep ORM or business-specific rules such as unique / exists outside core

Not recommended:

  • making ValidationResult implement ArrayAccess just for backward compatibility
  • turning the core package back into an array-first API
  • promoting one project's historical helper contract into the default public contract of this package

If a real project still needs adapter samples, keep them in that project's own repository or in the shared collaboration workspace instead of pushing project compatibility notes back into the public package docs.

Testing

composer test

Or run directly:

php tests/TestRunner.php

GitHub Actions currently runs the package test matrix on:

  • PHP 5.6
  • PHP 7.0
  • PHP 7.1
  • PHP 7.2
  • PHP 7.3
  • PHP 7.4
  • PHP 8.0
  • PHP 8.1
  • PHP 8.2
  • PHP 8.3
  • PHP 8.4
  • PHP 8.5

using:

composer test

Static Analysis

The repository currently uses:

  • phpstan

Default command:

composer analyse

Static analysis runs in CI as a dedicated job on a modern PHP runtime.

Notes:

  • the current phpstan line no longer models PHP 5.6 directly;
  • static analysis therefore focuses on structure and type safety;
  • runtime compatibility down to PHP 5.6 remains covered by the multi-version test matrix.

License

This project is licensed under the MIT License.