The framework provides a thin request-validation layer on top of the existing router and entity tooling. Core checks route through Marwa\Support\Validation. Use validate_request() for quick checks or extend Marwa\Framework\Validation\FormRequest for reusable request objects.
The validation system is built with a modular architecture:
Marwa\Framework\Validation\
├── RequestValidator.php # Backward-compatible entrypoint
└── FormRequest.php # Form request base class
Marwa\Support\Validation\
├── RuleRegistry.php # Rule registration and resolution
├── RequestValidator.php # Canonical validator
├── ValidationException.php # Canonical exception
└── Rules\ # Built-in rules
| Rule | Description | Example |
|——|————-|———|
| required | Field must be present and not empty | required |
| present | Field must be present in input | present |
| filled | Field must not be empty if present | filled |
| string | Value must be a string | string |
| integer | Value must be an integer | integer |
| numeric | Value must be numeric | numeric |
| boolean | Value must be true/false | boolean |
| array | Value must be an array | array |
| email | Valid email address | email |
| url | Valid URL | url |
| file | Uploaded file | file |
| image | Image file | image |
| accepted | Value must be accepted (yes, on, 1, true) | accepted |
| declined | Value must be declined (no, off, 0, false) | declined |
| Rule | Description | Example |
|——|————-|———|
| min | Minimum value/length | min:3 |
| max | Maximum value/length | max:10 |
| between | Value between range | between:1,10 |
| in | Value must be in list | in:foo,bar,baz |
| same | Value must match another field | same:password_confirm |
| confirmed | Field must have _confirmation match | password with password_confirmation |
| Rule | Description | Example |
|——|————-|———|
| date | Valid date | date |
| date_format | Match specific format | date_format:Y-m-d |
| regex | Match regex pattern | regex:/^[a-z]+$/ |
| Rule | Description | Example |
|——|————-|———|
| trim | Trim whitespace | trim |
| lowercase | Convert to lowercase | lowercase |
| uppercase | Convert to uppercase | uppercase |
| default | Default value if empty | default:value |
| Rule | Description |
|——|————-|
| nullable | Field can be null |
| sometimes | Validate only if present |
| bail | Stop on first validation failure |
$data = validate_request([
'title' => 'trim|required|string|min:3',
'email' => 'required|email',
'published' => 'boolean',
]);
The helper reads from the current PSR-7 request, validates the payload, and returns normalized data. Invalid requests throw Marwa\Support\Validation\ValidationException, which the router middleware converts into a JSON 422 response or a redirect with flashed input.
use Marwa\Framework\Validation\RequestValidator;
$validator = new RequestValidator();
$data = $validator->validateInput([
'title' => 'Hello World',
'email' => 'test@example.com',
], [
'title' => 'required|string|min:3',
'email' => 'required|email',
]);
This is the full flow for a custom rule:
Marwa\Support\Validation\RuleRegistry.<?php
namespace App\Validation\Rules;
use Marwa\Support\Validation\AbstractRule;
final class StrongPasswordRule extends AbstractRule
{
public function name(): string
{
return 'strong_password';
}
public function validate(mixed $value, array $context): bool
{
return is_string($value)
&& strlen($value) >= 12
&& preg_match('/[A-Z]/', $value)
&& preg_match('/[a-z]/', $value)
&& preg_match('/[0-9]/', $value)
&& preg_match('/[^A-Za-z0-9]/', $value);
}
public function message(string $field, array $attributes): string
{
return $this->formatMessage(
'The :attribute must be at least 12 characters and include upper, lower, number, and symbol.',
$field,
$attributes
);
}
}
use App\Validation\Rules\StrongPasswordRule;
use Marwa\Support\Validation\RuleRegistry;
$registry = app(RuleRegistry::class);
$registry->register('strong_password', StrongPasswordRule::class);
use Marwa\Framework\Validation\RequestValidator;
$validator = app(RequestValidator::class);
$data = $validator->validateInputWithCustomRules(
[
'password' => 'Secret123!@#',
'password_confirmation' => 'Secret123!@#',
],
[
'password' => 'required|strong_password|confirmed',
],
[],
[],
[
'strong_password' => StrongPasswordRule::class,
]
);
If validation fails, catch the support exception:
use Marwa\Support\Validation\ValidationException;
try {
$validator->validateInputWithCustomRules(...);
} catch (ValidationException $e) {
$errors = $e->errors()->all();
}
use Marwa\Framework\Validation\FormRequest;
final class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string|min:3',
'email' => 'required|email',
'published' => 'boolean',
];
}
public function messages(): array
{
return [
'title.required' => 'Please provide a title.',
'email.email' => 'Please provide a valid email address.',
];
}
public function attributes(): array
{
return [
'email' => 'email address',
];
}
protected function prepareForValidation(array $input): array
{
$input['title'] = trim((string) ($input['title'] ?? ''));
return $input;
}
protected function passedValidation(array $validated): array
{
// Add computed fields
$validated['slug'] = strtolower(str_replace(' ', '-', $validated['title']));
return $validated;
}
}
Use validated() or validate() inside your controller:
$data = (new StorePostRequest(request()))->validated();
// or
$data = (new StorePostRequest(request()))->validate();
You can also use closures as rules:
$data = $validator->validateInput([
'age' => 25,
], [
'age' => [
'required',
function ($value, $input, $field) {
if ($value < 18) {
return 'You must be at least 18 years old.';
}
return true;
},
],
]);
Validation failures flash two session keys:
_old_input for the submitted payloaderrors for the validation error bagThe old() helper reads back flashed input in your views:
// Get all old input
$old = old();
// Get specific field
$title = old('title');
// Get with default
$name = old('name', 'Anonymous');
Access validation errors from the exception:
try {
$data = validate_request($rules);
} catch (\Marwa\Support\Validation\ValidationException $e) {
$errors = $e->errors()->all();
// ['title' => ['The title field is required.']]
}