# Introduction

Roma is a **Request/Response Object MApper** for Laravel. It maps _all_ aspects of an
`Illuminate\Http\Request` — headers, the query string, the body, files, cookies, route
parameters, and convenience methods like `$request->ajax()` — into a fully type-safe,
validated plain PHP object. The goal: when you use a Roma request, you never touch the
underlying Laravel request directly.

On the response side, Roma converts a plain object (recursively) into the JSON body of a
`JsonResponse`, with properties that can instead drive the status code or headers.

And with one command, Roma generates TypeScript definitions for both requests and
responses — one source of truth shared by your backend and frontend.

## When to reach for Roma

**When an endpoint needs typed, validated input from anywhere in the request, reach for a
Roma request object instead of a hand-rolled `FormRequest` plus a manual array-to-DTO
step.** You declare typed properties; Roma populates and validates them. It is a type-safe
`FormRequest` and DTO in one.

Mark the class `#[Request]` and type-hint it in your controller — Roma maps and validates
it before your action runs:

```php
use BYanelli\Roma\Request\Attributes\Rule;
use BYanelli\Roma\Request\ContextualBinding\Request;

#[Request]
readonly class CreateContactRequest {
    public function __construct(
        #[Rule('max:255')]
        public string $name,

        #[Rule(['email', 'unique:contacts', 'max:255'])]
        public string $email,
    ) {}
}

class CreateContactController {
    public function __invoke(CreateContactRequest $request) {
        Contact::create([
            'name'  => $request->name,
            'email' => $request->email,
        ]);
    }
}
```

The rest of these docs walk through defining requests, the sources a property can bind to,
headers and request metadata, nested objects, response objects, and TypeScript generation.

## These docs as Markdown

Prefer plain text? The entire documentation is served as a single Markdown file at
[yanelli.dev/docs/roma.md](/docs/roma.md) — hand it to an AI coding assistant, or just read
everything in one place. Each version has its own too (e.g.
[/docs/roma/v1.md](/docs/roma/v1.md)).

---

# Request objects

A request object is a class whose typed properties you want populated from the request.
Constructor-promoted properties and plain class properties can be used interchangeably.

```php
use BYanelli\Roma\Request\Attributes\Rule;

readonly class CreateContactRequest {
    public function __construct(
        #[Rule('max:255')]
        public string $name,

        #[Rule(['email', 'unique:contacts', 'max:255'])]
        public string $email,
    ) {}

    #[Rule('phone')]
    public string $phone;
}
```

## Inject it into your controller

Mark the request _class_ with `#[Request]` and type-hint it in your controller. Roma
resolves it from the container, maps the request onto it, and validates it before your
action runs:

```php
use BYanelli\Roma\Request\ContextualBinding\Request;

#[Request]
readonly class CreateContactRequest { /* properties as above */ }

class CreateContactController {
    public function __invoke(CreateContactRequest $request) {
        Contact::create([
            'name'  => $request->name,
            'email' => $request->email,
            'phone' => $request->phone,
        ]);
    }
}
```

Marking the class is also what lets the [TypeScript generator](/docs/roma/v1/typescript)
auto-detect it as a request. Auto-injection is on by default; to require an explicit
attribute everywhere, set `auto_inject` to `false` in `config/roma.php`.

### Annotating the parameter instead

`#[Request]` also works on the controller parameter rather than the class. Both forms work
and can coexist — reach for the parameter attribute when auto-injection is off, or for a
one-off request you'd rather not mark at the class level:

```php
use BYanelli\Roma\Request\ContextualBinding\Request;

class CreateContactController {
    public function __invoke(#[Request] CreateContactRequest $request) {
        // ...
    }
}
```

## Share properties with traits

Common properties can be factored into a trait and mixed into any request:

```php
use BYanelli\Roma\Request\Attributes\Rule;

trait HasPagination {
    #[Rule('integer|min:1')]
    public int $page = 1;

    #[Rule('integer|min:1|max:100')]
    public int $perPage = 15;
}

class ProductListRequest {
    use HasPagination;

    public ?string $search;
}
```

---

# Response objects

Responses are the mirror of requests. Where a request property has a _source_ it's pulled
_from_, a response property has a _destination_ it's pushed _to_. By default that
destination is the JSON body; a property can instead be lifted to the status code or a
header.

## Define and return

Extend `Response`, declare typed public properties, and return the object. Roma serializes
it to a `JsonResponse`:

```php
use BYanelli\Roma\Response\Response;

class UserResponse extends Response {
    public function __construct(
        public string $name,
        public int $age,
    ) {}
}

class ShowUserController {
    public function __invoke(): UserResponse {
        return new UserResponse('Bill', 40);
    }
}
```

If a class already extends something else, use the traits directly: `IsResponsable` for a
full HTTP response, or `IsArrayable` alone for a nested value that only needs to serialize.

## Value conversion

Property values are converted to their JSON form on the way out, recursively — backed enums
become `{ name, value }`, unit enums their name, `DateTimeInterface` an ISO-8601 string,
nested response objects and arrays recurse element by element.

## Omit unset properties with `#[Optional]`

A response property has no implicit default: leaving it unset makes serialization throw,
surfacing a field you forgot to populate. Mark it `#[Optional]` to omit it when unset, or
give it an explicit default to serialize that value:

```php
use BYanelli\Roma\Response\Attributes\Optional;
use BYanelli\Roma\Response\Response;

class ContactResponse extends Response {
    public string $name;

    #[Optional]
    public ?string $nickname;    // omitted entirely when unset

    public ?string $note = null; // an explicit default serializes as null
}
```

## Status and headers

Mark an `int` property `#[Status]` to make its value the HTTP status code, or a property
`#[Header('Name')]` to emit it as a response header. Both are lifted out of the body:

```php
use BYanelli\Roma\Response\Attributes\Header;
use BYanelli\Roma\Response\Attributes\Status;
use BYanelli\Roma\Response\Response;

class CreatedResponse extends Response {
    public string $name = 'Bill';

    #[Status]
    public int $status = 201; // response is 201; body is {"name":"Bill"}

    #[Header('Cache-Control')]
    public string $cacheControl = 'max-age=3600';
}
```

Without a `#[Status]` property the response defaults to 200. Date formatting can be
customized per property with `#[DateFormat]`. For values computed at runtime, override the
`responseStatus()`, `responseHeaders()`, or `dateFormat()` methods.

---

# TypeScript generation

Roma generates TypeScript definitions for your request and response objects, so the
frontend and backend share one source of truth. Run:

```bash
php artisan roma:typescript
```

It writes a `.d.ts` file (default `resources/js/roma.d.ts`, overridable with `--output` or
config) containing an interface for every request and response.

## What gets generated

A request is split into up to three interfaces — one per HTTP location its properties come
from — named `{Name}Body`, `{Name}Query`, and `{Name}Headers`; empty ones are dropped. A
response produces a `{Name}Body`, plus a `{Name}Headers` when it emits `#[Header]`s. Fields
are keyed by their **wire key** (the source key, or a `#[Key]`/header name), and optional
properties get a `?`.

```php
#[Request]
readonly class SearchRequest {
    public function __construct(
        public string $note,                          // default (input) -> Body
        #[Query] public int $page = 1,                // -> Query (optional)
        #[Header('X-Api-Key')] public string $apiKey, // -> Headers
    ) {}
}
```

generates:

```typescript
export interface SearchRequestBody {
  note: string;
}

export interface SearchRequestHeaders {
  'X-Api-Key': string;
}

export interface SearchRequestQuery {
  page?: number;
}
```

Enums become a named `const` of `{ name, value }` objects plus a union type, emitted ahead
of the interfaces that use them.

## Auto-detection

Classes are discovered by scanning the directories in `roma.typescript.discover` (default
`app/`) — there is no list to maintain by hand:

* a **request** is any class marked with a class-level `#[Request]` attribute;
* a **response** is any class extending `Response` or using the `IsResponsable` trait.

The `requests` and `responses` config lists are an additive escape hatch for classes
outside the scanned directories.

## Renaming a type

A generated type takes its short class name by default. Override it with `#[TypeScriptName]`
when the short name would collide. An `#[Input]` property defaults to the `Body` interface
(it reads from both body and query); force it into `Query` with
`#[InputMapsToTypeScriptQuery]`.

---

# Installation

Install Roma via Composer:

```bash
composer require byanelli/roma
```

Roma registers its service provider automatically. That's enough to start defining request
and response objects.

## Configuration

Publish the config file if you want to tune auto-injection, the TypeScript output, or
Precognition behaviour:

```bash
php artisan vendor:publish --tag=roma-config
```

This writes `config/roma.php`:

```php
return [
    // Inject a request object by type-hint alone, without the parameter-level
    // #[Request] attribute. See "Request objects".
    'auto_inject' => true,

    'typescript' => [
        // Where the generated .d.ts file is written.
        'output' => resource_path('js/roma.d.ts'),

        // Directories scanned to auto-detect request and response classes.
        'discover' => [app_path()],

        // Additional classes to include beyond what discovery finds.
        'requests' => [],
        'responses' => [],
    ],

    'precognition' => [
        // Keep Roma's source-prefixed error keys under Precognition, instead of
        // the bare field names front-end helpers expect.
        'source_prefixed_errors' => false,
    ],
];
```

Each setting is covered in the section it relates to.

---

# Validation

Attach validation rules with the `#[Rule]` attribute. Pass a single rule string, a
pipe-delimited string, or a list:

```php
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:

```php
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.

```php
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:

```php
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]` property
* `header.X-Flag` — a header, by its real (un-normalized) name
* `route.id` — a `#[RouteParameter]` property
* `cookie.session` — a `#[Cookie]` property
* `request.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](/docs/roma/v1/precognition),
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`).

```php
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`:

```php
use BYanelli\Roma\Request\Attributes\Present;

readonly class UpdateNoteRequest {
    #[Present]
    public ?string $note; // must be sent, but may be null
}
```

---

# Sources

Every property is populated from a **source** — a part of the request. By default a
property reads from the merged input bag (query string + body). Attributes bind it to a
specific source instead.

## Query and body

Use `#[Query]` or `#[Body]` to pin a property to one bag — handy when the same key can
appear in both:

```php
use BYanelli\Roma\Request\Attributes\Body;
use BYanelli\Roma\Request\Attributes\Query;

readonly class SearchRequest {
    #[Query]
    public int $page;     // always from the query string

    #[Body]
    public string $token; // always from the request body
}
```

### The QUERY method

`QUERY` ([RFC 10008](https://www.rfc-editor.org/info/rfc10008/)) is a safe, idempotent
method that carries its input in the request content instead of the URI — "GET with a
body". Roma treats that content as the body, so `#[Body]` and the default input source
both read it, and `#[Query]` still means the query string:

```php
use BYanelli\Roma\Request\Attributes\Query;
use BYanelli\Roma\Request\Enums\Method;

// QUERY /search?page=3  with  {"term": "roma"}
readonly class SearchRequest {
    public string $term;  // from the QUERY body

    #[Query]
    public int $page;     // from the query string

    public Method $method; // Method::Query
}
```

Laravel's `Route::any()` predates the method, so register a QUERY route explicitly:

```php
Route::match(['QUERY'], '/search', SearchController::class);
```

## Route parameters

Bind to a route parameter with `#[RouteParameter]`. The property name is the parameter name
unless you pass an explicit one. Route parameters arrive as strings, so scalar and enum
coercion applies:

```php
use BYanelli\Roma\Request\Attributes\RouteParameter;

// Route: /users/{id}/posts/{post_slug}
readonly class ShowPostRequest {
    #[RouteParameter]
    public int $id;      // from {id}, coerced "42" -> 42

    #[RouteParameter('post_slug')]
    public string $slug; // from {post_slug}
}
```

If the request has no bound route, a required route parameter fails validation with a
`route.` error — it never crashes.

## Cookies

Bind to a cookie with `#[Cookie]`, using the property name or an explicit cookie name.
Cookie names may contain literal dots, so pass one explicitly when the name isn't a valid
PHP property name:

```php
use BYanelli\Roma\Request\Attributes\Cookie;

readonly class PreferencesRequest {
    #[Cookie]
    public bool $darkMode;   // from the "darkMode" cookie

    #[Cookie('my.pref')]
    public string $pref;     // from the "my.pref" cookie
}
```

## Files

Type-hint a property as `Illuminate\Http\UploadedFile` and the upload is mapped:

```php
use Illuminate\Http\UploadedFile;

class FileRequest {
    public UploadedFile $myFile;
}
```

File uploads must be declared on the top-level request class — a `UploadedFile` inside a
nested object is not supported and throws.

## Type coercion and enums

Roma coerces string input to the property's declared type, and maps values onto
string-backed, integer-backed, and unit enums automatically:

```php
class OrderRequest {
    public float $price;                   // "9.99" -> 9.99
    public bool $isGift;                   // "true" -> true
    public \DateTimeInterface $deliverBy;  // "2024-01-01" -> DateTime
    public Status $status;                 // "complete" -> Status::Complete (string enum)

    /** @var array<int> */
    public array $itemIds;                 // ["1","2"] -> [1, 2]
}
```

---

# Headers & metadata

Roma maps request headers and the convenience surface of the Laravel request — the things
you'd normally reach into `$request` for — onto typed properties.

## Headers

Map a header with `#[Header]`, or use a pre-made shortcut like `#[ContentType]`:

```php
use BYanelli\Roma\Request\Attributes\Header;
use BYanelli\Roma\Request\Attributes\Headers\ContentType;

readonly class ApiRequest {
    #[Header('X-API-Key')]
    public string $apiKey;

    #[ContentType]
    public string $contentType;
}
```

## Header value objects

Some headers carry structure, not just a string. Type a property as a header **value
object** and Roma parses the header for you. `Authorization` splits the header into its
scheme and credentials, and self-locates — no attribute needed:

```php
use BYanelli\Roma\Request\Values\Authorization;

readonly class ApiRequest {
    public Authorization $auth;
}
```

```php
$request->auth->scheme;       // AuthScheme::Bearer | Basic | Digest
$request->auth->credentials;  // the raw credentials after the scheme
$request->auth->isBearer();   // convenience check

// Basic credentials, base64-decoded and split — null if not Basic / malformed.
$request->auth->basic()?->username;
$request->auth->basic()?->password;
```

A malformed `Authorization` header is rejected with a clean, header-level validation error.

## Self-sourcing metadata enums

Some metadata has a small fixed set of values. Roma ships enums for these, and typing a
property as one is enough — the enum knows which request source it comes from:

```php
use BYanelli\Roma\Request\Enums\ContentType;
use BYanelli\Roma\Request\Enums\Method;
use BYanelli\Roma\Request\Enums\Scheme;

readonly class MetadataEnumRequest {
    public Method $method;           // Method::Get, Method::Post, Method::Query, ...
    public ContentType $contentType; // ContentType::Json, ... (params stripped before matching)
    public Scheme $scheme;           // Scheme::Http | Https
}
```

An explicit source attribute still wins over the inferred one, and a value outside the
enum's cases can be mapped as a plain string via the accessor/header attribute directly
(e.g. `#[ContentType] public string $contentType`).

## Accessor attributes

Roma wraps most of the Laravel request surface with accessor attributes:

```php
use BYanelli\Roma\Request\Attributes\Accessors\Ip;
use BYanelli\Roma\Request\Attributes\Accessors\Segments;
use BYanelli\Roma\Request\Attributes\Accessors\UserAgent;

readonly class RequestInfo {
    #[Ip]        public string $ip;
    #[UserAgent] public string $userAgent;

    /** @var array<string> */
    #[Segments]  public array $segments;
}
```

* **Booleans:** `#[Ajax]`, `#[Secure]`, `#[Pjax]`, `#[Prefetch]`, `#[IsJson]`, `#[ExpectsJson]`, `#[WantsJson]`
* **Strings:** `#[Method]`, `#[Ip]`, `#[UserAgent]`, `#[Url]`, `#[FullUrl]`, `#[Path]`, `#[DecodedPath]`, `#[Root]`, `#[Host]`, `#[SchemeAndHttpHost]`, `#[BearerToken]`, `#[Format]`
* **Arrays:** `#[Ips]`, `#[Segments]`

Every boolean accessor accepts `mustBe` to become a constraint: `#[Secure(mustBe: true)]`
requires HTTPS, `#[Ajax(mustBe: false)]` requires a non-AJAX request.

---

# Nested objects

Type-hint a property as another plain object to deserialize nested JSON structures:

```php
class Address {
    public string $address;
    public string $city;
    public State $state;
    public string $zipCode;
}

class UserRequest {
    public string $name;
    public string $email;
    public Address $address;
}
```

A nested object inherits its location from the parent property, so source attributes
(`#[Input]`, `#[Query]`, `#[Body]`, `#[Header]`, `#[RouteParameter]`, `#[Cookie]`,
accessors) and self-sourcing metadata enums are only valid on **top-level** request
classes. Declaring one on a nested property throws — its data always comes from within the
parent's slice.

## Nullable nested objects

An absent or null nullable object stays `null`, and its children are _not_ required. But a
_present_ object — even an empty `{}` — is validated, so its required children must be
supplied:

```php
readonly class Address {
    public string $city;
}

readonly class OrderRequest {
    public string $name;
    public ?Address $shipTo; // null when absent; when present, `city` is required
}
```

## Overriding a nested key with `#[Key]`

To override a nested property's key — for example when the client field name contains a
literal dot that can't be a PHP property name — use `#[Key]`. It is nested-only (top-level
properties pass the key to their source attribute instead, e.g. `#[Body('a.b')]`):

```php
use BYanelli\Roma\Request\Attributes\Key;

class Meta {
    #[Key('created.at')] // reads the "created.at" field from the parent's slice
    public string $createdAt;
}

class ArticleRequest {
    public string $title;
    public Meta $meta;
}
```

---

# Laravel Precognition

Roma request objects work with [Laravel Precognition](https://laravel.com/docs/precognition)
out of the box. Add the framework's `HandlePrecognitiveRequests` middleware to the route,
and a request carrying the `Precognition: true` header is validated without running your
controller:

```php
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/signup', SignupController::class)
    ->middleware(HandlePrecognitiveRequests::class);
```

## What gets validated

Precognition is a front-end form concern, so a precognitive request validates **form data
only** — the `input`, `query`, `body`, and `file` sources. Rules for headers, cookies,
route parameters, and request metadata are skipped, and because those values go
unvalidated, the request object is never constructed and `#[Guard]` methods never run.
Everything runs as normal on the real submission.

## Responses and error keys

A failing precognitive request returns the usual `422`, with its form-data errors keyed by
the **bare field name** the client posted (`email`, `address.city`) rather than Roma's
usual [source-prefixed keys](/docs/roma/v1/validation) — so the official
`laravel-precognition-*` front-end helpers map them onto form fields without translation.
Set `roma.precognition.source_prefixed_errors` to `true` to keep the prefixed keys
(`input.email`) under Precognition too.

A passing precognitive request returns an empty `204` with a `Precognition-Success: true`
header.

## Validate-only

When the client narrows validation with a `Precognition-Validate-Only` header — as the
official helpers do on every keystroke — Roma validates only the matching fields. A pattern
matches a field by either name the client might know it by: the bare posted name (`email`,
`items.0.code`) or Roma's source-prefixed key (`input.email`). Fields outside the filter
may be missing or invalid; the request still succeeds if the named fields pass.
