Skip to content

Commit d0071c0

Browse files
committed
fix blogpost
1 parent 9ff28d7 commit d0071c0

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
---
2+
title: Request Objects in Tempest
3+
description: Why Tempest requests are super intuitive
4+
author: Brent
5+
category: Discovering Tempest
6+
---
7+
8+
Tempest's tagline is "the framework that gets out of your way". One of the best examples of that principle in action is request validation. A pattern I learned to appreciate over the years was to represent "raw data" (like for example, request data), as typed objects in PHP — so-called "data transfer objects". The sooner I have a typed object within my app's lifecycle, the sooner I have a bunch of guarantees about that data, which makes coding a lot easier.
9+
10+
For example: not having to worry about whether the "title of the book" is actually present in the request's body. If we have an object of `BookData`, and that object has a typed property `public string $title` then we don't have to worry about adding extra `isset` or `null` checks, and fallbacks all over the place.
11+
12+
Data transfer objects aren't unheard of in frameworks like [Symfony](https://symfony.com/blog/new-in-symfony-6-3-mapping-request-data-to-typed-objects) or [Laravel](https://spatie.be/docs/laravel-data/v4/introduction), although Tempest takes it a couple of steps further. In Tempest, the starting point of "the request validation flow" is _that_ data object, because _that object_ is what we're _actually_ interested in.
13+
14+
Here's what such a data object looks like:
15+
16+
```php
17+
final class BookData
18+
{
19+
public string $title;
20+
21+
public string $description;
22+
23+
public ?DateTimeImmutable $publishedAt = null;
24+
}
25+
```
26+
27+
It doesn't get much simpler than this, right? We have an object representing the fields we expect from the request. Now how do we get the request data into that object? There are several ways of doing so. I'll start by showing the most verbose way, mostly to understand what's going on. This approach makes use of the `map()` function. Tempest has a built-in [mapper component](/docs/framework/mapper), which is responsible to map data from one point to another. It could from an array to an object, object to json, one class to another, … Or, in our case: the request to our data object.
28+
29+
Here's what that looks like in practice:
30+
31+
```php
32+
use Tempest\Router\Request;
33+
use function Tempest\map;
34+
35+
final readonly class BookController
36+
{
37+
#[Post('/books')]
38+
public function store(Request $request): Redirect
39+
{
40+
$bookData = map($request)->to(BookData::class);
41+
42+
// Do something with that book data
43+
}
44+
}
45+
```
46+
47+
We have a controller action to store a book, we _inject_ the `Request` class into that action (this class can be injected everywhere when we're running a web app). Next, we map the request to our `BookData` class, and… that's it! We have a validated book object:
48+
49+
```php
50+
/*
51+
* Book {
52+
* title: Timeline Taxi
53+
* description: Brent's newest sci-fi novel
54+
* publishedAt: 2024-10-01 00:00:00
55+
* }
56+
*/
57+
```
58+
59+
Now, hang on — _validated_? Yes, that's what I mean when I say that "Tempest gets out of your way": `BookData` uses typed properties, which means we can infer a lot of validation rules from those type signatures alone: `title` and `description` are required since these aren't nullable properties, they should both be text; `publishedAt` is optional, and it expects a valid date time string to be passed via the request.
60+
61+
Tempest infers all this information just by looking at the object itself, without you having to hand-hold the framework every step of the way. There are of course validation attributes for rules that can't be inferred by the type definition itself, but you already get a lot out of the box just by using types.
62+
63+
```php
64+
use Tempest\Validation\Rules\DateTimeFormat;
65+
use Tempest\Validation\Rules\Length;
66+
67+
final class BookData
68+
{
69+
#[Length(min: 5, max: 50)]
70+
public string $title;
71+
72+
public string $description;
73+
74+
#[DateTimeFormat('Y-m-d')]
75+
public ?DateTimeImmutable $publishedAt = null;
76+
}
77+
```
78+
79+
This kind of validation also works with nested objects, by the way. Here's for example an `Author` class:
80+
81+
```php
82+
use Tempest\Validation\Rules\Length;
83+
use Tempest\Validation\Rules\Email;
84+
85+
final class Author
86+
{
87+
#[Length(min: 2)]
88+
public string $name;
89+
90+
#[Email]
91+
public string $email;
92+
}
93+
```
94+
95+
Which can be used on the `Book` class:
96+
97+
```php
98+
final class Book
99+
{
100+
#[Length(min: 2)]
101+
public string $title;
102+
103+
public string $description;
104+
105+
public ?DateTimeImmutable $publishedAt = null;
106+
107+
public Author $author;
108+
}
109+
```
110+
111+
Now any request mapped to `Book` will expect the `author.name` and `author.email` fields to be present as well.
112+
113+
114+
## Request Objects
115+
116+
With validation out of the way, let's take a look at other approaches of mapping request data to objects. Since request objects are such a common use case, Tempest allows you to make custom request implementations. There's only a very small difference between a standalone data object and a request object though: a request object implements the `Request` interface. Tempest also provides a `IsRequest` trait that will take care of all the interface-related code. This interface/trait combination is a pattern you'll see all throughout Tempest, it's a very deliberate choice instead of relying on abstract classes, but that's a topic for another day.
117+
118+
Here's what our `BookRequest` looks like:
119+
120+
```php
121+
use Tempest\Router\IsRequest;
122+
use Tempest\Router\Request;
123+
124+
final class BookRequest implements Request
125+
{
126+
use IsRequest;
127+
128+
#[Length(min: 5, max: 50)]
129+
public string $title;
130+
131+
public string $description;
132+
133+
// …
134+
}
135+
```
136+
137+
With this request class, we can now simply inject it, and we're done. No more mapping from the request to the data object. Of course, Tempest has taken care of validation as well: by the time you've reached the controller, you're certain that whatever data is present, is also valid.
138+
139+
```php
140+
use function Tempest\map;
141+
142+
final readonly class BookController
143+
{
144+
#[Post('/books')]
145+
public function store(BookRequest $request): Redirect
146+
{
147+
// Do something with the request
148+
}
149+
}
150+
```
151+
152+
## Mapping to models
153+
154+
You might be thinking: a request can be mapped to virtually any kind of object. What about models then? Indeed. Requests can be mapped to models directly as well! Let's do some quick setup work.
155+
156+
First we add `database.config.php`, Tempest will discover it, so you can place it anywhere you like. In this example we'll use sqlite as our database:
157+
158+
```php
159+
// app/database.config.php
160+
161+
use Tempest\Database\Config\SQLiteConfig;
162+
163+
return new SQLiteConfig(
164+
path: __DIR__ . '/database.sqlite'
165+
);
166+
```
167+
168+
Next, create a migration. For the sake of simplicity I like to use raw SQL migrations. You can read more about them [here](/docs/framework/models/#migrations). These are discovered as well, so you can place them wherever suits you:
169+
170+
```sql
171+
-- app/Migrations/CreateBookTable.sql
172+
173+
CREATE TABLE `Books`
174+
(
175+
`id` INTEGER PRIMARY KEY,
176+
`title` TEXT NOT NULL,
177+
`description` TEXT NOT NULL,
178+
`publishedAt` DATETIME
179+
)
180+
```
181+
182+
Next, we'll create a `Book` class, which implements `DatabaseModel` and uses the `IsDatabaseModel` trait:
183+
184+
```php
185+
use Tempest\Database\IsDatabaseModel;
186+
use Tempest\Database\DatabaseModel;
187+
188+
final class Book implements DatabaseModel
189+
{
190+
use IsDatabaseModel;
191+
192+
public string $title;
193+
194+
public string $description;
195+
196+
public ?DateTimeImmutable $publishedAt = null;
197+
}
198+
```
199+
200+
Then we run our migrations:
201+
202+
```console
203+
~ tempest migrate:up
204+
205+
<em>Migrate up…</em>
206+
- 0000-00-00_create_migrations_table
207+
- CreateBookTable_0
208+
209+
<success>Migrated 2 migrations</success>
210+
```
211+
212+
And, finally, we create our controller class, this time mapping the request straight to the `Book`:
213+
214+
```php
215+
use function Tempest\map;
216+
217+
final readonly class BookController
218+
{
219+
#[Post('/books')]
220+
public function store(Request $request): Redirect
221+
{
222+
$book = map($request)->to(Book::class);
223+
224+
$book->save();
225+
226+
// …
227+
}
228+
}
229+
```
230+
231+
And that is all! Pretty clean, right?

0 commit comments

Comments
 (0)