Contributte Apitte-negotiation
Content negotiation for Apitte (opens new window).
Transform response entity into response with unified format in dependence on Accept
header and uri path suffix /api/v1/users(.json|.xml)
# Content
# Setup
First of all, setup core (opens new window) package and enable CoreDecoratorPlugin
.
Install and register negotiation plugin
composer require apitte/negotiation
api:
plugins:
Apitte\Negotiation\DI\NegotiationPlugin:
2
3
# Response
Instead of writing data into response body use $response->withEntity($entity)
so transformers could handle transformation for you.
namespace App\Api\V1\Controllers;
use Apitte\Core\Annotation\Controller\ControllerPath;
use Apitte\Core\Annotation\Controller\Method;
use Apitte\Core\Annotation\Controller\Path;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Apitte\Negotiation\Http\ArrayEntity;
/**
* @ControllerPath("/users")
*/
class UsersController extends BaseV1Controller
{
/**
* @Path("/")
* @Method("GET")
*/
public function index(ApiRequest $request, ApiResponse $response): ApiResponse
{
$entity = ArrayEntity::from([
[
'id' => 1,
'firstName' => 'John',
'lastName' => 'Doe',
'emailAddress' => 'john@doe.com',
],
[
'id' => 2,
'firstName' => 'Elon',
'lastName' => 'Musk',
'emailAddress' => 'elon.musk@spacex.com',
],
]);
return $response
->withStatus(ApiResponse::S200_OK)
->withEntity($entity);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Entities
Value objects which are used to create response
ArrayEntity
- create from arrayObjectEntity
- create from stdClassScalarEntity
- create from raw data
# Error handling
Negotiations are implemented through an IErrorDecorator
, which have higher priority than internal ErrorHandler
so response is created from exception in an ITransformer
and ErrorHandler
only log that exception (if you use PsrLogErrorHandler
)
# Negotiators
Handle request and based on path suffix or request headers call appropriate transformer.
SuffixNegotiator
- used for request with path suffix like
/api/v1/users.json
-> transformer forjson
suffix is used
DefaultNegotiator
- called when none other transform
- require annotation
@Negotiation(default = true, suffix = "json")
defined on endpoint - transformer for given suffix is looked for
FallbackNegotiator
- used last if no other negotiator transformed response
- uses json transformer by default
# Transformers
Transformers convert entities and exceptions into response.
JsonTransformer
- transform into json
JsonUnifyTransformer
- transform into json with unified format
api:
plugins:
Apitte\Negotiation\DI\NegotiationPlugin:
unification: true
2
3
4
CsvTransformer
- transform into csv
- known limitation: data need to be a flat structure
# Implementing transformer
services:
- factory: App\Api\Transformer\XmlTransformer
tags: [apitte.negotiator.transformer: [suffix: xml, fallback: true]]
2
3
- register transformer for suffix
xml
, used for uris like/api/v1/users.xml
- if
fallback: true
is defined and none of transformers matched then use that transformer
namespace App\Api\Transformer;
use Apitte\Core\Exception\ApiException;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Apitte\Core\Http\ResponseAttributes;
use Apitte\Negotiation\Http\ArrayEntity;
use Apitte\Negotiation\Transformer\AbstractTransformer;
use Throwable;
class XmlTransformer extends AbstractTransformer
{
/**
* Encode given data for response
*
* @param mixed[] $context
*/
public function transform(ApiRequest $request, ApiResponse $response, array $context = []) : ApiResponse
{
if (isset($context['exception'])) {
return $this->transformError($context['exception'], $request, $response);
}
return $this->transformResponse($request, $response);
}
protected function transformResponse(ApiRequest $request, ApiResponse $response): ApiResponse
{
$data = $this->getEntity($response)->getData();
$content = $this->dataToXmlString($data);
$response->getBody()->write($content);
return $response
->withHeader('Content-Type', 'application/xml');
}
protected function transformError(Throwable $error, ApiRequest $request, ApiResponse $response): ApiResponse
{
if ($error instanceof ApiException) {
$code = $error->getCode();
$message = $error->getMessage();
} else {
$code = 500;
$message = 'Application encountered an internal error. Please try again later.';
}
return $response
->withStatus($code)
->withAttribute(ResponseAttributes::ATTR_ENTITY, ArrayEntity::from([
'status' => 'error',
'message' => $message,
]));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56