annotate 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
rev   line source
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 }