Skip to content

Commit d4f1e9e

Browse files
author
Julien Neuhart
committed
WIP: validation documentation
1 parent c43faba commit d4f1e9e

File tree

5 files changed

+447
-0
lines changed

5 files changed

+447
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: Overview
3+
slug: /validation/overview
4+
---
5+
6+
The Symfony Boilerplate centralizes the validation in the API.
7+
This approach has many benefits:
8+
9+
* No need to rewrite the validation logic for all your front-ends.
10+
* The web application focus on UI logic, not business logic.
11+
12+
In the next chapters, we explain how it works and how you should implement it.
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
---
2+
title: Models
3+
slug: /validation/models
4+
---
5+
6+
Most of the time, you want to validate a `Model`'s data.
7+
8+
In the Symfony Boilerplate, we distinguish three kinds of models:
9+
10+
1. The [TDBM](https://github.com/thecodingmachine/tdbm) models for business data.
11+
2. The `Storable` models for uploads.
12+
3. The `Proxy` models for data that do not fit in previous scenarios.
13+
14+
## TDBM Models
15+
16+
### Migrations
17+
18+
The first stone for validating your [TDBM](https://github.com/thecodingmachine/tdbm) models occurs in
19+
the Doctrine migrations.
20+
21+
:::note
22+
23+
📣  See the [Database](http://localhost) chapter for more details about Doctrine migrations.
24+
25+
:::
26+
27+
For instance, let's take a look at the Doctrine migration for the `users` table:
28+
29+
```php
30+
public function up(Schema $schema): void
31+
{
32+
$db = new TdbmFluidSchema($schema);
33+
34+
$db->table('users')
35+
->column('id')->guid()->primaryKey()->comment('@UUID')->graphqlField()
36+
->column('first_name')->string(255)->notNull()->graphqlField()
37+
->column('last_name')->string(255)->notNull()->graphqlField()
38+
->column('email')->string(255)->notNull()->unique()->graphqlField()
39+
->column('password')->string(255)->null()->default(null);
40+
}
41+
```
42+
43+
Here we are already defining rules:
44+
45+
* Scalar values (string, int, etc.).
46+
* Nullable or not.
47+
* Default values.
48+
* Unique values.
49+
* GraphQL fields (i.e., values available in the GraphQL API).
50+
51+
### PHP Classes
52+
53+
Of course, most of these rules are not user-friendly nor developer-friendly as they occur on the database level.
54+
55+
Yet, after applying this migration, [TDBM](https://github.com/thecodingmachine/tdbm) is able to generate two PHP classes:
56+
57+
* `BaseUser`.
58+
* `User` that extends `BaseUser`.
59+
60+
Let's take a look at the constructor's signature from the `BaseUser` class:
61+
62+
```php title="src/api/src/Domain/Model/Generated/BaseUser.php"
63+
public function __construct(string $firstName, string $lastName, string $email, string $locale, string $role);
64+
```
65+
66+
Here we can see that non-nullable properties are **mandatory**. Also, all the properties have an **explicit type**.
67+
68+
For getters and setters, it works the same:
69+
70+
```php title="src/api/src/Domain/Model/Generated/BaseUser.php"
71+
public function getFirstName(): string;
72+
public function setFirstName(string $firstName): void;
73+
```
74+
75+
Overall, it greatly improves the developer experience as you cannot put a wrong type nor miss a mandatory property when
76+
creating/updating an instance of a [TDBM](https://github.com/thecodingmachine/tdbm) model. 😉
77+
78+
### Annotations
79+
80+
That being said, it's still not enough. For instance, how to make sure a value is unique? Or a string is not superior to
81+
256 characters?
82+
83+
You could let the database tell you about these issues, but that's usually done in a non developer-friendly way.
84+
85+
Thankfully, the [Symfony Validation](https://symfony.com/doc/current/validation.html) bundle provides most of the rules
86+
(a.k.a. constraints) you may want to apply to a [TDBM](https://github.com/thecodingmachine/tdbm) model's property.
87+
88+
You may also add your own rules.
89+
90+
:::note
91+
92+
📣  See the [Constraints](https://symfony.com/doc/current/validation.html#constraints) chapter of the official
93+
documentation for the list of available rules.
94+
95+
:::
96+
97+
:::note
98+
99+
📣  The folder *src/api/src/Domain/Constraint* contains our custom-made constraints.
100+
101+
:::
102+
103+
As we cannot modify the `BaseUser` class, we have to override the getters in the `User` class.
104+
105+
For instance, let's take a look at the `email` property getter:
106+
107+
```php title="src/api/src/Domain/Model/User.php"
108+
use Symfony\Component\Validator\Constraints as Assert;
109+
use TheCodingMachine\GraphQLite\Annotations\Field;
110+
111+
/**
112+
* @Field
113+
* @Assert\NotBlank(message="not_blank")
114+
* @Assert\Length(max=255, maxMessage="max_length_255")
115+
* @Assert\Email(message="invalid_email")
116+
*/
117+
public function getEmail(): string
118+
{
119+
return parent::getEmail();
120+
}
121+
```
122+
123+
In addition to type hint (non-nullable string), we add three rules:
124+
125+
1. The email cannot be blank.
126+
2. The email cannot have a length superior to 255 characters.
127+
3. The email has to be valid.
128+
129+
:::note
130+
131+
📣  The message attribute contains a translation key. See the [i18n](http://localhost) chapter for more
132+
details.
133+
134+
:::
135+
136+
:::note
137+
138+
📣  Don't forget to add the `@Field` annotation if the property should be available in GraphQL. Indeed,
139+
when overriding a getter, [GraphQLite](https://graphqlite.thecodingmachine.io/) does not parse anymore the annotation
140+
from the parent getter.
141+
:::
142+
143+
You may also add a validation annotation to the class itself:
144+
145+
```php title="src/api/src/Domain/Model/User.php"
146+
use App\Domain\Constraint as DomainAssert;
147+
use TheCodingMachine\GraphQLite\Annotations\Type;
148+
149+
/*
150+
* @Type
151+
* @DomainAssert\Unicity(table="users", column="email", message="user.email_not_unique")
152+
*/
153+
class User extends BaseUser {}
154+
```
155+
156+
In this scenario, we use our custom-made `Unicity` constraint that verify if a value does not already exist in the
157+
database.
158+
159+
### DAOs
160+
161+
In addition to the model classes, [TDBM](https://github.com/thecodingmachine/tdbm) also generates the DAO classes.
162+
163+
Like the models, there are two of them:
164+
165+
* `BaseUserDao`.
166+
* `UserDao` that extends `BaseDao`.
167+
168+
In the later, we have to inject a `ValidatorInterface`:
169+
170+
```php title="src/api/src/Domain/Dao/UserDao.php"
171+
use Symfony\Component\Validator\Validator\ValidatorInterface;
172+
use TheCodingMachine\TDBM\TDBMService;
173+
174+
class UserDao extends BaseUserDao
175+
{
176+
private ValidatorInterface $validator;
177+
178+
public function __construct(TDBMService $tdbmService, ValidatorInterface $validator)
179+
{
180+
$this->validator = $validator;
181+
parent::__construct($tdbmService);
182+
}
183+
}
184+
```
185+
186+
The `ValidatorInterface` provides the method `validate` that returns the list of all violations according to the model
187+
constraints:
188+
189+
```php
190+
/** User $user */
191+
$violations = $this->validator->validate($user);
192+
```
193+
194+
By convention, it's great to add a `validate` method in your DAOs:
195+
196+
```php title="src/api/src/Domain/Dao/UserDao.php"
197+
use App\Domain\Throwable\InvalidModel;
198+
199+
/**
200+
* @throws InvalidModel
201+
*/
202+
public function validate(User $user): void
203+
{
204+
$violations = $this->validator->validate($user);
205+
InvalidModel::throwException($violations);
206+
}
207+
```
208+
209+
This method throws an exception if there are any violations in the model.
210+
211+
Last but not least, you should override the `save` method from the base DAO:
212+
213+
```php title="src/api/src/Domain/Dao/UserDao.php"
214+
/**
215+
* @throws InvalidModel
216+
*/
217+
public function save(User $user): void
218+
{
219+
$this->validate($user);
220+
parent::save($user);
221+
}
222+
```
223+
224+
This approach has two HUGE benefits:
225+
226+
1. You centralize the action of validating at one place.
227+
2. You **always** validate a model before saving it in the database.
228+
229+
## Storable Models
230+
231+
:::note
232+
233+
📣  See the [Uploads](http://localhost) chapter for more details about uploads storage.
234+
235+
:::
236+
237+
### PHP Class
238+
239+
A storable is a wrapper around an upload. You may want to validate its extension, size, etc.
240+
241+
Most of the time, you extend the `Storable` class with a custom class:
242+
243+
```php title="src/api/src/Domain/Model/Storable/MyStorable.php"
244+
final class MyStorable extends Storable {}
245+
```
246+
247+
Here you may override or add custom getters.
248+
249+
Indeed, like the [TDBM](https://github.com/thecodingmachine/tdbm) models, a storable
250+
may uses Symfony Validation annotations. 😉
251+
252+
For instance, let's say you want to validate the upload's extension:
253+
254+
```php title="src/api/src/Domain/Model/Storable/MyStorable.php"
255+
use Symfony\Component\Validator\Constraints as Assert;
256+
257+
/**
258+
* @Assert\Choice({"png", "jpg"}, message="my_storable.invalid_extensions")
259+
*/
260+
public function getExtension(): string
261+
{
262+
return parent::getExtension();
263+
}
264+
```
265+
266+
### Storage
267+
268+
A storage is like a DAO but for storables.
269+
270+
It provides methods for validating one or more storables:
271+
272+
```php title="src/api/src/Domain/Storage/Storage.php"
273+
use App\Domain\Throwable\InvalidStorable;
274+
275+
/**
276+
* @param Storable[] $storables
277+
*
278+
* @throws InvalidStorable
279+
*/
280+
public function validateAll(array $storables): void;
281+
282+
/**
283+
* @throws InvalidStorable
284+
*/
285+
public function validate(Storable $storable): void;
286+
```
287+
288+
Like the `save` method from a `DAO`, its `write` and `writeAll` methods also call the validation methods:
289+
290+
```php title="src/api/src/Domain/Storage/Storage.php"
291+
use App\Domain\Throwable\InvalidStorable;
292+
293+
/**
294+
* @param Storable[] $storables
295+
*
296+
* @return string[]
297+
*
298+
* @throws InvalidStorable
299+
*/
300+
public function writeAll(array $storables): array;
301+
302+
/**
303+
* @throws InvalidStorable
304+
*/
305+
public function write(Storable $storable): string;
306+
```
307+
308+
## Proxy Models
309+
310+
Proxy models are PHP classes that does not reflect a database's table nor an upload.
311+
312+
In other words, they are plain old PHP objects.
313+
314+
However, you may use Symfony Validation annotations on these models getters and validate them
315+
using the `ValidatorInterface` and `InvalidModel` classes. 😉
316+
317+
:::note
318+
319+
📣  Don't forget to add the `@Type` and `@Field` annotations if the model should be available in GraphQL.
320+
321+
:::

0 commit comments

Comments
 (0)