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