Chris@18
|
1 <?php
|
Chris@18
|
2
|
Chris@18
|
3 namespace Drupal\jsonapi\EventSubscriber;
|
Chris@18
|
4
|
Chris@18
|
5 use Drupal\Core\Cache\CacheableResponse;
|
Chris@18
|
6 use Drupal\Core\Cache\CacheableResponseInterface;
|
Chris@18
|
7 use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
Chris@18
|
8 use Drupal\jsonapi\ResourceResponse;
|
Chris@18
|
9 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
Chris@18
|
10 use Symfony\Component\HttpFoundation\Request;
|
Chris@18
|
11 use Symfony\Component\HttpFoundation\Response;
|
Chris@18
|
12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
Chris@18
|
13 use Symfony\Component\HttpKernel\KernelEvents;
|
Chris@18
|
14 use Symfony\Component\Serializer\SerializerInterface;
|
Chris@18
|
15
|
Chris@18
|
16 /**
|
Chris@18
|
17 * Response subscriber that serializes and removes ResourceResponses' data.
|
Chris@18
|
18 *
|
Chris@18
|
19 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
|
Chris@18
|
20 * may change at any time and could break any dependencies on it.
|
Chris@18
|
21 *
|
Chris@18
|
22 * @see https://www.drupal.org/project/jsonapi/issues/3032787
|
Chris@18
|
23 * @see jsonapi.api.php
|
Chris@18
|
24 *
|
Chris@18
|
25 * This is 99% identical to:
|
Chris@18
|
26 *
|
Chris@18
|
27 * \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
|
Chris@18
|
28 *
|
Chris@18
|
29 * but with a few differences:
|
Chris@18
|
30 * 1. It has the @jsonapi.serializer service injected instead of @serializer
|
Chris@18
|
31 * 2. It has the @current_route_match service no longer injected
|
Chris@18
|
32 * 3. It hardcodes the format to 'api_json'
|
Chris@18
|
33 * 4. It adds the CacheableNormalization object returned by JSON:API
|
Chris@18
|
34 * normalization to the response object.
|
Chris@18
|
35 * 5. It flattens only to a cacheable response if the HTTP method is cacheable.
|
Chris@18
|
36 *
|
Chris@18
|
37 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
|
Chris@18
|
38 */
|
Chris@18
|
39 class ResourceResponseSubscriber implements EventSubscriberInterface {
|
Chris@18
|
40
|
Chris@18
|
41 /**
|
Chris@18
|
42 * The serializer.
|
Chris@18
|
43 *
|
Chris@18
|
44 * @var \Symfony\Component\Serializer\SerializerInterface
|
Chris@18
|
45 */
|
Chris@18
|
46 protected $serializer;
|
Chris@18
|
47
|
Chris@18
|
48 /**
|
Chris@18
|
49 * Constructs a ResourceResponseSubscriber object.
|
Chris@18
|
50 *
|
Chris@18
|
51 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
|
Chris@18
|
52 * The serializer.
|
Chris@18
|
53 */
|
Chris@18
|
54 public function __construct(SerializerInterface $serializer) {
|
Chris@18
|
55 $this->serializer = $serializer;
|
Chris@18
|
56 }
|
Chris@18
|
57
|
Chris@18
|
58 /**
|
Chris@18
|
59 * {@inheritdoc}
|
Chris@18
|
60 *
|
Chris@18
|
61 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getSubscribedEvents()
|
Chris@18
|
62 * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
|
Chris@18
|
63 */
|
Chris@18
|
64 public static function getSubscribedEvents() {
|
Chris@18
|
65 // Run before the dynamic page cache subscriber (priority 100), so that
|
Chris@18
|
66 // Dynamic Page Cache can cache flattened responses.
|
Chris@18
|
67 $events[KernelEvents::RESPONSE][] = ['onResponse', 128];
|
Chris@18
|
68 return $events;
|
Chris@18
|
69 }
|
Chris@18
|
70
|
Chris@18
|
71 /**
|
Chris@18
|
72 * Serializes ResourceResponse responses' data, and removes that data.
|
Chris@18
|
73 *
|
Chris@18
|
74 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
Chris@18
|
75 * The event to process.
|
Chris@18
|
76 */
|
Chris@18
|
77 public function onResponse(FilterResponseEvent $event) {
|
Chris@18
|
78 $response = $event->getResponse();
|
Chris@18
|
79 if (!$response instanceof ResourceResponse) {
|
Chris@18
|
80 return;
|
Chris@18
|
81 }
|
Chris@18
|
82
|
Chris@18
|
83 $request = $event->getRequest();
|
Chris@18
|
84 $format = 'api_json';
|
Chris@18
|
85 $this->renderResponseBody($request, $response, $this->serializer, $format);
|
Chris@18
|
86 $event->setResponse($this->flattenResponse($response, $request));
|
Chris@18
|
87 }
|
Chris@18
|
88
|
Chris@18
|
89 /**
|
Chris@18
|
90 * Renders a resource response body.
|
Chris@18
|
91 *
|
Chris@18
|
92 * Serialization can invoke rendering (e.g., generating URLs), but the
|
Chris@18
|
93 * serialization API does not provide a mechanism to collect the
|
Chris@18
|
94 * bubbleable metadata associated with that (e.g., language and other
|
Chris@18
|
95 * contexts), so instead, allow those to "leak" and collect them here in
|
Chris@18
|
96 * a render context.
|
Chris@18
|
97 *
|
Chris@18
|
98 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@18
|
99 * The request object.
|
Chris@18
|
100 * @param \Drupal\jsonapi\ResourceResponse $response
|
Chris@18
|
101 * The response from the JSON:API resource.
|
Chris@18
|
102 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
|
Chris@18
|
103 * The serializer to use.
|
Chris@18
|
104 * @param string|null $format
|
Chris@18
|
105 * The response format, or NULL in case the response does not need a format,
|
Chris@18
|
106 * for example for the response to a DELETE request.
|
Chris@18
|
107 *
|
Chris@18
|
108 * @todo Add test coverage for language negotiation contexts in
|
Chris@18
|
109 * https://www.drupal.org/node/2135829.
|
Chris@18
|
110 */
|
Chris@18
|
111 protected function renderResponseBody(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format) {
|
Chris@18
|
112 $data = $response->getResponseData();
|
Chris@18
|
113
|
Chris@18
|
114 // If there is data to send, serialize and set it as the response body.
|
Chris@18
|
115 if ($data !== NULL) {
|
Chris@18
|
116 // First normalize the data. Note that error responses do not need a
|
Chris@18
|
117 // normalization context, since there are no entities to normalize.
|
Chris@18
|
118 // @see \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber::isJsonApiExceptionEvent()
|
Chris@18
|
119 $context = !$response->isSuccessful() ? [] : static::generateContext($request);
|
Chris@18
|
120 $jsonapi_doc_object = $serializer->normalize($data, $format, $context);
|
Chris@18
|
121 // Having just normalized the data, we can associate its cacheability with
|
Chris@18
|
122 // the response object.
|
Chris@18
|
123 assert($jsonapi_doc_object instanceof CacheableNormalization);
|
Chris@18
|
124 $response->addCacheableDependency($jsonapi_doc_object);
|
Chris@18
|
125 // Finally, encode the normalized data (JSON:API's encoder rasterizes it
|
Chris@18
|
126 // automatically).
|
Chris@18
|
127 $response->setContent($serializer->encode($jsonapi_doc_object->getNormalization(), $format));
|
Chris@18
|
128 $response->headers->set('Content-Type', $request->getMimeType($format));
|
Chris@18
|
129 }
|
Chris@18
|
130 }
|
Chris@18
|
131
|
Chris@18
|
132 /**
|
Chris@18
|
133 * Generates a top-level JSON:API normalization context.
|
Chris@18
|
134 *
|
Chris@18
|
135 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@18
|
136 * The request from which the context can be derived.
|
Chris@18
|
137 *
|
Chris@18
|
138 * @return array
|
Chris@18
|
139 * The generated context.
|
Chris@18
|
140 */
|
Chris@18
|
141 protected static function generateContext(Request $request) {
|
Chris@18
|
142 // Build the expanded context.
|
Chris@18
|
143 $context = [
|
Chris@18
|
144 'account' => NULL,
|
Chris@18
|
145 'sparse_fieldset' => NULL,
|
Chris@18
|
146 ];
|
Chris@18
|
147 if ($request->query->get('fields')) {
|
Chris@18
|
148 $context['sparse_fieldset'] = array_map(function ($item) {
|
Chris@18
|
149 return explode(',', $item);
|
Chris@18
|
150 }, $request->query->get('fields'));
|
Chris@18
|
151 }
|
Chris@18
|
152 return $context;
|
Chris@18
|
153 }
|
Chris@18
|
154
|
Chris@18
|
155 /**
|
Chris@18
|
156 * Flattens a fully rendered resource response.
|
Chris@18
|
157 *
|
Chris@18
|
158 * Ensures that complex data structures in ResourceResponse::getResponseData()
|
Chris@18
|
159 * are not serialized. Not doing this means that caching this response object
|
Chris@18
|
160 * requires deserializing the PHP data when reading this response object from
|
Chris@18
|
161 * cache, which can be very costly, and is unnecessary.
|
Chris@18
|
162 *
|
Chris@18
|
163 * @param \Drupal\jsonapi\ResourceResponse $response
|
Chris@18
|
164 * A fully rendered resource response.
|
Chris@18
|
165 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@18
|
166 * The request for which this response is generated.
|
Chris@18
|
167 *
|
Chris@18
|
168 * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
|
Chris@18
|
169 * The flattened response.
|
Chris@18
|
170 */
|
Chris@18
|
171 protected static function flattenResponse(ResourceResponse $response, Request $request) {
|
Chris@18
|
172 $final_response = ($response instanceof CacheableResponseInterface && $request->isMethodCacheable()) ? new CacheableResponse() : new Response();
|
Chris@18
|
173 $final_response->setContent($response->getContent());
|
Chris@18
|
174 $final_response->setStatusCode($response->getStatusCode());
|
Chris@18
|
175 $final_response->setProtocolVersion($response->getProtocolVersion());
|
Chris@18
|
176 if ($charset = $response->getCharset()) {
|
Chris@18
|
177 $final_response->setCharset($charset);
|
Chris@18
|
178 }
|
Chris@18
|
179 $final_response->headers = clone $response->headers;
|
Chris@18
|
180 if ($final_response instanceof CacheableResponseInterface) {
|
Chris@18
|
181 $final_response->addCacheableDependency($response->getCacheableMetadata());
|
Chris@18
|
182 }
|
Chris@18
|
183 return $final_response;
|
Chris@18
|
184 }
|
Chris@18
|
185
|
Chris@18
|
186 }
|