Skip to content

Validation ​

Overview ​

Nexphant provides two validation approaches:

  1. Data Objects (recommended) - Type-safe DTOs with attribute-based rules
  2. Manual Validation - Fluent rule builders for dynamic validation

Data Object Validation ​

Define Rules ​

php
use Nexphant\Support\Data;
use Nexphant\Validation\Rules\{Email, Length, Required};

class RegisterData extends Data
{
    #[Email]
    #[Required]
    public string $email;

    #[Length(min: 3, max: 100)]
    #[Required]
    public string $name;

    #[Length(min: 8)]
    #[Required]
    public string $password;
}

Auto Validation ​

php
$app->post('/register', function (RegisterData $data) {
    // Already validated
    return ['success' => true];
});

Manual Validation ​

Basic Usage ​

php
use Nexphant\Validation\Rule;

$app->post('/submit', function (Request $request) {
    $validated = $request->validate([
        'email' => Rule::email()->required(),
        'name' => Rule::string()->min(3)->max(100)->required(),
        'age' => Rule::integer()->min(18)->max(120),
    ]);
    
    return ['data' => $validated];
});

Available Rules ​

String Rules ​

php
Rule::string()
    ->min(3)
    ->max(100)
    ->length(10)              // Exact length
    ->alpha()                 // Only letters
    ->alphaNum()              // Letters and numbers
    ->alphaNumDash()          // Letters, numbers, dashes, underscores
    ->lowercase()
    ->uppercase()
    ->trim()
    ->pattern('/^[a-z0-9]+$/') // Regex
    ->startsWith('prefix')
    ->endsWith('suffix')
    ->contains('substring')
    ->required();

Email ​

php
Rule::email()
    ->required()
    ->unique('users', 'email'); // Check uniqueness

Integer ​

php
Rule::integer()
    ->min(0)
    ->max(100)
    ->between(1, 10)
    ->positive()
    ->negative()
    ->required();

Float ​

php
Rule::float()
    ->min(0.0)
    ->max(100.0)
    ->between(0.0, 10.0)
    ->decimals(2)             // Max 2 decimal places
    ->required();

Boolean ​

php
Rule::boolean()
    ->required();

Array ​

php
Rule::array()
    ->min(1)                  // Min items
    ->max(10)                 // Max items
    ->unique()                // All items unique
    ->required();

In (Enum) ​

php
Rule::in(['active', 'inactive', 'pending'])
    ->required();

Date ​

php
Rule::date()
    ->format('Y-m-d')
    ->before('2025-01-01')
    ->after('2024-01-01')
    ->required();

URL ​

php
Rule::url()
    ->https()                 // HTTPS only
    ->required();

File ​

php
Rule::file()
    ->maxSize(2048)           // KB
    ->mimes(['jpg', 'png'])
    ->image()
    ->required();

Required ​

php
Rule::required();

Optional ​

php
Rule::string()->optional();

Nullable ​

php
Rule::string()->nullable();

Nested Validation ​

Nested Objects ​

php
$request->validate([
    'user' => [
        'name' => Rule::string()->required(),
        'email' => Rule::email()->required(),
    ],
    'address' => [
        'street' => Rule::string()->required(),
        'city' => Rule::string()->required(),
    ],
]);

Array of Objects ​

php
$request->validate([
    'items' => Rule::array()->min(1),
    'items.*.name' => Rule::string()->required(),
    'items.*.quantity' => Rule::integer()->min(1),
]);

Custom Rules ​

Callback ​

php
Rule::string()->custom(function ($value) {
    if (!preg_match('/^[A-Z]/', $value)) {
        return 'Must start with uppercase letter';
    }
    return null; // Valid
});

Custom Rule Class ​

php
use Nexphant\Validation\Rules\CustomRule;

class StrongPasswordRule extends CustomRule
{
    public function validate($value): ?string
    {
        if (strlen($value) < 8) {
            return 'Password must be at least 8 characters';
        }
        
        if (!preg_match('/[A-Z]/', $value)) {
            return 'Password must contain uppercase letter';
        }
        
        if (!preg_match('/[a-z]/', $value)) {
            return 'Password must contain lowercase letter';
        }
        
        if (!preg_match('/[0-9]/', $value)) {
            return 'Password must contain number';
        }
        
        return null;
    }
}

// Usage
$request->validate([
    'password' => new StrongPasswordRule(),
]);

Conditional Rules ​

php
$rules = [
    'email' => Rule::email()->required(),
];

if ($request->json('subscribe')) {
    $rules['newsletter'] = Rule::boolean()->required();
}

$validated = $request->validate($rules);

Error Messages ​

Default ​

json
{
    "error": "Validation failed",
    "errors": {
        "email": ["Invalid email format"],
        "password": ["Must be at least 8 characters"]
    }
}

Custom Messages ​

php
Rule::email()
    ->required()
    ->message('email', 'Please provide a valid email address')
    ->message('required', 'Email is required');

Validation Exceptions ​

php
use Nexphant\Validation\ValidationException;

try {
    $validated = $request->validate($rules);
} catch (ValidationException $e) {
    return [
        'error' => 'Validation failed',
        'errors' => $e->errors(),
    ];
}

Complete Examples ​

Registration Form ​

php
$app->post('/register', function (Request $request) {
    $validated = $request->validate([
        'email' => Rule::email()
            ->required()
            ->unique('users', 'email'),
        
        'name' => Rule::string()
            ->min(3)
            ->max(100)
            ->required(),
        
        'password' => Rule::string()
            ->min(8)
            ->required(),
        
        'password_confirm' => Rule::string()
            ->same('password')
            ->required(),
        
        'age' => Rule::integer()
            ->min(18)
            ->required(),
        
        'terms' => Rule::boolean()
            ->accepted()
            ->required(),
    ]);
    
    $userId = User::query()->insertGetId([
        'email' => $validated['email'],
        'name' => $validated['name'],
        'password' => password_hash($validated['password'], PASSWORD_DEFAULT),
        'age' => $validated['age'],
    ]);
    
    return ['id' => $userId];
});

Update Profile ​

php
$app->put('/profile', function (Request $request) {
    $userId = auth()->id();
    
    $validated = $request->validate([
        'name' => Rule::string()->min(3)->max(100)->optional(),
        'bio' => Rule::string()->max(500)->optional(),
        'website' => Rule::url()->optional(),
        'age' => Rule::integer()->min(18)->max(120)->optional(),
    ]);
    
    if (!empty($validated)) {
        User::query()
            ->where('id', $userId)
            ->update($validated);
    }
    
    return ['success' => true];
})->middleware('auth');

File Upload ​

php
$app->post('/upload', function (Request $request) {
    $validated = $request->validate([
        'avatar' => Rule::file()
            ->image()
            ->maxSize(2048)
            ->mimes(['jpg', 'jpeg', 'png'])
            ->required(),
    ]);
    
    $file = $request->file('avatar');
    $path = $file->store('avatars');
    
    return ['path' => $path];
})->middleware('auth');

Search with Filters ​

php
$app->get('/search', function (Request $request) {
    $validated = $request->validate([
        'q' => Rule::string()->min(2)->required(),
        'page' => Rule::integer()->min(1)->optional(),
        'per_page' => Rule::integer()->min(1)->max(100)->optional(),
        'sort' => Rule::in(['created_at', 'updated_at', 'title'])->optional(),
        'order' => Rule::in(['asc', 'desc'])->optional(),
        'tags' => Rule::array()->optional(),
    ]);
    
    $query = Post::query()
        ->where('title', 'LIKE', '%' . $validated['q'] . '%');
    
    if (isset($validated['tags'])) {
        $query->whereIn('tag', $validated['tags']);
    }
    
    $query->orderBy(
        $validated['sort'] ?? 'created_at',
        $validated['order'] ?? 'desc'
    );
    
    return $query->paginate(
        $validated['page'] ?? 1,
        $validated['per_page'] ?? 20
    );
});

Immutable Rules ​

All rule builders are immutable:

php
$base = Rule::string()->min(3);

$emailRule = $base->email(); // New instance
$lengthRule = $base->max(100); // New instance

// $base unchanged

Benefits:

  • Fiber safe
  • Thread safe
  • Reusable
  • Predictable

Best Practices ​

  1. Use Data Objects for controllers
  2. Use manual validation for dynamic forms
  3. Validate early before processing
  4. Provide clear messages for users
  5. Sanitize input after validation
  6. Validate file uploads with size/type limits
  7. Use unique() rule for duplicate prevention
  8. Implement rate limiting on forms

Next Steps ​

Released under the MIT License.