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

# Reglas de validación personalizadas

> Explica cómo extender el sistema de validación de Laravel con objetos de regla o closures, profundizando en la implementación interna del framework.

## Qué son las reglas de validación personalizadas

Laravel incluye una amplia lista de reglas de validación integradas, pero hay ocasiones en las que necesitas lógica de validación específica de tu aplicación. Con las reglas personalizadas puedes definir lógica de validación reutilizable en clases o closures y usarla igual que las reglas estándar.

Hay dos formas principales de definir reglas personalizadas:

* **Objetos de regla** — muy reutilizables y fáciles de testear.
* **Closures** — recomendados para reglas simples de un solo uso.

## Objetos de regla

### Generar una clase de regla

Con el comando Artisan `make:rule` se crea una nueva clase de regla. La clase generada se coloca en el directorio `app/Rules`.

```bash theme={null}
php artisan make:rule Uppercase
```

### Implementar la interfaz ValidationRule

Implementa el método `validate` en la clase generada. Este método llama al closure `$fail` cuando la validación falla.

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Uppercase implements ValidationRule
{
    /**
     * Ejecuta la regla de validación
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
```

En la cadena que pasas al closure `$fail` puedes usar el placeholder `:attribute`; Laravel lo sustituye por el nombre del campo.

### Aplicar el objeto de regla

Pasa la instancia del objeto de regla al array de validación.

```php theme={null}
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
```

Se usa igual dentro del método `rules()` de un form request.

```php theme={null}
public function rules(): array
{
    return [
        'name' => ['required', 'string', new Uppercase],
    ];
}
```

### Mensajes de error usando claves de traducción

En lugar de escribir el mensaje directamente, puedes usar claves de traducción.

```php theme={null}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (strtoupper($value) !== $value) {
        $fail('validation.uppercase')->translate();
    }
}
```

Añade el mensaje en el archivo de traducción `lang/es/validation.php`.

```php theme={null}
return [
    'uppercase' => 'El campo :attribute debe estar en mayúsculas.',
    // ...
];
```

### Añadir varios mensajes de error

Para reportar varios errores en un mismo campo, llama a `$fail` más de una vez.

```php theme={null}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (! is_string($value)) {
        $fail('The :attribute must be a string.');
        return;
    }

    if (strlen($value) < 8) {
        $fail('The :attribute must be at least 8 characters.');
    }

    if (! preg_match('/[A-Z]/', $value)) {
        $fail('The :attribute must contain at least one uppercase letter.');
    }
}
```

## Reglas basadas en closures

Para reglas simples que solo se usan una vez en la aplicación, puedes definirlas mediante closures sin crear una clase.

```php theme={null}
use Illuminate\Support\Facades\Validator;
use Closure;

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value === 'foo') {
                $fail("The {$attribute} is invalid.");
            }
        },
    ],
]);
```

Las reglas por closure son cómodas porque se definen en línea, pero son difíciles de reutilizar y de testear de forma aislada, así que si necesitas usar la lógica en varios sitios es preferible extraerla a un objeto de regla.

## Reglas implícitas (se ejecutan aunque el valor esté vacío)

Por defecto, si el campo está vacío o no existe, las reglas personalizadas no se ejecutan. Si quieres que la regla se ejecute también con valores vacíos, genera la clase con la opción `--implicit`.

```bash theme={null}
php artisan make:rule Uppercase --implicit
```

La clase generada implementa la interfaz `ImplicitRule`. Esta interfaz no añade métodos; simplemente actúa como señal para Laravel.

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ImplicitRule;
use Illuminate\Contracts\Validation\ValidationRule;

class RequiredIfJapanese implements ValidationRule, ImplicitRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (empty($value)) {
            $fail('The :attribute is required.');
            return;
        }

        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー]+$/u', $value)) {
            $fail('The :attribute must contain only Japanese characters.');
        }
    }
}
```

<Warning>
  `ImplicitRule` solo indica a Laravel que «el atributo es obligatorio». Que la validación realmente falle cuando el valor esté vacío depende de lo que hagas dentro del método `validate`.
</Warning>

## Acceso a datos

### DataAwareRule — acceso a todos los datos del formulario

Si necesitas validar en función del valor de otros campos, implementa la interfaz `DataAwareRule`. El método `setData` se invoca automáticamente antes de empezar la validación.

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class UniqueForTenant implements DataAwareRule, ValidationRule
{
    /**
     * Todos los datos objeto de validación
     *
     * @var array<string, mixed>
     */
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column = 'value',
    ) {}

    /**
     * Establece los datos de la validación
     *
     * @param  array<string, mixed>  $data
     */
    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $exists = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value)
            ->exists();

        if ($exists) {
            $fail('The :attribute has already been taken for this tenant.');
        }
    }
}
```

Ejemplo de uso:

```php theme={null}
$request->validate([
    'tenant_id' => 'required|integer',
    'email' => ['required', 'email', new UniqueForTenant('users', 'email')],
]);
```

### ValidatorAwareRule — acceso a la instancia del validator

Para acceder a toda la información del validator (reglas que han fallado, mensajes personalizados, etc.), implementa la interfaz `ValidatorAwareRule`.

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Validation\Validator;

class ConditionalFormat implements ValidationRule, ValidatorAwareRule
{
    protected Validator $validator;

    public function setValidator(Validator $validator): static
    {
        $this->validator = $validator;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Salta si otro campo ya ha fallado la validación
        if ($this->validator->errors()->has('type')) {
            return;
        }

        $type = $this->validator->getData()['type'] ?? null;

        if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) {
            $fail('The :attribute must be a valid phone number.');
        }

        if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $fail('The :attribute must be a valid email address.');
        }
    }
}
```

## Casos de uso prácticos

### Comprobación de caracteres japoneses

Regla para validar el conjunto de caracteres (por ejemplo, japonés).

```bash theme={null}
php artisan make:rule JapaneseOnly
```

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapaneseOnly implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Solo permite hiragana, katakana, kanji y el signo de alargamiento
        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value)) {
            $fail('El campo :attribute debe estar en japonés (hiragana, katakana o kanji).');
        }
    }
}
```

```php theme={null}
$request->validate([
    'name_kana' => ['required', 'string', new JapaneseOnly],
]);
```

### Validación de formato de teléfono

Regla para validar el formato de números de teléfono de Japón.

```bash theme={null}
php artisan make:rule JapanesePhone
```

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapanesePhone implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Admite con o sin guiones y también el formato internacional
        $normalized = preg_replace('/[\s\-\(\)]/', '', $value);

        $patterns = [
            '/^0\d{9,10}$/',        // Número fijo o móvil habitual
            '/^\+81\d{9,10}$/',     // Formato internacional
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $normalized)) {
                return;
            }
        }

        $fail('El campo :attribute debe tener un formato de teléfono válido.');
    }
}
```

### Restricción de unicidad con ID de tenant

Restricción típica en aplicaciones multi-tenant: unicidad dentro del ámbito de un tenant.

<Steps>
  <Step title="Crear la clase de regla">
    ```bash theme={null}
    php artisan make:rule TenantUnique
    ```

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

    namespace App\Rules;

    use Closure;
    use Illuminate\Contracts\Validation\DataAwareRule;
    use Illuminate\Contracts\Validation\ValidationRule;
    use Illuminate\Support\Facades\DB;

    class TenantUnique implements DataAwareRule, ValidationRule
    {
        protected array $data = [];

        public function __construct(
            protected string $table,
            protected string $column,
            protected ?int $ignoreId = null,
        ) {}

        public function setData(array $data): static
        {
            $this->data = $data;

            return $this;
        }

        public function validate(string $attribute, mixed $value, Closure $fail): void
        {
            $tenantId = $this->data['tenant_id'] ?? null;

            $query = DB::table($this->table)
                ->where('tenant_id', $tenantId)
                ->where($this->column, $value);

            if ($this->ignoreId !== null) {
                $query->where('id', '!=', $this->ignoreId);
            }

            if ($query->exists()) {
                $fail('El campo :attribute ya está en uso.');
            }
        }
    }
    ```
  </Step>

  <Step title="Usarla en un form request">
    ```php theme={null}
    <?php

    namespace App\Http\Requests;

    use App\Rules\TenantUnique;
    use Illuminate\Foundation\Http\FormRequest;

    class CreateProjectRequest extends FormRequest
    {
        public function rules(): array
        {
            return [
                'tenant_id' => 'required|integer|exists:tenants,id',
                'name' => [
                    'required',
                    'string',
                    'max:100',
                    new TenantUnique('projects', 'name'),
                ],
            ];
        }

        public function authorize(): bool
        {
            return true;
        }
    }
    ```
  </Step>

  <Step title="En actualización, excluir el propio ID">
    Cuando actualizas un registro existente, excluye su propio ID de la comprobación de duplicados.

    ```php theme={null}
    class UpdateProjectRequest extends FormRequest
    {
        public function rules(): array
        {
            $projectId = $this->route('project');

            return [
                'tenant_id' => 'required|integer|exists:tenants,id',
                'name' => [
                    'required',
                    'string',
                    'max:100',
                    new TenantUnique('projects', 'name', ignoreId: $projectId),
                ],
            ];
        }
    }
    ```
  </Step>
</Steps>

## Registro de reglas en un service provider

### Añadir reglas con Validator::extend()

Con `Validator::extend()` puedes usar la regla personalizada en formato de cadena (`'nombre_de_la_regla'`). Regístrala en el método `boot()` de `AppServiceProvider`.

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

namespace App\Providers;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Validator as ValidatorInstance;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::extend('japanese_only', function (string $attribute, mixed $value, array $parameters, ValidatorInstance $validator): bool {
            return preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value) === 1;
        });

        Validator::replacer('japanese_only', function (string $message, string $attribute): string {
            return str_replace(':attribute', $attribute, 'El campo :attribute debe estar en japonés.');
        });
    }
}
```

Las reglas registradas con `Validator::extend()` pueden usarse como una cadena.

```php theme={null}
$request->validate([
    'name_kana' => 'required|japanese_only',
]);
```

<Warning>
  `Validator::extend()` es un mecanismo de registro más antiguo que los objetos de regla. Para código nuevo, se recomienda usar objetos que implementen la interfaz `ValidationRule`.
</Warning>

### Añadir métodos estáticos a la clase Rule

Añadiendo macros a la facade `Rule` puedes ofrecer una API fluida del tipo `Rule::myRule()`.

```php theme={null}
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
```

```php theme={null}
// Ejemplo de uso
$request->validate([
    'name' => ['required', Rule::tenantUnique('projects', 'name')],
]);
```

## Detalles de la implementación interna

Veamos cómo `Illuminate\Validation\Validator` invoca las reglas personalizadas.

Dentro del validator, `validateAttribute()` procesa cada campo. Si la regla implementa la interfaz `ValidationRule`, se llama al método `validateUsingCustomRule()`.

```php theme={null}
// Versión simplificada de Illuminate\Validation\Validator::validateUsingCustomRule()
protected function validateUsingCustomRule($attribute, $value, $rule)
{
    // Si es DataAwareRule, inyecta todos los datos
    if ($rule instanceof DataAwareRule) {
        $rule->setData($this->getData());
    }

    // Si es ValidatorAwareRule, inyecta el propio validator
    if ($rule instanceof ValidatorAwareRule) {
        $rule->setValidator($this);
    }

    // Llama a validate()
    $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) {
        // El closure $fail — añade el mensaje de error
        $this->errors()->add($attribute, $this->makeReplacements(
            $message,
            $attribute,
            get_class($rule),
            [], // sustituciones adicionales (por ejemplo, ['min' => '8'])
        ));
    });
}
```

El tratamiento de `ImplicitRule` se decide con `isImplicit()`, que hace que la regla se ejecute incluso cuando el valor está vacío.

```php theme={null}
// Decide si se ejecuta la regla para un valor vacío
protected function isImplicit($rule): bool
{
    return $rule instanceof ImplicitRule
        || in_array($rule, $this->implicitRules);
}
```

<Tip>
  Puedes implementar `DataAwareRule` y `ValidatorAwareRule` a la vez. Si una clase implementa ambas interfaces, se ejecutan las dos inyecciones.
</Tip>

## Páginas relacionadas

<Card title="Validación (introducción)" icon="shield-check" href="/es/validation">
  Revisa cómo validar de forma estándar en controladores y form requests.
</Card>


## Related topics

- [Validación](/es/validation.md)
- [Actualización de Laravel 9 a 10](/es/blog/upgrade-9-to-10.md)
- [Guía de actualización de Laravel 11 a 12](/es/blog/upgrade-11-to-12.md)
- [Laravel MCP](/es/mcp.md)
- [Precognition](/es/precognition.md)
