Saltar al contenido principal

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.
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

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.
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
Se usa igual dentro del método rules() de un form request.
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.
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.
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.
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.
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.
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

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.');
        }
    }
}
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.

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

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:
$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

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).
php artisan make:rule JapaneseOnly
<?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).');
        }
    }
}
$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.
php artisan make:rule JapanesePhone
<?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.
1

Crear la clase de regla

php artisan make:rule TenantUnique
<?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.');
        }
    }
}
2

Usarla en un form request

<?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;
    }
}
3

En actualización, excluir el propio ID

Cuando actualizas un registro existente, excluye su propio ID de la comprobación de duplicados.
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),
            ],
        ];
    }
}

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

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.
$request->validate([
    'name_kana' => 'required|japanese_only',
]);
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.

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().
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
// 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().
// 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.
// 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);
}
Puedes implementar DataAwareRule y ValidatorAwareRule a la vez. Si una clase implementa ambas interfaces, se ejecutan las dos inyecciones.

Páginas relacionadas

Validación (introducción)

Revisa cómo validar de forma estándar en controladores y form requests.
Última modificación el 13 de julio de 2026