Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\rest\EventSubscriber;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Core\Cache\CacheableResponse;
|
Chris@0
|
6 use Drupal\Core\Cache\CacheableResponseInterface;
|
Chris@0
|
7 use Drupal\Core\Render\RenderContext;
|
Chris@0
|
8 use Drupal\Core\Render\RendererInterface;
|
Chris@0
|
9 use Drupal\Core\Routing\RouteMatchInterface;
|
Chris@0
|
10 use Drupal\rest\ResourceResponseInterface;
|
Chris@0
|
11 use Symfony\Component\HttpFoundation\Request;
|
Chris@0
|
12 use Symfony\Component\HttpFoundation\Response;
|
Chris@0
|
13 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
Chris@0
|
14 use Symfony\Component\HttpKernel\KernelEvents;
|
Chris@0
|
15 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
Chris@0
|
16 use Symfony\Component\Serializer\SerializerInterface;
|
Chris@0
|
17
|
Chris@0
|
18 /**
|
Chris@0
|
19 * Response subscriber that serializes and removes ResourceResponses' data.
|
Chris@0
|
20 */
|
Chris@0
|
21 class ResourceResponseSubscriber implements EventSubscriberInterface {
|
Chris@0
|
22
|
Chris@0
|
23 /**
|
Chris@0
|
24 * The serializer.
|
Chris@0
|
25 *
|
Chris@0
|
26 * @var \Symfony\Component\Serializer\SerializerInterface
|
Chris@0
|
27 */
|
Chris@0
|
28 protected $serializer;
|
Chris@0
|
29
|
Chris@0
|
30 /**
|
Chris@0
|
31 * The renderer.
|
Chris@0
|
32 *
|
Chris@0
|
33 * @var \Drupal\Core\Render\RendererInterface
|
Chris@0
|
34 */
|
Chris@0
|
35 protected $renderer;
|
Chris@0
|
36
|
Chris@0
|
37 /**
|
Chris@0
|
38 * The current route match.
|
Chris@0
|
39 *
|
Chris@0
|
40 * @var \Drupal\Core\Routing\RouteMatchInterface
|
Chris@0
|
41 */
|
Chris@0
|
42 protected $routeMatch;
|
Chris@0
|
43
|
Chris@0
|
44 /**
|
Chris@0
|
45 * Constructs a ResourceResponseSubscriber object.
|
Chris@0
|
46 *
|
Chris@0
|
47 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
|
Chris@0
|
48 * The serializer.
|
Chris@0
|
49 * @param \Drupal\Core\Render\RendererInterface $renderer
|
Chris@0
|
50 * The renderer.
|
Chris@0
|
51 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
Chris@0
|
52 * The current route match.
|
Chris@0
|
53 */
|
Chris@0
|
54 public function __construct(SerializerInterface $serializer, RendererInterface $renderer, RouteMatchInterface $route_match) {
|
Chris@0
|
55 $this->serializer = $serializer;
|
Chris@0
|
56 $this->renderer = $renderer;
|
Chris@0
|
57 $this->routeMatch = $route_match;
|
Chris@0
|
58 }
|
Chris@0
|
59
|
Chris@0
|
60 /**
|
Chris@0
|
61 * Serializes ResourceResponse responses' data, and removes that data.
|
Chris@0
|
62 *
|
Chris@0
|
63 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
Chris@0
|
64 * The event to process.
|
Chris@0
|
65 */
|
Chris@0
|
66 public function onResponse(FilterResponseEvent $event) {
|
Chris@0
|
67 $response = $event->getResponse();
|
Chris@0
|
68 if (!$response instanceof ResourceResponseInterface) {
|
Chris@0
|
69 return;
|
Chris@0
|
70 }
|
Chris@0
|
71
|
Chris@0
|
72 $request = $event->getRequest();
|
Chris@0
|
73 $format = $this->getResponseFormat($this->routeMatch, $request);
|
Chris@0
|
74 $this->renderResponseBody($request, $response, $this->serializer, $format);
|
Chris@0
|
75 $event->setResponse($this->flattenResponse($response));
|
Chris@0
|
76 }
|
Chris@0
|
77
|
Chris@0
|
78 /**
|
Chris@0
|
79 * Determines the format to respond in.
|
Chris@0
|
80 *
|
Chris@0
|
81 * Respects the requested format if one is specified. However, it is common to
|
Chris@0
|
82 * forget to specify a request format in case of a POST or PATCH. Rather than
|
Chris@0
|
83 * simply throwing an error, we apply the robustness principle: when POSTing
|
Chris@0
|
84 * or PATCHing using a certain format, you probably expect a response in that
|
Chris@0
|
85 * same format.
|
Chris@0
|
86 *
|
Chris@0
|
87 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
Chris@0
|
88 * The current route match.
|
Chris@0
|
89 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@0
|
90 * The current request.
|
Chris@0
|
91 *
|
Chris@0
|
92 * @return string
|
Chris@0
|
93 * The response format.
|
Chris@0
|
94 */
|
Chris@0
|
95 public function getResponseFormat(RouteMatchInterface $route_match, Request $request) {
|
Chris@0
|
96 $route = $route_match->getRouteObject();
|
Chris@0
|
97 $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
|
Chris@0
|
98 $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
|
Chris@0
|
99 $acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats;
|
Chris@0
|
100
|
Chris@0
|
101 $requested_format = $request->getRequestFormat();
|
Chris@0
|
102 $content_type_format = $request->getContentType();
|
Chris@0
|
103
|
Chris@0
|
104 // If an acceptable format is requested, then use that. Otherwise, including
|
Chris@0
|
105 // and particularly when the client forgot to specify a format, then use
|
Chris@0
|
106 // heuristics to select the format that is most likely expected.
|
Chris@0
|
107 if (in_array($requested_format, $acceptable_formats)) {
|
Chris@0
|
108 return $requested_format;
|
Chris@0
|
109 }
|
Chris@0
|
110 // If a request body is present, then use the format corresponding to the
|
Chris@0
|
111 // request body's Content-Type for the response, if it's an acceptable
|
Chris@0
|
112 // format for the request.
|
Chris@0
|
113 elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) {
|
Chris@0
|
114 return $content_type_format;
|
Chris@0
|
115 }
|
Chris@0
|
116 // Otherwise, use the first acceptable format.
|
Chris@0
|
117 elseif (!empty($acceptable_formats)) {
|
Chris@0
|
118 return $acceptable_formats[0];
|
Chris@0
|
119 }
|
Chris@0
|
120 // Sometimes, there are no acceptable formats, e.g. DELETE routes.
|
Chris@0
|
121 else {
|
Chris@0
|
122 return NULL;
|
Chris@0
|
123 }
|
Chris@0
|
124 }
|
Chris@0
|
125
|
Chris@0
|
126 /**
|
Chris@0
|
127 * Renders a resource response body.
|
Chris@0
|
128 *
|
Chris@0
|
129 * Serialization can invoke rendering (e.g., generating URLs), but the
|
Chris@0
|
130 * serialization API does not provide a mechanism to collect the
|
Chris@0
|
131 * bubbleable metadata associated with that (e.g., language and other
|
Chris@0
|
132 * contexts), so instead, allow those to "leak" and collect them here in
|
Chris@0
|
133 * a render context.
|
Chris@0
|
134 *
|
Chris@0
|
135 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@0
|
136 * The request object.
|
Chris@0
|
137 * @param \Drupal\rest\ResourceResponseInterface $response
|
Chris@0
|
138 * The response from the REST resource.
|
Chris@0
|
139 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
|
Chris@0
|
140 * The serializer to use.
|
Chris@0
|
141 * @param string|null $format
|
Chris@0
|
142 * The response format, or NULL in case the response does not need a format,
|
Chris@0
|
143 * for example for the response to a DELETE request.
|
Chris@0
|
144 *
|
Chris@0
|
145 * @todo Add test coverage for language negotiation contexts in
|
Chris@0
|
146 * https://www.drupal.org/node/2135829.
|
Chris@0
|
147 */
|
Chris@0
|
148 protected function renderResponseBody(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) {
|
Chris@0
|
149 $data = $response->getResponseData();
|
Chris@0
|
150
|
Chris@0
|
151 // If there is data to send, serialize and set it as the response body.
|
Chris@0
|
152 if ($data !== NULL) {
|
Chris@0
|
153 $context = new RenderContext();
|
Chris@0
|
154 $output = $this->renderer
|
Chris@0
|
155 ->executeInRenderContext($context, function () use ($serializer, $data, $format) {
|
Chris@0
|
156 return $serializer->serialize($data, $format);
|
Chris@0
|
157 });
|
Chris@0
|
158
|
Chris@0
|
159 if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
|
Chris@0
|
160 $response->addCacheableDependency($context->pop());
|
Chris@0
|
161 }
|
Chris@0
|
162
|
Chris@0
|
163 $response->setContent($output);
|
Chris@0
|
164 $response->headers->set('Content-Type', $request->getMimeType($format));
|
Chris@0
|
165 }
|
Chris@0
|
166 }
|
Chris@0
|
167
|
Chris@0
|
168 /**
|
Chris@0
|
169 * Flattens a fully rendered resource response.
|
Chris@0
|
170 *
|
Chris@0
|
171 * Ensures that complex data structures in ResourceResponse::getResponseData()
|
Chris@0
|
172 * are not serialized. Not doing this means that caching this response object
|
Chris@0
|
173 * requires unserializing the PHP data when reading this response object from
|
Chris@0
|
174 * cache, which can be very costly, and is unnecessary.
|
Chris@0
|
175 *
|
Chris@0
|
176 * @param \Drupal\rest\ResourceResponseInterface $response
|
Chris@0
|
177 * A fully rendered resource response.
|
Chris@0
|
178 *
|
Chris@0
|
179 * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
|
Chris@0
|
180 * The flattened response.
|
Chris@0
|
181 */
|
Chris@0
|
182 protected function flattenResponse(ResourceResponseInterface $response) {
|
Chris@0
|
183 $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response();
|
Chris@0
|
184 $final_response->setContent($response->getContent());
|
Chris@0
|
185 $final_response->setStatusCode($response->getStatusCode());
|
Chris@0
|
186 $final_response->setProtocolVersion($response->getProtocolVersion());
|
Chris@0
|
187 $final_response->setCharset($response->getCharset());
|
Chris@0
|
188 $final_response->headers = clone $response->headers;
|
Chris@0
|
189 if ($final_response instanceof CacheableResponseInterface) {
|
Chris@0
|
190 $final_response->addCacheableDependency($response->getCacheableMetadata());
|
Chris@0
|
191 }
|
Chris@0
|
192 return $final_response;
|
Chris@0
|
193 }
|
Chris@0
|
194
|
Chris@0
|
195 /**
|
Chris@0
|
196 * {@inheritdoc}
|
Chris@0
|
197 */
|
Chris@0
|
198 public static function getSubscribedEvents() {
|
Chris@0
|
199 // Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
|
Chris@0
|
200 // (priority 100), so that Dynamic Page Cache can cache flattened responses.
|
Chris@0
|
201 $events[KernelEvents::RESPONSE][] = ['onResponse', 128];
|
Chris@0
|
202 return $events;
|
Chris@0
|
203 }
|
Chris@0
|
204
|
Chris@0
|
205 }
|