> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Registro de reglas de validación de cadena en paquetes

> Analiza Factory::extend de Laravel 13 y explica el contrato de llamada de las reglas de cadena, los mensajes de error y las traducciones, la diferencia entre Implicit y Dependent, la vida útil de los Validator ya creados y las pruebas.

Para ofrecer desde un paquete distribuido una regla de cadena como `acme_tracking:AC`, registra la lógica de validación en el `Factory` de validación. Así, desde los form requests de la aplicación consumidora se puede usar con la misma notación que las reglas integradas.

Esta página se basa en la implementación oficial de Laravel Framework `v13.40.0`. Complementa el ejemplo básico de [Reglas de validación personalizadas](https://kawax.biz/es/advanced/custom-validation-rules.md) y trata el registro, la traducción y la vida útil del estado cuando se mantienen como API pública de un paquete.

## Distinguirlas de los objetos de regla

Los objetos `ValidationRule` que presenta la documentación oficial también se pueden usar en paquetes. Si poder especificar la regla como cadena no es en sí un requisito, considera un objeto que reciba argumentos tipados en su constructor.

| Enfoque | Uso adecuado | Notificación del fallo |
| - | - | - |
| Objeto de regla como `new CustomRule(...)` | Distribuir como clase configuraciones tipadas o validaciones complejas | Llamar al `$fail` pasado a `validate()` |
| Regla de cadena registrada como `acme_tracking:AC` | Ofrecer una API que también se pueda incorporar en definiciones de reglas basadas en cadenas | El callback registrado devuelve `false` |

Aunque el método tenga el mismo nombre `validate()`, el contrato de llamada es distinto. Si pasas directamente una clase que implementa `ValidationRule` como cadena de clase a `Factory::extend()`, el tipo del tercer argumento y el valor de retorno no coinciden.

## Separar registro, creación y validación

El `extend()` de la fachada `Validator` es una llamada a `Illuminate\Validation\Factory`. Es un objeto distinto de la instancia de `Illuminate\Validation\Validator` que valida la entrada.

```mermaid theme={null}
flowchart TD
    A["boot del paquete"] --> B["Registrar la regla y el replacer<br>en el Factory"]
    B --> C["Crear un nuevo Validator<br>con Factory::make"]
    C --> D["Copiar las extensiones registradas<br>y los mensajes fallback"]
    D --> E["Analizar las reglas de cada atributo<br>y comprobar las condiciones de ejecución"]
    E --> F["Validar con la clase o el closure"]
    F --> G{"¿El valor de retorno es true?"}
    G -->|Sí| H["Esta regla tiene éxito"]
    G -->|No| I["Seleccionar el mensaje y<br>reemplazar los placeholders"]
```

El diagrama muestra el recorrido cuando se ejecuta una regla de cadena. Las omisiones por entrada vacía o por `nullable`, entre otras, se explican más adelante.

El Factory se registra como singleton con la clave `validator`, pero el Validator que devuelve `make()` en la implementación estándar se crea nuevo cada vez. **Los registros posteriores en el Factory no actualizan el array de extensiones de los Validator ya creados.**

## Registrar una regla propia del paquete

Como ejemplo, validamos un código de seguimiento ficticio compuesto por un prefijo en mayúsculas, un guion y 8 dígitos. Es un ejemplo de formato como `AC-00000001`, no la especificación de numeración de una empresa de transporte real.

Se supone que el espacio de nombres del paquete `Acme\Courier\` corresponde a `src/` mediante PSR-4 de Composer y que se carga un service provider normal no diferido. Para la carga de proveedores, consulta [Autodescubrimiento de paquetes](https://kawax.biz/es/advanced/package-discovery.md).

### Separar la lógica de validación en una clase

Prepara en `src/Validation/TrackingCode.php` un manejador para la regla de cadena. Esta clase no implementa la interfaz `ValidationRule`.

```php theme={null}
<?php

namespace Acme\Courier\Validation;

use Illuminate\Validation\Validator;
use InvalidArgumentException;

final class TrackingCode
{
    public function validate(
        string $attribute,
        mixed $value,
        array $parameters,
        Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || preg_match('/\A[A-Z]{2,8}\z/', $parameters[0]) !== 1
        ) {
            throw new InvalidArgumentException(
                'acme_tracking expects one uppercase ASCII prefix of 2-8 characters.',
            );
        }

        return is_string($value)
            && preg_match(
                '/\A'.preg_quote($parameters[0], '/').'-[0-9]{8}\z/',
                $value,
            ) === 1;
    }
}
```

El callback recibe cuatro argumentos: el nombre del atributo, el valor de entrada, el array de parámetros y el Validator que está validando. En el caso de `acme_tracking:AC`, el array de parámetros es `['AC']`. La regla también comprueba el tipo del valor para no provocar un error de tipo aunque se use de forma aislada.

Un prefijo o un número de parámetros no válidos se tratan como una excepción, ya que son **un error de configuración en la definición de la regla** y no un error de entrada del usuario. Cuando la regla se omite por un valor vacío, esta comprobación de configuración tampoco se ejecuta, por lo que también se necesitan pruebas con valores no vacíos válidos.

### Registrar después de resolver el Factory

Desde `boot()` de `src/CourierServiceProvider.php`, registra la regla y el proceso de reemplazo del mensaje.

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Validation\TrackingCode;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Factory;

final class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('validator', function (Factory $factory): void {
            $factory->extend(
                'acme_tracking',
                TrackingCode::class.'@validate',
                'The :attribute must be :prefix- followed by eight digits.',
            );

            $factory->replacer(
                'acme_tracking',
                static function (
                    string $message,
                    string $attribute,
                    string $rule,
                    array $parameters,
                ): string {
                    return str_replace(':prefix', $parameters[0] ?? '', $message);
                },
            );
        });
    }
}
```

`callAfterResolving()` espera a que se cree el Factory si aún no se ha resuelto, y registra en el acto si ya está resuelto. Los detalles del hook se explican en [Hooks posteriores a la resolución de servicios en paquetes](https://kawax.biz/es/advanced/package-service-resolution.md).

La cadena de clase `TrackingCode::class.'@validate'` resuelve la clase en el contenedor durante la validación y llama al método. Aunque solo indiques el nombre de la clase sin `@validate`, el nombre de método predeterminado es `validate`. Si hay dependencias en el constructor, vincúlalas para que el contenedor pueda resolverlas.

Este ejemplo no registra el manejador como singleton ni tiene propiedades que guarden la entrada. Los datos de la validación se manejan a partir de los argumentos, y el diseño evita incorporar estado específico de la petición en los closures que conserva el Factory.

### Usarla desde la aplicación consumidora

```php theme={null}
$request->validate([
    'tracking_code' => ['bail', 'required', 'string', 'acme_tracking:AC'],
]);
```

`bail` detiene las reglas siguientes cuando falla una regla anterior de ese atributo. Se reparten los papeles: la obligatoriedad con `required` y el formato con `acme_tracking`.

Unifica los nombres públicos en snake case propio, como `acme_tracking`. Si se registra el mismo nombre en el mismo tipo de extensión, el registro posterior sobrescribe al anterior. Además, cuando existe un método integrado como `validateRequired()`, se llama directamente a ese método, por lo que no se puede reemplazar una validación integrada con una extensión del mismo nombre.

## Conservar los mensajes de error del usuario

Los mensajes de las reglas de cadena normales se seleccionan aproximadamente en el siguiente orden de prioridad. Las ramas especiales, como las de las reglas de tamaño, quedan fuera de este análisis.

| Prioridad | Origen del mensaje |
| - | - |
| 1 | El tercer argumento de `Validator::make()` o `messages()` del form request |
| 2 | La traducción `validation.custom.nombre_del_atributo.nombre_de_la_regla` de la aplicación consumidora |
| 3 | La traducción con el nombre de la regla, como `validation.acme_tracking` |
| 4 | La cadena fallback registrada en el tercer argumento de `Factory::extend()` |

El replacer personalizado se llama después de que Laravel reemplace `:attribute` y otros placeholders. Los argumentos del replacer son cinco: el mensaje seleccionado, el nombre del atributo, el nombre de la regla, el array de parámetros y el Validator. En el ejemplo anterior se omite el quinto argumento, que no se usa.

> [!WARNING]
>
> Si el replacer siempre sustituye el mensaje completo por un texto fijo, ignora las sobrescrituras del usuario mediante `messages()` o traducciones. El ejemplo anterior solo reemplaza `:prefix` en el `$message` recibido.

Para definir un texto predeterminado en japonés en la aplicación, añade `acme_tracking` al array que devuelve el `lang/ja/validation.php` existente. El ejemplo siguiente muestra la entrada que se añade; no elimina los demás mensajes integrados.

```php theme={null}
<?php

return [
    'acme_tracking' => ':attributeは:prefix-に続く8桁の数字で入力してください。',
];
```

Para preparar los archivos de traducción y configurar el locale, consulta [Localización](https://kawax.biz/es/localization.md). Si ejecutas `__('...')` durante el registro y lo guardas como fallback, en el Factory queda la cadena del locale de ese momento. En aplicaciones que cambian de locale, usa la traducción en el momento de generar el error.

### Cuando se usan traducciones con espacio de nombres del paquete

Si llamas a `loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier')` en `boot()`, podrás referenciar `lang/ja/validation.php` y otros archivos del paquete con `acme-courier::validation.tracking_code`.

Sin embargo, **pasar una clave de traducción como tercer argumento de `extend()` no hace que se traduzca automáticamente.** El Factory la conserva como una simple cadena fallback. Si sustituyes `extend()` y `replacer()` dentro del hook anterior por el siguiente registro, podrás obtener la traducción del paquete solo cuando se seleccione el fallback.

```php theme={null}
$factory->extend(
    'acme_tracking',
    TrackingCode::class.'@validate',
    'acme-courier::validation.tracking_code',
);

$factory->replacer(
    'acme_tracking',
    static function (
        string $message,
        string $attribute,
        string $rule,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): string {
        if ($message === 'acme-courier::validation.tracking_code') {
            return $validator->getTranslator()->get($message, [
                'attribute' => $validator->getDisplayableAttribute($attribute),
                'prefix' => $parameters[0] ?? '',
            ]);
        }

        return str_replace(':prefix', $parameters[0] ?? '', $message);
    },
);
```

Prepara una entrada `tracking_code` en el `validation.php` de cada locale del paquete. Para el texto en japonés puedes usar el mismo que la entrada `acme_tracking` anterior.

Como aquí se obtiene una nueva cadena de traducción dentro del replacer, también se pasa `:attribute` explícitamente a la traducción. El reemplazo estándar de Laravel no se vuelve a aplicar a este valor de retorno. Si se selecciona un mensaje propio del usuario, se mantiene ese texto. Para la sobrescritura de las traducciones distribuidas, consulta [Sobrescritura y actualización de traducciones de paquetes](https://kawax.biz/es/advanced/package-translations.md).

## Comprender las condiciones de ejecución de Implicit y nullable

`extendImplicit()` registra una regla que se convierte en candidata a validación incluso con atributos ausentes o cadenas vacías. No significa "hacer que falle siempre" ni "ignorar todas las demás condiciones de omisión".

| Entrada o especificación | `extend()` normal | `extendImplicit()` |
| - | - | - |
| El atributo no existe | No se ejecuta | Pasa a ser objetivo de ejecución |
| Cadena vacía o solo con espacios | No se ejecuta | Pasa a ser objetivo de ejecución |
| `null` en un atributo existente, sin `nullable` | Pasa a ser objetivo de ejecución | Pasa a ser objetivo de ejecución |
| `null` en un atributo existente, con `nullable` | No se ejecuta | Pasa a ser objetivo de ejecución |
| El atributo no existe y hay `sometimes` | No se ejecuta | No se ejecuta |

La tabla compara las condiciones de ejecución de cada regla. Los casos en los que el procesamiento se detiene antes por `bail`, la exclusión de atributos, `stopOnFirstFailure()`, etc., son aparte. En las entradas HTTP, un middleware puede convertir las cadenas vacías en `null`, así que prueba por separado la cadena vacía y `null`.

Para la regla de formato de este ejemplo basta con combinar el `extend()` normal con `required`. Cambiar a Implicit una regla ya publicada puede hacer que fallen entradas que antes no se validaban, así que trátalo como un cambio de compatibilidad.

## Resolver referencias dentro de arrays con reglas Dependent

Para reglas que reciben como parámetro el nombre de otro campo, puedes usar `extendDependent()`. Al validar `items.*.confirmed_tracking`, el parámetro `items.*.tracking` se convierte en el de la misma fila, como `items.0.tracking`. Con el `extend()` normal no se realiza este reemplazo.

A continuación se muestra una regla de coincidencia para comprobar este mecanismo. Si solo necesitas validar una coincidencia simple, en una aplicación real puedes usar la regla integrada `same`. Tómalo como un ejemplo para aplicarlo a validaciones entre campos propias del paquete.

Añádela al Factory recibido desde el hook.

```php theme={null}
$factory->extendDependent(
    'acme_matches_field',
    static function (
        string $attribute,
        mixed $value,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || $parameters[0] === ''
        ) {
            throw new \InvalidArgumentException('acme_matches_field expects one field name.');
        }

        $data = $validator->getData();

        return \Illuminate\Support\Arr::has($data, $parameters[0])
            && $value === \Illuminate\Support\Arr::get($data, $parameters[0]);
    },
    'The :attribute does not match the related field.',
);
```

```php theme={null}
$request->validate([
    'items' => ['required', 'array'],
    'items.*' => ['array:tracking,confirmed_tracking'],
    'items.*.tracking' => ['required', 'string', 'acme_tracking:AC'],
    'items.*.confirmed_tracking' => [
        'required',
        'string',
        'acme_matches_field:items.*.tracking',
    ],
]);
```

Registrar como Dependent no garantiza la existencia del campo referenciado ni que ya esté validado. El `getData()` del cuarto argumento devuelve **los datos sujetos a validación**, no solo los datos validados. Define las reglas necesarias tanto en el campo que referencia como en el referenciado.

Además, hacerla Dependent no la convierte en Implicit. La obligatoriedad frente a valores ausentes o vacíos se especifica por separado. La implementación aplica a los parámetros el escape de puntos y el reemplazo de `*`, así que evita APIs que mezclen cadenas arbitrarias que no sean nombres de campo.

## Probar desde el registro hasta la validación en la aplicación

En una aplicación Laravel 13 que cargue el paquete y su service provider, crea `tests/Feature/TrackingRuleTest.php`. Además de las pruebas unitarias de la clase, se verifican el registro y el procesamiento de mensajes a través del Factory real.

```php theme={null}
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Factory;
use Tests\TestCase;

final class TrackingRuleTest extends TestCase
{
    public function test_format_presence_and_null_are_separate(): void
    {
        $rules = ['code' => ['acme_tracking:AC']];

        foreach ([
            [['code' => 'AC-00000001'], true],
            [['code' => 'ZX-00000001'], false],
            [['code' => ['AC-00000001']], false],
            [['code' => ''], true],
            [['code' => null], false],
            [[], true],
        ] as [$data, $expected]) {
            $this->assertSame($expected, Validator::make($data, $rules)->passes());
        }

        $this->assertTrue(Validator::make(
            ['code' => null],
            ['code' => ['nullable', 'acme_tracking:AC']],
        )->passes());

        $this->assertFalse(Validator::make(
            [],
            ['code' => ['required', 'acme_tracking:AC']],
        )->passes());
    }

    public function test_application_message_and_display_name_are_kept(): void
    {
        $validator = Validator::make(
            ['code' => 'invalid'],
            ['code' => ['acme_tracking:AC']],
            ['code.acme_tracking' => ':attributeには:prefixのコードを入力してください。'],
            ['code' => '追跡番号'],
        );

        $this->assertSame(
            '追跡番号にはACのコードを入力してください。',
            $validator->errors()->first('code'),
        );
    }

    public function test_existing_validator_keeps_its_extensions(): void
    {
        $factory = new Factory($this->app['translator'], $this->app);
        $factory->extend('acme_probe', static fn (): bool => true);
        $before = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $factory->extend('acme_probe', static fn (): bool => false, 'Rejected.');
        $after = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $this->assertTrue($before->passes());
        $this->assertFalse($after->passes());
    }
}
```

```bash theme={null}
php artisan test --compact tests/Feature/TrackingRuleTest.php
```

La última prueba no modifica el Factory compartido: crea un Factory nuevo y comprueba la copia de lo registrado. Si también ofreces los ejemplos de traducciones con espacio de nombres o de Dependent, prepara pruebas adicionales en un entorno que incluya los registros correspondientes.

## Qué comprobar para el mantenimiento a largo plazo

* Documenta como API pública el nombre de la regla, el significado y el número de parámetros, la obligatoriedad y los tipos de valor aceptados.
* Verifica parámetros no válidos, valores ausentes, cadenas vacías y `null`, y las combinaciones de `nullable`, `sometimes` y `bail`.
* Comprueba `messages()` del usuario, los nombres de atributos mostrados, el cambio de locale y la sobrescritura de las traducciones del paquete.
* En las reglas Dependent, comprueba con varias filas y arrays anidados que no se referencien valores de otras filas.
* Comprueba el registro en Factory sin resolver y ya resuelto, y no esperes que se aplique retroactivamente a los Validator ya creados.
* Evita conflictos de nombres con otros paquetes y no mezcles el mismo nombre en registros normales, Implicit y Dependent.
* Para aplicar cambios en workers de larga duración, reinicia los procesos y no dejes entradas ni información de usuario en manejadores compartidos.

Para el entorno de pruebas de un paquete aislado, consulta también [Pruebas de paquetes](https://kawax.biz/es/advanced/package-testing.md), y para el tratamiento de la compatibilidad, [Gestión de la compatibilidad de versiones de paquetes](https://kawax.biz/es/advanced/package-versioning.md).

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: reglas personalizadas y reglas Implicit](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/validation.md#custom-validation-rules)
* [Documentación oficial de Laravel: service providers y traducciones de paquetes](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [Factory: make, extend, extendImplicit, extendDependent y replacer](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Factory.php)
* [Validator: condiciones de ejecución, registro de extensiones y resolución de clases](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Validator.php)
* [ValidationRuleParser: análisis de reglas de cadena](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationRuleParser.php)
* [FormatsMessages: prioridad y reemplazo de mensajes](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Concerns/FormatsMessages.php)
* [ValidationServiceProvider: registro singleton de validator](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationServiceProvider.php)
* [ServiceProvider: callAfterResolving](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php#L310-L317)
* [Pruebas oficiales: transferencia de extensiones y fallback desde el Factory](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationFactoryTest.php)
* [Pruebas oficiales: reglas de cadena, de clase, Implicit y Dependent](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationValidatorTest.php#L7372-L7471)


## Related topics

- [Hooks de resolución de servicios en paquetes](https://kawax.biz/es/advanced/package-service-resolution.md)
- [Reglas de validación personalizadas](https://kawax.biz/es/advanced/custom-validation-rules.md)
- [Registro dinámico de relaciones en paquetes](https://kawax.biz/es/advanced/package-dynamic-relations.md)
- [Registro de comandos Artisan de paquetes y resolución de dependencias](https://kawax.biz/es/advanced/package-commands.md)
- [Registro y pruebas de transportes de correo en paquetes](https://kawax.biz/es/advanced/package-mail-transports.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.