comparison core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php @ 5:12f9dff5fda9 tip

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