Chris@0: serializer = $serializer; Chris@0: $this->renderer = $renderer; Chris@0: $this->routeMatch = $route_match; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Serializes ResourceResponse responses' data, and removes that data. Chris@0: * Chris@0: * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event Chris@0: * The event to process. Chris@0: */ Chris@0: public function onResponse(FilterResponseEvent $event) { Chris@0: $response = $event->getResponse(); Chris@0: if (!$response instanceof ResourceResponseInterface) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $request = $event->getRequest(); Chris@0: $format = $this->getResponseFormat($this->routeMatch, $request); Chris@0: $this->renderResponseBody($request, $response, $this->serializer, $format); Chris@0: $event->setResponse($this->flattenResponse($response)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines the format to respond in. Chris@0: * Chris@0: * Respects the requested format if one is specified. However, it is common to Chris@0: * forget to specify a request format in case of a POST or PATCH. Rather than Chris@0: * simply throwing an error, we apply the robustness principle: when POSTing Chris@0: * or PATCHing using a certain format, you probably expect a response in that Chris@0: * same format. Chris@0: * Chris@0: * @param \Drupal\Core\Routing\RouteMatchInterface $route_match Chris@0: * The current route match. Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * The current request. Chris@0: * Chris@0: * @return string Chris@0: * The response format. Chris@0: */ Chris@0: public function getResponseFormat(RouteMatchInterface $route_match, Request $request) { Chris@0: $route = $route_match->getRouteObject(); Chris@0: $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : []; Chris@0: $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : []; Chris@0: $acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats; Chris@0: Chris@0: $requested_format = $request->getRequestFormat(); Chris@0: $content_type_format = $request->getContentType(); Chris@0: Chris@0: // If an acceptable format is requested, then use that. Otherwise, including Chris@0: // and particularly when the client forgot to specify a format, then use Chris@0: // heuristics to select the format that is most likely expected. Chris@0: if (in_array($requested_format, $acceptable_formats)) { Chris@0: return $requested_format; Chris@0: } Chris@0: // If a request body is present, then use the format corresponding to the Chris@0: // request body's Content-Type for the response, if it's an acceptable Chris@0: // format for the request. Chris@0: elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) { Chris@0: return $content_type_format; Chris@0: } Chris@0: // Otherwise, use the first acceptable format. Chris@0: elseif (!empty($acceptable_formats)) { Chris@0: return $acceptable_formats[0]; Chris@0: } Chris@0: // Sometimes, there are no acceptable formats, e.g. DELETE routes. Chris@0: else { Chris@0: return NULL; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders a resource response body. Chris@0: * Chris@0: * Serialization can invoke rendering (e.g., generating URLs), but the Chris@0: * serialization API does not provide a mechanism to collect the Chris@0: * bubbleable metadata associated with that (e.g., language and other Chris@0: * contexts), so instead, allow those to "leak" and collect them here in Chris@0: * a render context. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * The request object. Chris@0: * @param \Drupal\rest\ResourceResponseInterface $response Chris@0: * The response from the REST resource. Chris@0: * @param \Symfony\Component\Serializer\SerializerInterface $serializer Chris@0: * The serializer to use. Chris@0: * @param string|null $format Chris@0: * The response format, or NULL in case the response does not need a format, Chris@0: * for example for the response to a DELETE request. Chris@0: * Chris@0: * @todo Add test coverage for language negotiation contexts in Chris@0: * https://www.drupal.org/node/2135829. Chris@0: */ Chris@0: protected function renderResponseBody(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) { Chris@0: $data = $response->getResponseData(); Chris@0: Chris@0: // If there is data to send, serialize and set it as the response body. Chris@0: if ($data !== NULL) { Chris@0: $context = new RenderContext(); Chris@0: $output = $this->renderer Chris@0: ->executeInRenderContext($context, function () use ($serializer, $data, $format) { Chris@0: return $serializer->serialize($data, $format); Chris@0: }); Chris@0: Chris@0: if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) { Chris@0: $response->addCacheableDependency($context->pop()); Chris@0: } Chris@0: Chris@0: $response->setContent($output); Chris@0: $response->headers->set('Content-Type', $request->getMimeType($format)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Flattens a fully rendered resource response. Chris@0: * Chris@0: * Ensures that complex data structures in ResourceResponse::getResponseData() Chris@0: * are not serialized. Not doing this means that caching this response object Chris@0: * requires unserializing the PHP data when reading this response object from Chris@0: * cache, which can be very costly, and is unnecessary. Chris@0: * Chris@0: * @param \Drupal\rest\ResourceResponseInterface $response Chris@0: * A fully rendered resource response. Chris@0: * Chris@0: * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response Chris@0: * The flattened response. Chris@0: */ Chris@0: protected function flattenResponse(ResourceResponseInterface $response) { Chris@0: $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response(); Chris@0: $final_response->setContent($response->getContent()); Chris@0: $final_response->setStatusCode($response->getStatusCode()); Chris@0: $final_response->setProtocolVersion($response->getProtocolVersion()); Chris@0: $final_response->setCharset($response->getCharset()); Chris@0: $final_response->headers = clone $response->headers; Chris@0: if ($final_response instanceof CacheableResponseInterface) { Chris@0: $final_response->addCacheableDependency($response->getCacheableMetadata()); Chris@0: } Chris@0: return $final_response; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function getSubscribedEvents() { Chris@0: // Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber Chris@0: // (priority 100), so that Dynamic Page Cache can cache flattened responses. Chris@0: $events[KernelEvents::RESPONSE][] = ['onResponse', 128]; Chris@0: return $events; Chris@0: } Chris@0: Chris@0: }