Validation
Attach validation rules with the #[Rule] attribute. Pass a single rule string, a
pipe-delimited string, or a list:
use BYanelli\Roma\Request\Attributes\Rule;
readonly class CreateContactRequest {
#[Rule('max:255')]
public string $name;
#[Rule(['email', 'unique:contacts', 'max:255'])]
public string $email;
}
Type coercion and enum/nested-object validation are applied automatically on top of your
rules — a public int $page is validated as an integer without you writing integer.
Dynamic rules
A #[Rule] argument can also be a first-class-callable reference. Roma calls it through the
container at validation time, so the rule can depend on runtime state — a service, the
current user, config. It may return a single rule or a list, which is spread in place:
use BYanelli\Roma\Request\Attributes\Rule;
readonly class UpdateBioRequest {
public function __construct(
#[Rule('string', self::maxLength(...))]
public string $bio = '',
) {}
public static function maxLength(BioSettings $settings): string {
return "max:{$settings->limit}";
}
}
Guards
Mark a method #[Guard] to run it after validation passes. Guards are called through the
container, so they can type-hint dependencies and have them injected. A guard rejects the
request by throwing; its return value is ignored. Multiple guards run in declaration order.
use BYanelli\Roma\Request\Attributes\Guard;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Container\Attributes\CurrentUser;
readonly class UpdatePostRequest {
public function __construct(
public int $postId,
public string $body,
) {}
#[Guard]
public function authorize(#[CurrentUser] User $user): void {
if ($user->cannot('update', Post::findOrFail($this->postId))) {
throw new AuthorizationException;
}
}
}
Class-level constraints
Some accessor and header attributes can be applied at the class level to enforce a global requirement on every request mapped to the class:
use BYanelli\Roma\Request\Attributes\Accessors\Ajax;
use BYanelli\Roma\Request\Attributes\Headers\ContentType;
use BYanelli\Roma\Request\Enums\ContentType as ContentTypeEnum;
#[Ajax] // requires an AJAX request
#[ContentType(ContentTypeEnum::Json)] // requires a JSON Content-Type
class ApiOnlyRequest {
public string $data;
}
Error keys
When validation fails, Roma throws Laravel's ValidationException with errors keyed by a
source-prefixed, request-relative name, so the caller always knows where the offending
value belongs:
input.price— merged input (query + body)query.page/body.token— a#[Query]/#[Body]propertyheader.X-Flag— a header, by its real (un-normalized) nameroute.id— a#[RouteParameter]propertycookie.session— a#[Cookie]propertyrequest.ajax— request metadata from an accessor
Nested fields keep their full path and array elements are indexed (input.address.city,
input.items.1.code). The one exception is a precognitive request,
whose errors are keyed by the bare posted field name for front-end form tooling.
Nullable and optional
A non-nullable property with no default is required. Give it a default to make it
optional; making the type nullable (?T) also makes it optional — an absent or
explicitly-null key resolves to null (Roma applies nullable rather than required).
class ProductSearchRequest {
public string $name; // required
public int $perPage = 15; // optional (has a default)
public ?string $search; // optional; null when absent or null
}
Use #[Present] on a nullable property when the key must appear but may be null:
use BYanelli\Roma\Request\Attributes\Present;
readonly class UpdateNoteRequest {
#[Present]
public ?string $note; // must be sent, but may be null
}