Skip to content

Commit ba8b346

Browse files
authored
Merge pull request #406 from GregoireHebert/GregoireHebert-exception-rebased
Errors to Exception
2 parents f3cac0b + eb1d21e commit ba8b346

File tree

3 files changed

+242
-24
lines changed

3 files changed

+242
-24
lines changed

core/errors.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Errors Handling
2+
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 sends the appropriate HTTP status code to the client: `400` for expected errors, `500` for
6+
unexpected ones. It also provides a description of the error in [the Hydra error format](http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors)
7+
or in the format described in the [RFC 7807](https://tools.ietf.org/html/rfc7807), depending of the format selected during the [content negotiation](content-negotiation.md).
8+
9+
## Converting PHP Exceptions to HTTP Errors
10+
11+
The framework also allows to configure the HTTP status code sent to the clients when custom exceptions are thrown.
12+
13+
In the following example, we 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:
15+
16+
```php
17+
<?php
18+
// src/AppBundle/Exception/ProductNotFoundException.php
19+
20+
namespace AppBundle\Exception;
21+
22+
final class ProductNotFoundException extends \Exception
23+
{
24+
}
25+
```
26+
27+
```php
28+
<?php
29+
// src/AppBundle/EventSubscriber/CartManager.php
30+
31+
namespace AppBundle\EventSubscriber;
32+
33+
use ApiPlatform\Core\EventListener\EventPriorities;
34+
use AppBundle\Entity\Product;
35+
use AppBundle\Exception\ProductNotFoundException;
36+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
37+
use Symfony\Component\HttpFoundation\Request;
38+
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
39+
use Symfony\Component\HttpKernel\KernelEvents;
40+
41+
final class ProductManager implements EventSubscriberInterface
42+
{
43+
public static function getSubscribedEvents(): array
44+
{
45+
return [
46+
KernelEvents::REQUEST => ['checkProductAvailability', EventPriorities::POST_DESERIALIZE],
47+
];
48+
}
49+
50+
public function checkProductAvailability(GetResponseForControllerResultEvent $event): void
51+
{
52+
$product = $event->getControllerResult();
53+
if (!$product instanceof Product || !$event->getRequest()->isMethodSafe(false)) {
54+
return;
55+
}
56+
57+
if (!$product->isPubliclyAvailable()) {
58+
// Using internal codes for a better understanding of what's going on
59+
throw new ProductNotFoundException(sprintf('The product "%s" does not exist.', $product->getId()));
60+
}
61+
}
62+
}
63+
```
64+
65+
If you use the standard distribution of API Platform, this event listener will be automatically registered. If you use a
66+
custom installation, [learn how to register listeners](events.md).
67+
68+
Then, configure the framework to catch `AppBundle\Exception\ProductNotFoundException` exceptions and convert them in `404`
69+
errors:
70+
71+
```yaml
72+
# config/packages/api_platform.yaml
73+
api_platform:
74+
# ...
75+
exception_to_status:
76+
# The 2 following handlers are registered by default, keep those lines to prevent unexpected side effects
77+
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
78+
ApiPlatform\Core\Exception\InvalidArgumentException: 'HTTP_BAD_REQUEST' # Or a `Symfony\Component\HttpFoundation\Response`'s constant
79+
80+
AppBundle\Exception\ProductNotFoundException: 404 # Here is the handler for our custom exception
81+
```
82+
83+
Any type of `Exception` can be thrown, API Platform will convert it to a Symfony's `HttpException`. The framework also takes
84+
care of serializing the error description according to the request format. For instance, if the API should respond in JSON-LD,
85+
the error will be returned in this format as well:
86+
87+
`GET /products/1234`
88+
89+
```json
90+
{
91+
"@context": "/contexts/Error",
92+
"@type": "Error",
93+
"hydra:title": "An error occurred",
94+
"hydra:description": "The product \"1234\" does not exist."
95+
}
96+
```

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
{

index.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,29 @@
5858
1. [Using Validation Groups](core/validation.md#using-validation-groups)
5959
2. [Dynamic Validation Groups](core/validation.md#dynamic-validation-groups)
6060
3. [Error Levels and Payload Serialization](core/validation.md#error-levels-and-payload-serialization)
61-
9. [Pagination](core/pagination.md)
61+
9. [Error Handling](core/errors.md)
62+
1. [Converting PHP Exceptions to HTTP Errors](core/errors.md#converting-php-exceptions-to-http-errors)
63+
10. [Pagination](core/pagination.md)
6264
1. [Disabling the Pagination](core/pagination.md#disabling-the-pagination)
6365
2. [Changing the Number of Items per Page](core/pagination.md#changing-the-number-of-items-per-page)
6466
3. [Partial Pagination](core/pagination.md#partial-pagination)
65-
10. [The Event System](core/events.md)
66-
11. [Content Negotiation](core/content-negotiation.md)
67+
11. [The Event System](core/events.md)
68+
12. [Content Negotiation](core/content-negotiation.md)
6769
1. [Enabling Several Formats](core/content-negotiation.md#enabling-several-formats)
6870
2. [Registering a Custom Serializer](core/content-negotiation.md#registering-a-custom-serializer)
6971
3. [Creating a Responder](core/content-negotiation.md#creating-a-responder)
7072
4. [Writing a Custom Normalizer](core/content-negotiation.md#writing-a-custom-normalizer)
71-
12. [Using External JSON-LD Vocabularies](core/external-vocabularies.md)
72-
13. [Extending JSON-LD context](core/extending-jsonld-context.md)
73-
14. [Data Providers](core/data-providers.md)
73+
13. [Using External JSON-LD Vocabularies](core/external-vocabularies.md)
74+
14. [Extending JSON-LD context](core/extending-jsonld-context.md)
75+
15. [Data Providers](core/data-providers.md)
7476
1. [Custom Collection Data Provider](core/data-providers.md#custom-collection-data-provider)
7577
2. [Custom Item Data Provider](core/data-providers.md#custom-item-data-provider)
7678
3. [Injecting the Serializer in an `ItemDataProvider`](core/data-providers.md#injecting-the-serializer-in-an-itemdataprovider)
77-
15. [Extensions](core/extensions.md)
79+
16. [Extensions](core/extensions.md)
7880
1. [Custom Extension](core/extensions.md#custom-extension)
7981
2. [Filter upon the current user](core/extensions.md#example)
80-
16. [Security](core/security.md)
81-
17. [Performance](core/performance.md)
82+
17. [Security](core/security.md)
83+
18. [Performance](core/performance.md)
8284
1. [Enabling the Builtin HTTP Cache Invalidation System](core/performance.md#enabling-the-builtin-http-cache-invalidation-system)
8385
2. [Enabling the Metadata Cache](core/performance.md#enabling-the-metadata-cache)
8486
3. [Using PPM (PHP-PM)](core/performance.md#using-ppm-php-pm)
@@ -90,35 +92,35 @@
9092
3. [Override at Resource and Operation Level](core/performance.md#override-at-resource-and-operation-level)
9193
4. [Disable Eager Loading](core/performance.md#disable-eager-loading)
9294
3. [Partial Pagination](core/performance.md#partial-pagination)
93-
18. [Operation Path Naming](core/operation-path-naming.md)
95+
19. [Operation Path Naming](core/operation-path-naming.md)
9496
1. [Configuration](core/operation-path-naming.md#configuration)
9597
2. [Create a Custom Operation Path Naming](core/operation-path-naming.md#create-a-custom-operation-path-resolver)
9698
1. [Defining the Operation Path Naming](core/operation-path-naming.md#defining-the-operation-path-resolver)
9799
2. [Registering the Service](core/operation-path-naming.md#registering-the-service)
98100
3. [Configure it](core/operation-path-naming.md#configure-it)
99-
19. [Accept application/x-www-form-urlencoded Form Data](core/form-data.md)
100-
20. [FOSUserBundle Integration](core/fosuser-bundle.md)
101+
20. [Accept application/x-www-form-urlencoded Form Data](core/form-data.md)
102+
21. [FOSUserBundle Integration](core/fosuser-bundle.md)
101103
1. [Installing the Bundle](core/fosuser-bundle.md#installing-the-bundle)
102104
2. [Enabling the Bridge](core/fosuser-bundle.md#enabling-the-bridge)
103105
3. [Creating a `User` Entity with Serialization Groups](core/fosuser-bundle.md#creating-a-user-entity-with-serialization-groups)
104-
21. [Adding a JWT authentication using LexikJWTAuthenticationBundle](core/jwt.md)
106+
22. [Adding a JWT authentication using LexikJWTAuthenticationBundle](core/jwt.md)
105107
1. [Testing with Swagger](core/jwt.md#testing-with-swagger)
106108
2. [Testing with Behat](core/jwt.md#testing-with-behat)
107-
22. [NelmioApiDocBundle integration](core/nelmio-api-doc.md)
108-
23. [AngularJS Integration](core/angularjs-integration.md)
109+
23. [NelmioApiDocBundle integration](core/nelmio-api-doc.md)
110+
24. [AngularJS Integration](core/angularjs-integration.md)
109111
1. [Restangular](core/angularjs-integration.md#restangular)
110112
2. [ng-admin](core/angularjs-integration.md#ng-admin)
111-
24. [Swagger Support](core/swagger.md)
113+
25. [Swagger Support](core/swagger.md)
112114
1. [Override Swagger documentation](core/swagger.md#override-swagger-documentation)
113-
25. [GraphQL Support](core/graphql.md)
115+
26. [GraphQL Support](core/graphql.md)
114116
1. [Overall View](core/graphql.md#overall-view)
115117
2. [Enabling GraphQL](core/graphql.md#enabling-graphql)
116118
3. [GraphiQL](core/graphql.md#graphiql)
117-
26. [The Serialization Process](core/serialization.md)
119+
27. [The Serialization Process](core/serialization.md)
118120
1. [Overall Process](core/serialization.md#overall-process)
119121
2. [Available Serializers](core/serialization.md#available-serializers)
120122
3. [Decorating a Serializer and Add Extra Data](core/serialization.md#decorating-a-serializer-and-add-extra-data)
121-
27. [Handling Data Transfer Objects (DTOs)](core/dto.md)
123+
28. [Handling Data Transfer Objects (DTOs)](core/dto.md)
122124

123125
## The Schema Generator Component: Generate Data Models from Open Vocabularies
124126

0 commit comments

Comments
 (0)