annotate core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php @ 19:fa3358dc1485 tip

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