php
61 lines · 6 steps
Typed request DTOs in Laravel
A readonly data object validates and normalizes an HTTP request into an immutable, typed shape.
Explained by
highlit
1<?php
2
3namespace App\Http\Requests\DataObjects;
4
5use App\Enums\Currency;
6use Illuminate\Contracts\Support\Arrayable;
7use Illuminate\Http\Request;
8
9final readonly class CreateInvoiceData implements Arrayable
10{
11 public function __construct(
12 public string $customerId,
13 public Currency $currency,
14 public int $amountInCents,
15 public ?string $reference,
16 public array $lineItems,
17 public ?string $notes,
18 ) {
19 }
20
21 public static function fromRequest(Request $request): self
22 {
23 $validated = $request->validate([
24 'customer_id' => ['required', 'uuid'],
25 'currency' => ['required', 'string', 'size:3'],
26 'amount' => ['required', 'numeric', 'min:0.01'],
27 'reference' => ['nullable', 'string', 'max:64'],
28 'line_items' => ['required', 'array', 'min:1'],
29 'line_items.*.description' => ['required', 'string'],
30 'line_items.*.quantity' => ['required', 'integer', 'min:1'],
31 'notes' => ['nullable', 'string'],
32 ]);
33
34 return new self(
35 customerId: $validated['customer_id'],
36 currency: Currency::from(strtoupper($validated['currency'])),
37 amountInCents: (int) round($validated['amount'] * 100),
38 reference: $validated['reference'] ?? null,
39 lineItems: array_map(
40 static fn (array $item): array => [
41 'description' => trim($item['description']),
42 'quantity' => (int) $item['quantity'],
43 ],
44 $validated['line_items'],
45 ),
46 notes: $validated['notes'] ?? null,
47 );
48 }
49
50 public function toArray(): array
51 {
52 return [
53 'customer_id' => $this->customerId,
54 'currency' => $this->currency->value,
55 'amount_in_cents' => $this->amountInCents,
56 'reference' => $this->reference,
57 'line_items' => $this->lineItems,
58 'notes' => $this->notes,
59 ];
60 }
61}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A dedicated DTO turns loose request arrays into a typed, immutable contract your app can trust.
- 2Centralizing validation and normalization in one factory keeps controllers thin and consistent.
- 3Implementing Arrayable gives a clean, explicit mapping back to array form for storage or responses.
Related explainers
python
from fastapi import FastAPI, Request, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse
Redacting sensitive fields in FastAPI errors
validation
error-handling
security
Intermediate
7 steps
php
<?php namespace App\Jobs;
Rendering invoice PDFs in a Laravel queue job
queued jobs
pdf generation
file storage
Intermediate
9 steps
typescript
type Masker = (value: string) => string; const maskEmail: Masker = (value) => { const [local, domain] = value.split("@");
Recursively masking sensitive data for logs
recursion
regex
data-masking
Intermediate
9 steps
php
<?php namespace App\Services;
Building a valid iCalendar feed in PHP in Laravel
ical
serialization
formatting
Intermediate
8 steps
php
<?php namespace App\View\Components;
Building a breadcrumb component in Laravel
blade components
url parsing
string manipulation
Intermediate
8 steps
java
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TenantResolutionFilter extends OncePerRequestFilter {
How a tenant-resolution filter works in Spring
multi-tenancy
servlet-filter
thread-local
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/typed-request-dtos-in-laravel-explained-php-4941/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.