Validation - Grazulex/laravel-arc GitHub Wiki
✅ Validation
Overview
Laravel Arc provides powerful validation capabilities built directly into your model properties. This eliminates the need for separate validation logic and ensures data integrity at the model level.
Basic Validation
Automatic Validation
Validation rules are automatically generated from your property attributes:
class User extends Model
{
use HasArcProperties;
#[Property(
type: 'string',
required: true,
minLength: 2,
maxLength: 100
)]
public string $name;
#[Property(
type: 'email',
required: true
)]
public string $email;
#[Property(
type: 'integer',
required: false,
min: 18,
max: 120
)]
public ?int $age = null;
}
// Validation happens automatically on save
$user = new User();
$user->name = 'A'; // Too short - will fail validation
$user->email = 'invalid-email'; // Invalid format - will fail
$user->age = 150; // Too high - will fail
try {
$user->save(); // ValidationException thrown
} catch (ValidationException $e) {
$errors = $e->errors();
}
Manual Validation
// Validate data before assignment
$data = [
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 30
];
$user = new User();
$validator = $user->validateData($data);
if ($validator->fails()) {
$errors = $validator->errors();
// Handle validation errors
} else {
// Data is valid - safe to assign
$user->fill($data);
$user->save();
}
Validation Rules
Type-Based Rules
Different property types automatically include appropriate validation rules:
class Example extends Model
{
use HasArcProperties;
// String validation
#[Property(type: 'string', minLength: 5, maxLength: 50)]
public string $title; // Rules: required|string|min:5|max:50
// Numeric validation
#[Property(type: 'integer', min: 1, max: 100)]
public int $score; // Rules: required|integer|min:1|max:100
#[Property(type: 'float', min: 0.0, max: 999.99)]
public float $price; // Rules: required|numeric|min:0|max:999.99
// Email validation
#[Property(type: 'email')]
public string $email; // Rules: required|email
// URL validation
#[Property(type: 'url')]
public string $website; // Rules: required|url
// Date validation
#[Property(type: 'date')]
public Carbon $birth_date; // Rules: required|date
#[Property(type: 'datetime')]
public Carbon $created_at; // Rules: required|date
// Boolean validation
#[Property(type: 'boolean')]
public bool $is_active; // Rules: required|boolean
// JSON validation
#[Property(type: 'json')]
public array $metadata; // Rules: required|array
// Enum validation
#[Property(type: 'enum', enum: UserStatus::class)]
public UserStatus $status; // Rules: required|in:active,inactive,pending
}
Custom Validation Rules
class User extends Model
{
use HasArcProperties;
// Using Laravel's built-in rules
#[Property(
type: 'string',
rules: ['required', 'string', 'unique:users,username']
)]
public string $username;
// Custom rule with parameters
#[Property(
type: 'string',
rules: ['required', 'string', 'regex:/^[A-Z][a-z]+$/']
)]
public string $first_name;
// Complex validation with closure
#[Property(
type: 'string',
rules: [
'required',
'string',
function ($attribute, $value, $fail) {
if (str_contains(strtolower($value), 'admin')) {
$fail('The username cannot contain "admin".');
}
}
]
)]
public string $display_name;
// Custom validation class
#[Property(
type: 'string',
rules: [StrongPasswordRule::class]
)]
public string $password;
}
Pattern Validation
class Product extends Model
{
use HasArcProperties;
// Regex pattern validation
#[Property(
type: 'string',
pattern: '^[A-Z]{3}-\d{4}$', // Format: ABC-1234
patternMessage: 'Product code must be in format ABC-1234'
)]
public string $product_code;
// Phone number pattern
#[Property(
type: 'string',
pattern: '^\+?[1-9]\d{1,14}$',
patternMessage: 'Please enter a valid phone number'
)]
public string $phone;
// Credit card pattern (simplified)
#[Property(
type: 'string',
pattern: '^\d{4}-?\d{4}-?\d{4}-?\d{4}$',
patternMessage: 'Credit card must be 16 digits'
)]
public string $credit_card;
}
Advanced Validation
Conditional Validation
class Order extends Model
{
use HasArcProperties;
#[Property(type: 'string', required: true)]
public string $payment_method;
// Required only if payment_method is 'credit_card'
#[Property(
type: 'string',
rules: [
'required_if:payment_method,credit_card',
'string',
'size:16'
]
)]
public ?string $card_number = null;
// Required only if payment_method is 'bank_transfer'
#[Property(
type: 'string',
rules: [
'required_if:payment_method,bank_transfer',
'string',
'min:10',
'max:34'
]
)]
public ?string $iban = null;
}
Cross-Field Validation
class Event extends Model
{
use HasArcProperties;
#[Property(type: 'datetime', required: true)]
public Carbon $start_date;
#[Property(
type: 'datetime',
required: true,
rules: ['after:start_date']
)]
public Carbon $end_date;
#[Property(
type: 'string',
rules: [
'required',
'different:start_date' // End date must be different from start
]
)]
public string $description;
}
Array Validation
class Survey extends Model
{
use HasArcProperties;
// Validate array structure
#[Property(
type: 'json',
rules: [
'required',
'array',
'min:1', // At least one item
'max:10' // Maximum 10 items
]
)]
public array $questions;
// Validate nested array elements
#[Property(
type: 'json',
rules: [
'required',
'array',
'questions.*.title' => 'required|string|max:200',
'questions.*.type' => 'required|in:text,multiple_choice,rating',
'questions.*.required' => 'boolean'
]
)]
public array $question_details;
}
Validation Messages
Custom Messages
class User extends Model
{
use HasArcProperties;
// Simple custom message
#[Property(
type: 'string',
required: true,
minLength: 8,
message: 'Please provide a strong password with at least 8 characters'
)]
public string $password;
// Multiple custom messages
#[Property(
type: 'string',
required: true,
minLength: 2,
maxLength: 50,
messages: [
'required' => 'Name is required and cannot be empty',
'min' => 'Name must be at least 2 characters long',
'max' => 'Name cannot exceed 50 characters'
]
)]
public string $name;
// Localized messages
#[Property(
type: 'email',
required: true,
message: 'validation.custom.email' // References lang file
)]
public string $email;
}
Dynamic Messages
class Product extends Model
{
use HasArcProperties;
#[Property(
type: 'float',
min: 0.01,
max: 9999.99,
messages: [
'min' => 'Price must be at least $0.01',
'max' => 'Price cannot exceed $9,999.99'
]
)]
public float $price;
// Message with dynamic values
#[Property(
type: 'integer',
min: 1,
max: 100,
message: function ($attribute, $value, $parameters) {
return "The {$attribute} must be between {$parameters[0]} and {$parameters[1]}.";
}
)]
public int $quantity;
}
Validation Groups
Scenario-Based Validation
class User extends Model
{
use HasArcProperties;
#[Property(
type: 'string',
required: true,
groups: ['create', 'update']
)]
public string $name;
#[Property(
type: 'email',
required: true,
groups: ['create', 'update']
)]
public string $email;
#[Property(
type: 'string',
required: true,
groups: ['create'], // Only required during creation
minLength: 8
)]
public string $password;
#[Property(
type: 'datetime',
groups: ['profile_update'] // Only validated during profile updates
)]
public ?Carbon $last_login = null;
}
// Validate specific group
$user = new User();
$validator = $user->validateData($data, 'create');
// Validate multiple groups
$validator = $user->validateData($data, ['create', 'profile_update']);
Integration with Laravel
Form Requests
class CreateUserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
// Get validation rules from the model
return User::getValidationRules();
}
public function messages()
{
// Get custom messages from the model
return User::getValidationMessages();
}
}
// Enhanced form request with groups
class UpdateUserRequest extends FormRequest
{
public function rules()
{
// Get rules for specific validation group
return User::getValidationRules('update');
}
}
Controller Integration
class UserController extends Controller
{
public function store(Request $request)
{
// Automatic validation using Arc rules
$user = new User();
$validator = $user->validateData($request->all());
if ($validator->fails()) {
return response()->json([
'errors' => $validator->errors()
], 422);
}
$user->fill($request->all());
$user->save();
return response()->json($user, 201);
}
public function update(Request $request, User $user)
{
// Validate with specific group
$validator = $user->validateData($request->all(), 'update');
if ($validator->fails()) {
return response()->json([
'errors' => $validator->errors()
], 422);
}
$user->update($request->all());
return response()->json($user);
}
}
API Resource Validation
class UserResource extends JsonResource
{
public function toArray($request)
{
// Include validation errors if available
$array = [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
// Add validation status for API consumers
if ($this->hasValidationErrors()) {
$array['validation_errors'] = $this->getValidationErrors();
}
return $array;
}
}
Testing Validation
Unit Tests
class UserValidationTest extends TestCase
{
public function test_name_is_required()
{
$user = new User();
$validator = $user->validateData(['email' => '[email protected]']);
$this->assertTrue($validator->fails());
$this->assertArrayHasKey('name', $validator->errors()->toArray());
}
public function test_email_must_be_valid()
{
$user = new User();
$validator = $user->validateData([
'name' => 'John Doe',
'email' => 'invalid-email'
]);
$this->assertTrue($validator->fails());
$this->assertArrayHasKey('email', $validator->errors()->toArray());
}
public function test_valid_data_passes_validation()
{
$user = new User();
$validator = $user->validateData([
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 30
]);
$this->assertFalse($validator->fails());
}
}
Feature Tests
class UserApiTest extends TestCase
{
public function test_create_user_with_invalid_data()
{
$response = $this->postJson('/api/users', [
'name' => '', // Invalid - required
'email' => 'not-an-email', // Invalid format
'age' => 150 // Invalid - too high
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['name', 'email', 'age']);
}
public function test_create_user_with_valid_data()
{
$response = $this->postJson('/api/users', [
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 30
]);
$response->assertStatus(201)
->assertJsonStructure([
'id', 'name', 'email', 'age', 'created_at', 'updated_at'
]);
}
}
Performance Optimization
Validation Caching
// Enable validation rule caching in config
'validation' => [
'cache_rules' => true,
'cache_duration' => 3600, // 1 hour
],
// Or per model
class User extends Model
{
use HasArcProperties;
protected array $arcConfig = [
'cache_validation_rules' => true,
];
}
Selective Validation
// Validate only specific fields
$user = new User();
$validator = $user->validateData($data, null, ['name', 'email']);
// Skip validation for certain fields
$validator = $user->validateData($data, null, [], ['password']);
Best Practices
1. Keep Validation Close to Data
// Good: Validation rules with property definition
#[Property(
type: 'string',
required: true,
minLength: 3,
maxLength: 50,
pattern: '^[a-zA-Z\s]+$'
)]
public string $name;
2. Use Meaningful Error Messages
// Good: Clear, user-friendly messages
#[Property(
type: 'string',
required: true,
minLength: 8,
messages: [
'required' => 'Password is required for account security',
'min' => 'Password must be at least 8 characters for security'
]
)]
public string $password;
3. Group Related Validations
// Good: Use validation groups for different scenarios
#[Property(
type: 'string',
required: true,
groups: ['registration', 'profile_update']
)]
public string $name;
#[Property(
type: 'string',
required: true,
groups: ['registration'], // Only required during registration
minLength: 8
)]
public string $password;
4. Test Edge Cases
// Test boundary conditions
public function test_age_boundary_validation()
{
$user = new User();
// Test minimum boundary
$validator = $user->validateData(['age' => 17]); // Should fail
$this->assertTrue($validator->fails());
$validator = $user->validateData(['age' => 18]); // Should pass
$this->assertFalse($validator->fails());
// Test maximum boundary
$validator = $user->validateData(['age' => 120]); // Should pass
$this->assertFalse($validator->fails());
$validator = $user->validateData(['age' => 121]); // Should fail
$this->assertTrue($validator->fails());
}
Next Steps
- 🎯 Learn about Type Safety features
- 🔧 Explore Property Attributes for more options
- 🎨 Check out Advanced Features
- 🔍 Try Smart Validation Rules
- 📚 See Examples for real-world validation scenarios
Validation made simple! ✨ Laravel Arc's validation system ensures data integrity while keeping your code clean and maintainable.