Custom Validation
Create custom validation rules and attributes.
Introduction
Section titled “Introduction”Custom validation allows you to create reusable validation logic:
- ✅ Custom Attributes - Reusable validation attributes
- ✅ Custom Rules - Implement ValidationRuleInterface
- ✅ Inline Validation - Use closures
- ✅ Framework Integration - Use Laravel/Symfony validators
Creating Custom Attributes
Section titled “Creating Custom Attributes”Basic Custom Attribute
Section titled “Basic Custom Attribute”use event4u\DataHelpers\SimpleDTO\Attributes\ValidationAttribute;use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]class StrongPassword extends ValidationAttribute{ public function rules(): array { return [ 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', ]; }
public function message(): string { return 'Password must be at least 8 characters with uppercase, lowercase, number, and special character'; }}Using the Attribute
Section titled “Using the Attribute”class UserDTO extends SimpleDTO{ public function __construct( #[Required, StrongPassword] public readonly string $password, ) {}}Advanced Examples
Section titled “Advanced Examples”Attribute with Parameters
Section titled “Attribute with Parameters”#[Attribute(Attribute::TARGET_PROPERTY)]class MinWords extends ValidationAttribute{ public function __construct( private int $min, ) {}
public function rules(): array { return [ function ($attribute, $value, $fail) { $wordCount = str_word_count($value);
if ($wordCount < $this->min) { $fail("The {$attribute} must contain at least {$this->min} words."); } }, ]; }}
// Usage#[MinWords(10)]public readonly string $description;Multiple Rules
Section titled “Multiple Rules”#[Attribute(Attribute::TARGET_PROPERTY)]class Username extends ValidationAttribute{ public function rules(): array { return [ 'required', 'string', 'min:3', 'max:20', 'regex:/^[a-zA-Z0-9_]+$/', 'unique:users,username', ]; }
public function messages(): array { return [ 'regex' => 'Username can only contain letters, numbers, and underscores', 'unique' => 'This username is already taken', ]; }}Conditional Validation
Section titled “Conditional Validation”#[Attribute(Attribute::TARGET_PROPERTY)]class RequiredIf extends ValidationAttribute{ public function __construct( private string $field, private mixed $value, ) {}
public function rules(): array { return [ "required_if:{$this->field},{$this->value}", ]; }}
// Usageclass OrderDTO extends SimpleDTO{ public function __construct( public readonly string $paymentMethod,
#[RequiredIf('paymentMethod', 'credit_card')] public readonly ?string $cardNumber = null, ) {}}Custom Validation Rules
Section titled “Custom Validation Rules”Implementing ValidationRuleInterface
Section titled “Implementing ValidationRuleInterface”use event4u\DataHelpers\SimpleDTO\Contracts\ValidationRule;
class UniqueEmailRule implements ValidationRule{ public function passes(string $attribute, mixed $value): bool { return !User::where('email', $value)->exists(); }
public function message(): string { return 'This email is already registered'; }}Using Custom Rules
Section titled “Using Custom Rules”class UserDTO extends SimpleDTO{ public function __construct( #[CustomRule(UniqueEmailRule::class)] public readonly string $email, ) {}}Inline Validation
Section titled “Inline Validation”Using Closures
Section titled “Using Closures”class ProductDTO extends SimpleDTO{ public function __construct( #[CustomRule(function ($attribute, $value, $fail) { if ($value < 0) { $fail('Price cannot be negative'); } })] public readonly float $price, ) {}}Real-World Examples
Section titled “Real-World Examples”Credit Card Validation
Section titled “Credit Card Validation”#[Attribute(Attribute::TARGET_PROPERTY)]class CreditCard extends ValidationAttribute{ public function rules(): array { return [ function ($attribute, $value, $fail) { // Luhn algorithm $value = preg_replace('/\D/', '', $value); $sum = 0; $alt = false;
for ($i = strlen($value) - 1; $i >= 0; $i--) { $n = (int) $value[$i];
if ($alt) { $n *= 2; if ($n > 9) { $n -= 9; } }
$sum += $n; $alt = !$alt; }
if ($sum % 10 !== 0) { $fail('Invalid credit card number'); } }, ]; }}IBAN Validation
Section titled “IBAN Validation”#[Attribute(Attribute::TARGET_PROPERTY)]class IBAN extends ValidationAttribute{ public function rules(): array { return [ 'regex:/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', function ($attribute, $value, $fail) { // IBAN checksum validation $iban = str_replace(' ', '', strtoupper($value)); $checksum = substr($iban, 0, 4); $account = substr($iban, 4);
$numeric = $account . $checksum; $numeric = str_replace( range('A', 'Z'), range(10, 35), $numeric );
if (bcmod($numeric, '97') !== '1') { $fail('Invalid IBAN'); } }, ]; }}Date Range Validation
Section titled “Date Range Validation”#[Attribute(Attribute::TARGET_PROPERTY)]class DateRange extends ValidationAttribute{ public function __construct( private ?string $after = null, private ?string $before = null, ) {}
public function rules(): array { $rules = ['date'];
if ($this->after) { $rules[] = "after:{$this->after}"; }
if ($this->before) { $rules[] = "before:{$this->before}"; }
return $rules; }}
// Usage#[DateRange(after: 'today', before: '+1 year')]public readonly Carbon $eventDate;Framework Integration
Section titled “Framework Integration”Laravel Validation
Section titled “Laravel Validation”#[Attribute(Attribute::TARGET_PROPERTY)]class LaravelRule extends ValidationAttribute{ public function __construct( private string $rule, ) {}
public function rules(): array { return [$this->rule]; }}
// Usage#[LaravelRule('exists:users,id')]public readonly int $userId;Symfony Validation
Section titled “Symfony Validation”use Symfony\Component\Validator\Constraints as Assert;
#[Attribute(Attribute::TARGET_PROPERTY)]class SymfonyConstraint extends ValidationAttribute{ public function __construct( private Assert\Constraint $constraint, ) {}
public function getConstraint(): Assert\Constraint { return $this->constraint; }}
// Usage#[SymfonyConstraint(new Assert\Url())]public readonly string $website;Best Practices
Section titled “Best Practices”Clear Error Messages
Section titled “Clear Error Messages”// ✅ Good - clear messagepublic function message(): string{ return 'Password must be at least 8 characters with uppercase, lowercase, number, and special character';}
// ❌ Bad - vague messagepublic function message(): string{ return 'Invalid password';}Reusable Attributes
Section titled “Reusable Attributes”// ✅ Good - reusable#[Attribute(Attribute::TARGET_PROPERTY)]class MinWords extends ValidationAttribute
// ❌ Bad - specific#[Attribute(Attribute::TARGET_PROPERTY)]class ProductDescriptionMinWords extends ValidationAttributeSee Also
Section titled “See Also”- Validation - Built-in validation
- Custom Casts - Custom type casts
- Custom Attributes - Custom attributes