Validation Attributes
PHP 8 attributes live in Miko\Database\ORM (file ValidationAttributes.php), not a ValidationAttributes sub-namespace. Use #[Pattern] not #[Regex].
ModelValidator is an instance: new ModelValidator() then validate($model) / getErrors(). There is no static ModelValidator::validate().
ModelValidator currently enforces: Required, MaxLength, MinLength, Email, Url, Numeric, Integer, Min, Max, Range, Pattern, In, NotIn, Date, Phone, Json, Confirmed, CreditCard, UniqueValue, Exists.
Available Attributes
| Attribute | Description | Parameters |
|---|---|---|
#[Required] | Field must have a value | message |
#[MaxLength(n)] | Maximum string length | length, message |
#[MinLength(n)] | Minimum string length | length, message |
#[Email] | Valid email format | message |
#[Url] | Valid URL format | message |
#[Range(min, max)] | Numeric range | min, max, message |
#[Pattern(pattern)] | Match regex pattern | pattern, message |
#[UniqueValue(table, column)] | Unique in database | table, column, ignoreId |
#[In(values)] | Value must be in list | values, message |
#[NotIn(values)] | Value must not be in list | values, message |
#[Date] | Valid date format | format, message |
#[Phone] | Valid phone number | message |
Basic Usage
Model Definition
use Miko\Database\ORM\Model;
use Miko\Database\ORM\{
Required, MaxLength, MinLength, Email, Range, Regex, UniqueValue, In
};
class User extends Model
{
#[Required(message: "Name is required")]
#[MaxLength(100, message: "Name cannot exceed 100 characters")]
public string $Name;
#[Required]
#[Email(message: "Please enter a valid email address")]
#[MaxLength(100)]
#[UniqueValue('users', 'Email')]
public string $Email;
#[Required]
#[MinLength(8, message: "Password must be at least 8 characters")]
public string $Password;
#[Range(18, 120, message: "Age must be between 18 and 120")]
public ?int $Age;
#[In(['user', 'admin', 'moderator'], message: "Invalid role")]
public string $Role = 'user';
#[Pattern('/^[0-9]{10,15}$/', message: "Invalid phone number format")]
public ?string $Phone;
}
Validating Model
use Miko\Database\ORM\ModelValidator;
$user = new User();
$user->Name = 'John';
$user->Email = 'invalid-email';
$user->Password = '123';
$user->Age = 15;
$validator = new ModelValidator();
if (!$validator->validate($user)) {
foreach ($result->errors as $field => $messages) {
echo "$field:\n";
foreach ($messages as $message) {
echo " - $message\n";
}
}
}
// Output:
// Email:
// - Please enter a valid email address
// Password:
// - Password must be at least 8 characters
// Age:
// - Age must be between 18 and 120
Attribute Details
#[Required]
Field must have a non-empty value.
#[Required]
public string $Name;
#[Required(message: "Email address is required")]
public string $Email;
#[MaxLength] / #[MinLength]
String length constraints.
#[MaxLength(100)]
public string $Name;
#[MinLength(8)]
public string $Password;
#[MinLength(2, message: "Name must be at least 2 characters")]
#[MaxLength(50, message: "Name cannot exceed 50 characters")]
public string $Username;
#[Email]
Validates email format.
#[Email]
public string $Email;
#[Email(message: "Please provide a valid email address")]
public string $ContactEmail;
#[Url]
Validates URL format.
#[Url]
public ?string $Website;
#[Url(message: "Please enter a valid URL")]
public ?string $ProfileUrl;
#[Range]
Numeric range validation.
#[Range(1, 100)]
public int $Quantity;
#[Range(0, 5, message: "Rating must be between 0 and 5")]
public float $Rating;
#[Range(min: 18, message: "Must be at least 18 years old")]
public int $Age;
#[Pattern]
Custom pattern matching.
#[Pattern('/^[A-Z]{2}-[0-9]{4}$/')]
public string $Code; // Format: AB-1234
#[Pattern('/^[a-z0-9_]+$/', message: "Username can only contain lowercase letters, numbers, and underscores")]
public string $Username;
#[Pattern('/^\+?[0-9]{10,15}$/', message: "Invalid phone number")]
public ?string $Phone;
#[UniqueValue]
Database uniqueness check.
#[UniqueValue('users', 'Email')]
public string $Email;
// Ignore current record when updating
#[UniqueValue('users', 'Email', ignoreId: 'Id')]
public string $Email;
#[UniqueValue('products', 'SKU', message: "This SKU already exists")]
public string $SKU;
#[In] / #[NotIn]
Value must be (or not be) in a list.
#[In(['draft', 'published', 'archived'])]
public string $Status;
#[In(['user', 'admin', 'moderator'], message: "Invalid role selected")]
public string $Role;
#[NotIn(['admin', 'superadmin'], message: "This role is reserved")]
public string $Role;
#[Date]
Date format validation.
#[Date]
public ?string $BirthDate;
#[Date(format: 'Y-m-d', message: "Date must be in YYYY-MM-DD format")]
public string $StartDate;
#[Phone]
Phone number validation.
#[Phone]
public ?string $Phone;
#[Phone(message: "Please enter a valid phone number")]
public ?string $MobileNumber;
Combining Attributes
class Product extends Model
{
#[Required]
#[MinLength(3)]
#[MaxLength(200)]
public string $Name;
#[Required]
#[UniqueValue('products', 'SKU')]
#[Pattern('/^[A-Z]{3}-[0-9]{5}$/', message: "SKU format: ABC-12345")]
public string $SKU;
#[Required]
#[Range(0.01, 999999.99, message: "Price must be between $0.01 and $999,999.99")]
public float $Price;
#[Range(0, 10000)]
public int $Stock = 0;
#[In(['active', 'inactive', 'discontinued'])]
public string $Status = 'active';
#[Url]
public ?string $ImageUrl;
#[MaxLength(5000)]
public ?string $Description;
}
Validation in API
class UserController
{
public function store(): void
{
$data = json_decode(file_get_contents('php://input'), true);
$user = new User();
$user->Name = $data['name'] ?? '';
$user->Email = $data['email'] ?? '';
$user->Password = $data['password'] ?? '';
$user->Age = $data['age'] ?? null;
$user->Role = $data['role'] ?? 'user';
// Validate using attributes
$validator = new ModelValidator();
if (!$validator->validate($user)) {
JsonResponse::validationError($validator->getErrors());
return;
}
// Hash password before saving
$user->Password = password_hash($user->Password, PASSWORD_DEFAULT);
$user->save();
JsonResponse::created($user->only('Id', 'Name', 'Email', 'Role'));
}
public function update(int $id): void
{
$user = User::find($id);
if (!$user) {
JsonResponse::notFound('User not found');
return;
}
$data = json_decode(file_get_contents('php://input'), true);
// Update fields
if (isset($data['name'])) $user->Name = $data['name'];
if (isset($data['email'])) $user->Email = $data['email'];
if (isset($data['age'])) $user->Age = $data['age'];
// Validate with ignore for unique check
$validator = new ModelValidator();
if (!$validator->validate($user)) {
JsonResponse::validationError($validator->getErrors());
return;
}
$user->save();
JsonResponse::success($user->only('Id', 'Name', 'Email', 'Role'));
}
}
Custom Validation Attribute
Create your own validation attribute:
use Attribute;
use Miko\Database\ORM\Pattern;
#[Attribute(Attribute::TARGET_PROPERTY)]
class CreditCard extends ValidationAttribute
{
public function __construct(
public string $message = "Invalid credit card number"
) {}
public function validate(mixed $value, string $propertyName): ?string
{
if ($value === null || $value === '') {
return null; // Let Required handle empty values
}
// Luhn algorithm
$number = preg_replace('/\D/', '', $value);
if (strlen($number) < 13 || strlen($number) > 19) {
return $this->message;
}
$sum = 0;
$length = strlen($number);
$parity = $length % 2;
for ($i = 0; $i < $length; $i++) {
$digit = (int)$number[$i];
if ($i % 2 === $parity) {
$digit *= 2;
if ($digit > 9) {
$digit -= 9;
}
}
$sum += $digit;
}
if ($sum % 10 !== 0) {
return $this->message;
}
return null;
}
}
// Usage
class Payment extends Model
{
#[Required]
#[CreditCard(message: "Please enter a valid credit card number")]
public string $CardNumber;
}
Validation Result
$validator = new ModelValidator();
$ok = $validator->validate($model);
if ($ok) {
// All validations passed
}
$allErrors = $validator->getErrors();
// Get errors for specific field $emailErrors = $result->getErrors('Email'); // ['Invalid email format']
// Get first error for field $firstError = $result->getFirstError('Email'); // 'Invalid email format'
// Check if field has errors if ($result->hasErrors('Email')) { // Email has validation errors }