Skip to content

Commit c8cafbb

Browse files
dunglasGregoireHebert
authored andcommitted
Enhance docs for validation and errors
1 parent 054921e commit c8cafbb

File tree

2 files changed

+161
-147
lines changed

2 files changed

+161
-147
lines changed

core/errors.md

Lines changed: 36 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
# Error Handling
22

3-
API Platform Core allows to customize the HTTP status code sent to the clients when exceptions are thrown.
3+
API Platform comes with a powerful error system. It handles excepted (such as faulty JSON documents sent by the
4+
client or validation errors) as well as unexpected errors (PHP exceptions and errors).
5+
API Platform automatically send the appropriate HTTP status code to the client: `400` for expected errors, `500` for
6+
unexpected ones. It also provides a description of the respecting [the Hydra specification](http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors)
7+
or the [RFC 7807](https://tools.ietf.org/html/rfc7807) depending of the format selected during the [content negotiation](content-negotiation.md).
48

5-
```yaml
6-
# app/config/config.yml
7-
8-
api_platform:
9-
# Map exceptions to HTTP status codes using the `exception_to_status` configuration key
10-
exception_to_status:
11-
# The 2 following exceptions are handled by default
12-
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
13-
ApiPlatform\Core\Exception\InvalidArgumentException: 'HTTP_BAD_REQUEST' # Or with a constant of `Symfony\Component\HttpFoundation\Response`
9+
## Converting PHP Exceptions to HTTP Errors
1410

15-
AppBundle\Exception\ProductNotFoundException: 404 # Custom exceptions can easily be handled
16-
```
11+
The framework also allows to configure the HTTP status code sent to the clients when custom exceptions are thrown.
1712

18-
As in any php application, your exceptions have to extends the \Exception class or any of it's children.
13+
In the following example, we will throw explain to throw a domain exception from the business layer of the application and
14+
configure API Platform to convert it to a `404 Not Found` error. Let's create a this domain exception and the service throwing
15+
it:
1916

2017
```php
2118
<?php
22-
2319
// src/AppBundle/Exception/ProductNotFoundException.php
2420

2521
namespace AppBundle\Exception;
@@ -31,7 +27,6 @@ final class ProductNotFoundException extends \Exception
3127

3228
```php
3329
<?php
34-
3530
// src/AppBundle/EventSubscriber/CartManager.php
3631

3732
namespace AppBundle\EventSubscriber;
@@ -46,158 +41,57 @@ use Symfony\Component\HttpKernel\KernelEvents;
4641

4742
final class ProductManager implements EventSubscriberInterface
4843
{
49-
private const DOESNOTEXISTS = 51;
50-
private const DEACTIVATED = 52;
51-
private const OUTOFSTOCK = 53;
52-
53-
public static function getSubscribedEvents()
44+
public static function getSubscribedEvents(): array
5445
{
5546
return [
56-
KernelEvents::REQUEST => [['checkProductAvailability', EventPriorities::POST_DESERIALIZE]],
47+
KernelEvents::REQUEST => ['checkProductAvailability', EventPriorities::POST_DESERIALIZE],
5748
];
5849
}
5950

60-
public function checkProductAvailability(GetResponseForControllerResultEvent $event)
51+
public function checkProductAvailability(GetResponseForControllerResultEvent $event): void
6152
{
6253
$product = $event->getControllerResult();
63-
$method = $event->getRequest()->getMethod();
64-
65-
if (!$product instanceof Product || Request::METHOD_GET !== $method) {
54+
if (!$product instanceof Product || !$event->getRequest()->isMethodSafe(false)) {
6655
return;
6756
}
6857

69-
if (!$product->getVirtualStock()) {
58+
if (!$product->isPubliclyAvailable()) {
7059
// Using internal codes for a better understanding of what's going on
71-
throw new ProductNotFoundException(self::OUTOFSTOCK);
60+
throw new ProductNotFoundException(sprintf('The product "%s" does not exist.', $product->getId()));
7261
}
7362
}
7463
}
7564
```
7665

77-
The exception doesn't have to be a Symfony's `HttpException`. Any type of `Exception` can be thrown. The best part is that API Platform already takes care of how the error is handled and returned. For instance, if the API is configured to respond in JSON-LD, the error will be returned in this format as well.
78-
79-
```json
80-
{
81-
"@context": "/contexts/Error",
82-
"@type": "Error",
83-
"hydra:title": "An error occurred",
84-
"hydra:description": "53"
85-
}
86-
```
87-
88-
Is what you get, with an HTTP status code 404 as defined in the configuration.
89-
90-
## Validation errors
91-
92-
API Platform does handle the validation errors responses for you. You can define a Symfony supported constraint, or a custom constraint upon any `ApiResource` or it's properties.
93-
94-
```php
95-
<?php
96-
97-
// src/AppBundle/Entity/Product.php
98-
99-
namespace AppBundle\Entity;
100-
101-
use ApiPlatform\Core\Annotation\ApiResource;
102-
use Doctrine\ORM\Mapping as ORM;
103-
use Symfony\Component\Validator\Constraints as Assert;
104-
use AppBundle\Validator\Constraints as AppAssert;
105-
106-
/**
107-
*
108-
* @ApiResource
109-
* @ORM\Entity
110-
*/
111-
class Product
112-
{
113-
/**
114-
* @var int The id of this product.
115-
*
116-
* @ORM\Id
117-
* @ORM\GeneratedValue
118-
* @ORM\Column(type="integer")
119-
*/
120-
private $id;
121-
122-
/**
123-
* @var string The name of the product
124-
*
125-
* @ORM\Column
126-
* @Assert\NotBlank
127-
*/
128-
private name;
129-
130-
/**
131-
* @var ProductProperty[] Describe the product
132-
*
133-
* @ORM\Column(type="array")
134-
* @AppAssert\MinimalProperties
135-
*/
136-
private $properties;
137-
}
138-
```
139-
140-
```php
141-
<?php
142-
143-
// src/AppBundle/Validator/Constraints/MinimalProperties.php
144-
145-
namespace AppBundle\Validator\Constraints;
66+
If you use the standard distribution of API Platform, the event listener will be automatically registered. If you use a
67+
custom installation, [learn how to register listeners](events.md).
14668

147-
use Symfony\Component\Validator\Constraint;
69+
Then, configure the framework to catch `AppBundle\Exception\ProductNotFoundException` exceptions and convert them in `404`
70+
errors:
14871

149-
/**
150-
* @Annotation
151-
*/
152-
class MinimalProperties extends Constraint
153-
{
154-
public $message = 'The product must have the minimal properties required (description, price)';
72+
```yaml
73+
# app/config/config.yml
74+
api_platform:
75+
# ...
76+
exception_to_status:
77+
# The 2 following handlers are registered by default, keep those lines to prevent unexpected side effects
78+
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
79+
ApiPlatform\Core\Exception\InvalidArgumentException: 'HTTP_BAD_REQUEST' # Or a `Symfony\Component\HttpFoundation\Response`'s constant
15580

156-
public function validatedBy()
157-
{
158-
return get_class($this).'Validator';
159-
}
160-
}
81+
AppBundle\Exception\ProductNotFoundException: 404 # Here is the handler for our custom exception
16182
```
16283
163-
```php
164-
<?php
165-
166-
// src/AppBundle/Validator/Constraints/MinimalPropertiesValidator.php
84+
Any type of `Exception` can be thrown, API Platform will convert it to a Symfony's `HttpException`. The framework also takes
85+
care to serialize the error description according to the request format. For instance, if the API should respond in JSON-LD,
86+
the error will be returned in this format as well:
16787

168-
namespace AppBundle\Validator\Constraints;
169-
170-
use Symfony\Component\Validator\Constraint;
171-
use Symfony\Component\Validator\ConstraintValidator;
172-
173-
/**
174-
* @Annotation
175-
*/
176-
final class MinimalPropertiesValidator extends ConstraintValidator
177-
{
178-
public function validate($value, Constraint $constraint)
179-
{
180-
if (!in_array('description', $value) || !in_array('price', $value)) {
181-
$this->context->buildViolation($constraint->message)
182-
->addViolation();
183-
}
184-
}
185-
}
186-
```
187-
188-
API Platform will handle the error returned and adapt it's format according to the API configuration. If you did configured it to respond in JSON-LD. Your response would looks like:
88+
`GET /products/1234`
18989

19090
```json
19191
{
192-
"@context": "/contexts/ConstraintViolationList",
193-
"@type": "ConstraintViolationList",
92+
"@context": "/contexts/Error",
93+
"@type": "Error",
19494
"hydra:title": "An error occurred",
195-
"hydra:description": "properties: The product must have the minimal properties required (description, price)",
196-
"violations": [
197-
{
198-
"propertyPath": "properties",
199-
"message": "The product must have the minimal properties required (description, price)"
200-
}
201-
]
95+
"hydra:description": "The product \"1234\" does not exist."
20296
}
20397
```

core/validation.md

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,134 @@
11
# Validation
22

3-
API Platform Core uses the [Symfony Validator component](http://symfony.com/doc/current/book/validation.html) to validate
4-
entities.
3+
API Platform take care of validating data sent to the API by the client (usually user data entered through forms).
4+
By default, the framework relies on [the powerful Symfony Validator Component](http://symfony.com/doc/current/validation.html)
5+
for this task, but you can replace it by your preferred validation library such as [the PHP filter extension](http://php.net/manual/en/intro.filter.php)
6+
if you want to.
57

6-
Without specific configuration, it uses the default validation group, but this behavior is customizable.
8+
## Validating Submitted Data
9+
10+
Validating submitted data is simple as adding [Symfony's builtin constraints](http://symfony.com/doc/current/reference/constraints.html)
11+
or [custom constraints](http://symfony.com/doc/current/validation/custom_constraint.html) directly in classes marked with
12+
the `@ApiResource` annotation:
13+
14+
```php
15+
<?php
16+
// src/AppBundle/Entity/Product.php
17+
18+
namespace AppBundle\Entity;
19+
20+
use ApiPlatform\Core\Annotation\ApiResource;
21+
use Doctrine\ORM\Mapping as ORM;
22+
use Symfony\Component\Validator\Constraints as Assert; // Symfony's builtin constraints
23+
use AppBundle\Validator\Constraints\MinimalProperties; // A custom constraint
24+
25+
/**
26+
* A product.
27+
*
28+
* @ApiResource
29+
* @ORM\Entity
30+
*/
31+
class Product
32+
{
33+
/**
34+
* @var int The id of this product.
35+
*
36+
* @ORM\Id
37+
* @ORM\GeneratedValue
38+
* @ORM\Column(type="integer")
39+
*/
40+
private $id;
41+
42+
/**
43+
* @var string The name of the product
44+
*
45+
* @Assert\NotBlank
46+
* @ORM\Column
47+
*/
48+
private name;
49+
50+
/**
51+
* @var string[] Describe the product
52+
*
53+
* @MinimalProperties
54+
* @ORM\Column(type="json")
55+
*/
56+
private $properties;
57+
58+
// Getters and setters...
59+
}
60+
```
61+
62+
Here is a custom constraint and the related validator:
63+
64+
```php
65+
<?php
66+
// src/AppBundle/Validator/Constraints/MinimalProperties.php
67+
68+
namespace AppBundle\Validator\Constraints;
69+
70+
use Symfony\Component\Validator\Constraint;
71+
72+
/**
73+
* @Annotation
74+
*/
75+
class MinimalProperties extends Constraint
76+
{
77+
public $message = 'The product must have the minimal properties required ("description", "price")';
78+
}
79+
```
80+
81+
```php
82+
<?php
83+
// src/AppBundle/Validator/Constraints/MinimalPropertiesValidator.php
84+
85+
namespace AppBundle\Validator\Constraints;
86+
87+
use Symfony\Component\Validator\Constraint;
88+
use Symfony\Component\Validator\ConstraintValidator;
89+
90+
/**
91+
* @Annotation
92+
*/
93+
final class MinimalPropertiesValidator extends ConstraintValidator
94+
{
95+
public function validate($value, Constraint $constraint): void
96+
{
97+
if (!array_diff(['description', 'price'], $value)) {
98+
$this->context->buildViolation($constraint->message)->addViolation();
99+
}
100+
}
101+
}
102+
```
103+
104+
If the data submitted by the client is invalid, the HTTP status code will be set to `400 Bad Request` and the response's
105+
body will contain the list of violations serialized in a format compliant with the requested one. For instance, a validation
106+
error will look like the following if the requested format is JSON-LD (the default):
107+
108+
```json
109+
{
110+
"@context": "/contexts/ConstraintViolationList",
111+
"@type": "ConstraintViolationList",
112+
"hydra:title": "An error occurred",
113+
"hydra:description": "properties: The product must have the minimal properties required (\"description\", \"price\")",
114+
"violations": [
115+
{
116+
"propertyPath": "properties",
117+
"message": "The product must have the minimal properties required (\"description\", \"price\")"
118+
}
119+
]
120+
}
121+
```
122+
123+
Take a look at the [Errors Handling guide](errors.md) to learn how API Platform converts PHP exceptions like validation
124+
errors to HTTP errors.
7125

8126
## Using Validation Groups
9127

10-
Built-in actions are able to leverage Symfony's [validation groups](http://symfony.com/doc/current/book/validation.html#validation-groups).
128+
Without specific configuration, the default validation group is always used, but this behavior is customizable: the framework
129+
is able to leverage Symfony's [validation groups](http://symfony.com/doc/current/book/validation.html#validation-groups).
11130

12-
You can customize them by editing the resource configuration and add the groups you want to use when the validation occurs:
131+
You can configure the groups you want to use when the validation occurs directly through the `ApiResource` annotation:
13132

14133
```php
15134
<?php
@@ -20,6 +139,7 @@ use Symfony\Component\Validator\Constraints as Assert;
20139

21140
/**
22141
* @ApiResource(attributes={"validation_groups"={"a", "b"}})
142+
* ...
23143
*/
24144
class Book
25145
{

0 commit comments

Comments
 (0)