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

Validator Es Laravel Package

ajgl/validator-es

ajgl/validator-es is a PHP library with validators for common Spanish codes: DNI, NIE, IdCard (DNI or NIE), CCC, and Spanish IBAN. Each implements a simple ValidatorInterface with an isValid() method, making Spanish identifier and bank account validation easy in any PHP or Laravel project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin.

composer require ajgl/validator-es:^0.1.8

Pin the minor version explicitly. The package has a single maintainer and near-zero adoption, so an unlocked constraint is an unnecessary risk surface for zero benefit. If your app is still on PHP 8.1, you are capped at 0.1.7 — 0.1.8 dropped 8.1 support.

Where to look first. The README is thin, so go straight to vendor/ajgl/validator-es/src/. Every validator is a small class implementing one contract:

namespace Ajgl\ValidatorEs;

interface ValidatorInterface
{
    public function isValid(mixed $value): bool;
}

That directory is the source of truth for the exact class names (DniValidator, NieValidator, IdCardValidator, CccValidator, IbanValidator by convention) and — critically — for whether any trimming or uppercasing happens on your behalf. It does not. Read the isValid() body of whichever validator you plan to use before you wire it into a form.

Smoke test before you write any wrapper code. Confirm the behaviour you assume:

php artisan tinker
>>> (new \Ajgl\ValidatorEs\IdCardValidator())->isValid('Y0000000Z');   // true  (NIE)
>>> (new \Ajgl\ValidatorEs\IdCardValidator())->isValid('12345678Z');   // true  (DNI)
>>> (new \Ajgl\ValidatorEs\IdCardValidator())->isValid('y0000000z');   // check this
>>> (new \Ajgl\ValidatorEs\IbanValidator())->isValid('ES91 2100 0418 4502 0005 1332'); // check this

Those last two lines are the ones that decide your integration design. If lowercase and spaced input fail (they almost certainly do), normalization is your job, not the package's.

First real use case. A registration or supplier-onboarding form with a single "NIF / NIE" field. Use IdCardValidator for the combined field; use DniValidator or NieValidator only when the document type is a separate, explicit choice in your UI. One Rule class, one translated error message, one data-provider test — that is the whole first increment.


Implementation Patterns

Pattern 1: One Rule object per code (the default)

Wrap the package rather than calling it from controllers. This is the single most valuable decision: it makes the dependency swappable in one file.

namespace App\Rules;

use Ajgl\ValidatorEs\IdCardValidator;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class SpanishIdCard implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $normalized = strtoupper(preg_replace('/[\s\-\.]/', '', (string) $value));

        if ($normalized === '') {
            return; // let `required` own emptiness
        }

        if (! (new IdCardValidator())->isValid($normalized)) {
            $fail('validation.spanish_id_card')->translate();
        }
    }
}

Note the deliberate return on empty input. Do not make the rule responsible for presence — that leads to two error messages on one field and confused users.

Pattern 2: A base class for the family

Four near-identical rules is four places to fix a normalization bug. Parameterize instead:

abstract class SpanishCodeRule implements ValidationRule
{
    abstract protected function validator(): \Ajgl\ValidatorEs\ValidatorInterface;
    abstract protected function message(): string;

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $normalized = strtoupper(preg_replace('/[\s\-\.]/', '', (string) $value));

        if ($normalized !== '' && ! $this->validator()->isValid($normalized)) {
            $fail($this->message())->translate();
        }
    }
}

class SpanishIban extends SpanishCodeRule
{
    protected function validator(): ValidatorInterface { return new IbanValidator(); }
    protected function message(): string { return 'validation.spanish_iban'; }
}

Pattern 3: Closure rule for one-off endpoints

For a single legacy endpoint you are not going to revisit, a closure is fine and honest:

'dni' => ['required', 'string', function (string $attribute, mixed $value, Closure $fail) {
    if (! (new DniValidator())->isValid(strtoupper(trim($value)))) {
        $fail('El :attribute no es un DNI válido.');
    }
}],

Do not let this pattern spread. If it appears a third time, promote it to Pattern 1.

Pattern 4: FormRequest with normalization up front

Normalize in prepareForValidation() so the rule receives clean data, and keep the rule's own normalization as defence in depth for non-HTTP callers:

protected function prepareForValidation(): void
{
    $this->merge([
        'document' => strtoupper(preg_replace('/[\s\-\.]/', '', (string) $this->input('document'))),
    ]);
}

public function rules(): array
{
    return [
        'document' => ['required', 'string', 'max:20', new SpanishIdCard()],
        'iban'     => ['required', 'string', 'max:34', new SpanishIban()],
    ];
}

Pattern 5: Service layer for imports, jobs, and API payloads

Validation rules are an HTTP concern. For CSV imports, queued jobs, and webhooks, put the package behind a small service and inject it — this is also the seam where you swap implementations later:

final class SpanishIdentifierService
{
    public function isIdCard(?string $value): bool
    {
        $n = strtoupper(preg_replace('/[\s\-\.]/', '', (string) $value));
        return $n !== '' && (new IdCardValidator())->isValid($n);
    }
}

Bind it in a service provider if you want it mockable in tests. The package itself is stateless, so caching instances is unnecessary and premature.

Pattern 6: Test vectors in a data provider

Since upstream has no test suite you can inspect, your tests are the specification. Cover both directions and pin the canonical published examples:

public static function idCards(): array
{
    return [
        'dni valid'      => ['12345678Z', true],
        'nie valid'      => ['Y0000000Z', true],
        'dni bad letter' => ['12345678A', false],
        'nie bad prefix' => ['Q0000000Z', false],
        'lowercase'      => ['12345678z', true],  // true only because we normalize
        'spaced'         => ['12 345 678 Z', true],
        'empty'          => ['', true],           // rule defers to `required`
        'null'           => [null, true],
        '@/not-an-id'    => ['not-an-id', false],
    ];
}

That table is your regression fence. If a future upstream release changes normalization behaviour, you will see it immediately.

Pattern 7: Localization

Add keys to lang/es/validation.php and lang/en/validation.php rather than hardcoding Spanish strings:

'spanish_id_card' => 'El campo :attribute no es un DNI o NIE válido.',
'spanish_iban'    => 'El campo :attribute no es un IBAN español válido.',

The IBAN message matters — say "español" explicitly, because that is exactly the scope limit users will otherwise discover by surprise.

Pattern 8: Use the checksum calculators for fixtures

Since 0.1.3 the package ships checksum calculators alongside the validators. Reach for those when generating test fixtures or building admin tooling that displays a corrected check digit, rather than reimplementing the modulo-23 DNI letter table in your seeders.


Gotchas and Tips

Normalization is entirely yours. This is the number-one source of "it says my DNI is invalid but my DNI is fine." Users type 12.345.678-Z, 12345678 z, 12345678z. Decide a canonical form (uppercase, no separators) and apply it in prepareForValidation() and inside the rule. Never trust that only one entry point exists.

mixed $value means null and int arrive without complaint. Casting an ID to int silently destroys leading zeros on CCC and IBAN values. Always keep these as string in your database and DTOs — char(20)/varchar(34), not bigint. Pass strings through, not numbers.

The IBAN validator is Spanish-only. Do not register it as iban in your generic rule set. If you support multiple countries, either branch by country prefix before calling it, or use a general IBAN library for non-ES values. Naming your rule SpanishIban rather than Iban prevents a future developer from making this mistake.

There is no CIF/NIF. Spanish B2B onboarding overwhelmingly requires company tax IDs, which this package does not validate. Users will type a B12345678 into your "NIF" field, and it will fail with a message implying their tax ID is wrong. Either add a CIF validator yourself, or clearly label the field "DNI / NIE" and provide separate handling for companies.

Format validation is not identity verification. This is a compliance framing issue that becomes an engineering issue at exactly the wrong moment. Keep the distinction visible in code: name the service SpanishIdentifierFormatService, not IdentityVerifier. Put a comment in the rule class. You validate that an identifier is well-formed, not that it exists — real verification belongs with your KYC vendor.

Emptiness is required's job. Return early on empty string. Combining required with a rule that also rejects '' produces duplicate error messages, and duplicate messages on a single field look broken.

Validation is not uniqueness. A DNI can be perfectly valid and already registered. You still need a database unique index plus your own unique rule; the package does nothing here and cannot.

Do not treat the Symfony integration as a sign of Laravel support. The changelog mentions validators in the Symfony container — that is Symfony DI, not a Laravel service provider. Expect to write every line of Laravel wiring yourself. Check composer.json for a thecodingmachine/safe constraint clash with your existing dependency graph before you commit; 0.1.7 dropped safe:^2.

Pin the version and own the test vectors. One star, zero dependents, one maintainer, self-service support. That is acceptable for a pure helper function; it is not acceptable to adopt and forget. Lock the minor version, keep the data-provider tests above, and note in your dependency documentation that forking is the fallback — the surface area is small enough that a fork is a few hours, not a few days.

Debug by dumping the normalized value, not the raw input. When a user reports a failed validation, the interesting question is what string actually reached isValid(). Log or dd() the normalized form in a local environment; the raw input rarely tells you anything because normalization is where surprises live.

Extension points are narrow by design. There is one interface with one method — no configuration, no events, no hooks. If you need a custom code (CIF, NIF, passport), write your own class implementing Ajgl\ValidatorEs\ValidatorInterface so it slots into the same wrapper base class. That keeps your custom validators and the package's validators interchangeable, which is a genuinely pleasant property for a validation layer.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package