Skip to content

Commit b6f63fa

Browse files
Merge branch 'c-v-c-v-feature/from-data-attribute'
2 parents ef29408 + 5f86904 commit b6f63fa

29 files changed

+1276
-156
lines changed

docs/advanced-usage/filling from-route-parameters.md

-121
This file was deleted.

docs/as-a-data-transfer-object/factories.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Factories
3-
weight: 11
3+
weight: 12
44
---
55

66
It is possible to automatically create data objects in all sorts of forms with this package. Sometimes a little bit more
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
---
2+
title: Injecting property values
3+
weight: 11
4+
---
5+
6+
When creating a data object, it is possible to inject values into properties from all kinds of sources like route
7+
parameters, the current user or dependencies in the container.
8+
9+
## Filling properties from a route parameter
10+
11+
When creating data objects from requests, it's possible to automatically fill data properties from request route
12+
parameters, such as route models.
13+
14+
The `FromRouteParameter` attribute allows filling properties with route parameter values.
15+
16+
### Using scalar route parameters
17+
18+
```php
19+
Route::patch('/songs/{songId}', [SongController::class, 'update']);
20+
21+
class SongData extends Data {
22+
#[FromRouteParameter('songId')]
23+
public int $id;
24+
public string $name;
25+
}
26+
```
27+
28+
Here, the `$id` property will be filled with the `songId` route parameter value (which most likely is a string or
29+
integer).
30+
31+
### Using Models, objects or arrays as route parameters
32+
33+
Given that we have a route to create songs for a specific author, and that the `{author}` route parameter uses route
34+
model binding to automatically bind to an `Author` model:
35+
36+
```php
37+
Route::post('/songs/{artist}', [SongController::class, 'store']);
38+
39+
class SongData extends Data {
40+
public int $id;
41+
#[FromRouteParameter('artist')]
42+
public ArtistData $author;
43+
}
44+
```
45+
46+
Here, the `$artist` property will be filled with the `artist` route parameter value, which will be an instance of the
47+
`Artist` model. Note that the package will automatically cast the model to `ArtistData`.
48+
49+
## Filling properties from route parameter properties
50+
51+
The `FromRouteParameterProperty` attribute allows filling properties with values from route parameter properties. The
52+
main difference from `FromRouteParameter` is that the former uses the full route parameter value, while
53+
`FromRouteParameterProperty` uses a single property from the route parameter.
54+
55+
In the example below, we're using route model binding. `{song}` represents an instance of the `Song` model.
56+
`FromRouteParameterProperty` automatically attempts to fill the `SongData` `$id` property from `$song->id`.
57+
58+
```php
59+
Route::patch('/songs/{song}', [SongController::class, 'update']);
60+
61+
class SongData extends Data {
62+
#[FromRouteParameterProperty('song')]
63+
public int $id;
64+
public string $name;
65+
}
66+
```
67+
68+
### Using custom property mapping
69+
70+
In the example below, `$name` property will be filled with `$song->title` (instead of `$song->name).
71+
72+
```php
73+
Route::patch('/songs/{song}', [SongController::class, 'update']);
74+
75+
class SongData extends Data {
76+
#[FromRouteParameterProperty('song')]
77+
public int $id;
78+
#[FromRouteParameterProperty('song', 'title')]
79+
public string $name;
80+
}
81+
```
82+
83+
### Nested property mapping
84+
85+
Nested properties are supported as well. Here, we fill `$singerName` from `$artist->leadSinger->name`:
86+
87+
```php
88+
Route::patch('/artists/{artist}/songs/{song}', [SongController::class, 'update']);
89+
90+
class SongData extends Data {
91+
#[FromRouteParameterProperty('song')]
92+
public int $id;
93+
#[FromRouteParameterProperty('artist', 'leadSinger.name')]
94+
public string $singerName;
95+
}
96+
```
97+
98+
## Route parameters take priority over request body
99+
100+
By default, route parameters take priority over values in the request body. For example, when the song ID is present in
101+
the route model as well as request body, the ID from route model is used.
102+
103+
```php
104+
Route::patch('/songs/{song}', [SongController::class, 'update']);
105+
106+
// PATCH /songs/123
107+
// { "id": 321, "name": "Never gonna give you up" }
108+
109+
class SongData extends Data {
110+
#[FromRouteParameterProperty('song')]
111+
public int $id;
112+
public string $name;
113+
}
114+
```
115+
116+
Here, `$id` will be `123` even though the request body has `321` as the ID value.
117+
118+
In most cases, this is useful - especially when you need the ID for a validation rule. However, there may be cases when
119+
the exact opposite is required.
120+
121+
The above behavior can be turned off by switching the `replaceWhenPresentInPayload` flag off. This can be useful when
122+
you _intend_ to allow updating a property that is present in a route parameter, such as a slug:
123+
124+
```php
125+
Route::patch('/songs/{slug}', [SongController::class, 'update']);
126+
127+
// PATCH /songs/never
128+
// { "slug": "never-gonna-give-you-up", "name": "Never gonna give you up" }
129+
130+
class SongData extends Data {
131+
#[FromRouteParameter('slug', replaceWhenPresentInPayload: false )]
132+
public string $slug;
133+
}
134+
```
135+
136+
Here, `$slug` will be `never-gonna-give-you-up` even though the route parameter value is `never`.
137+
138+
## Filling properties from the authenticated user
139+
140+
The `FromCurrentUser` attribute allows filling properties with values from the authenticated user.
141+
142+
```php
143+
class SongData extends Data {
144+
#[FromAuthenticatedUser]
145+
public UserData $user;
146+
}
147+
```
148+
149+
It is possible to specify the guard to use when fetching the user:
150+
151+
```php
152+
class SongData extends Data {
153+
#[FromAuthenticatedUser('api')]
154+
public UserData $user;
155+
}
156+
```
157+
158+
Just like with route parameters, it is possible to fill properties with specific user properties using
159+
`FromAuthenticatedUserProperty`:
160+
161+
```php
162+
class SongData extends Data {
163+
#[FromAuthenticatedUserProperty('name')]
164+
public string $username;
165+
}
166+
```
167+
168+
All the other features like custom property mapping and not replacing values when present in the payload are supported
169+
as well.
170+
171+
## Filling properties from the container
172+
173+
The `FromContainer` attribute allows filling properties with dependencies from the container.
174+
175+
```php
176+
class SongData extends Data {
177+
#[FromContainer(SongService::class)]
178+
public SongService $song_service;
179+
}
180+
```
181+
182+
When a dependency requires additional parameters these can be provided as such:
183+
184+
```php
185+
class SongData extends Data {
186+
#[FromContainer(SongService::class, parameters: ['year' => 1984])]
187+
public SongService $song_service;
188+
}
189+
```
190+
191+
It is even possible to completely inject the container itself:
192+
193+
```php
194+
class SongData extends Data {
195+
#[FromContainer]
196+
public Container $container;
197+
}
198+
```
199+
200+
Selecting a property from a dependency can be done using `FromContainerProperty`:
201+
202+
```php
203+
class SongData extends Data {
204+
#[FromContainerProperty(SongService::class, 'name')]
205+
public string $service_name;
206+
}
207+
```
208+
209+
Again, all the other features like custom property mapping and not replacing values when present in the payload are
210+
supported as well.
211+
212+
## Creating your own injectable attributes
213+
214+
All the attributes we saw earlier implement the `InjectsPropertyValue` interface:
215+
216+
```php
217+
interface InjectsPropertyValue
218+
{
219+
public function resolve(
220+
DataProperty $dataProperty,
221+
mixed $payload,
222+
array $properties,
223+
CreationContext $creationContext
224+
): mixed;
225+
226+
public function shouldBeReplacedWhenPresentInPayload() : bool;
227+
}
228+
```
229+
230+
It is possible to create your own attribute by implementing this interface. The `resolve` method is responsible for
231+
returning the value that should be injected into the property. The `shouldBeReplacedWhenPresentInPayload` method should
232+
return `true` if the value should be replaced when present in the payload.

phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ parameters:
120120
count: 1
121121
path: src/Support/DataConfig.php
122122

123+
-
124+
message: "#^Unsafe usage of new static\\(\\)\\.$#"
125+
count: 1
126+
path: src/Support/Skipped.php
127+
123128
-
124129
message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#"
125130
count: 1

0 commit comments

Comments
 (0)