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

  1. 🎯 Learn about Type Safety features
  2. 🔧 Explore Property Attributes for more options
  3. 🎨 Check out Advanced Features
  4. 🔍 Try Smart Validation Rules
  5. 📚 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.