annotate core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents
children
rev   line source
Chris@5 1 <?php
Chris@5 2
Chris@5 3 namespace Drupal\jsonapi\EventSubscriber;
Chris@5 4
Chris@5 5 use Drupal\Core\Cache\CacheableResponse;
Chris@5 6 use Drupal\Core\Cache\CacheableResponseInterface;
Chris@5 7 use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
Chris@5 8 use Drupal\jsonapi\ResourceResponse;
Chris@5 9 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
Chris@5 10 use Symfony\Component\HttpFoundation\Request;
Chris@5 11 use Symfony\Component\HttpFoundation\Response;
Chris@5 12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
Chris@5 13 use Symfony\Component\HttpKernel\KernelEvents;
Chris@5 14 use Symfony\Component\Serializer\SerializerInterface;
Chris@5 15
Chris@5 16 /**
Chris@5 17 * Response subscriber that serializes and removes ResourceResponses' data.
Chris@5 18 *
Chris@5 19 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
Chris@5 20 * may change at any time and could break any dependencies on it.
Chris@5 21 *
Chris@5 22 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@5 23 * @see jsonapi.api.php
Chris@5 24 *
Chris@5 25 * This is 99% identical to:
Chris@5 26 *
Chris@5 27 * \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
Chris@5 28 *
Chris@5 29 * but with a few differences:
Chris@5 30 * 1. It has the @jsonapi.serializer service injected instead of @serializer
Chris@5 31 * 2. It has the @current_route_match service no longer injected
Chris@5 32 * 3. It hardcodes the format to 'api_json'
Chris@5 33 * 4. It adds the CacheableNormalization object returned by JSON:API
Chris@5 34 * normalization to the response object.
Chris@5 35 * 5. It flattens only to a cacheable response if the HTTP method is cacheable.
Chris@5 36 *
Chris@5 37 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
Chris@5 38 */
Chris@5 39 class ResourceResponseSubscriber implements EventSubscriberInterface {
Chris@5 40
Chris@5 41 /**
Chris@5 42 * The serializer.
Chris@5 43 *
Chris@5 44 * @var \Symfony\Component\Serializer\SerializerInterface
Chris@5 45 */
Chris@5 46 protected $serializer;
Chris@5 47
Chris@5 48 /**
Chris@5 49 * Constructs a ResourceResponseSubscriber object.
Chris@5 50 *
Chris@5 51 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
Chris@5 52 * The serializer.
Chris@5 53 */
Chris@5 54 public function __construct(SerializerInterface $serializer) {
Chris@5 55 $this->serializer = $serializer;
Chris@5 56 }
Chris@5 57
Chris@5 58 /**
Chris@5 59 * {@inheritdoc}
Chris@5 60 *
Chris@5 61 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getSubscribedEvents()
Chris@5 62 * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
Chris@5 63 */
Chris@5 64 public static function getSubscribedEvents() {
Chris@5 65 // Run before the dynamic page cache subscriber (priority 100), so that
Chris@5 66 // Dynamic Page Cache can cache flattened responses.
Chris@5 67 $events[KernelEvents::RESPONSE][] = ['onResponse', 128];
Chris@5 68 return $events;
Chris@5 69 }
Chris@5 70
Chris@5 71 /**
Chris@5 72 * Serializes ResourceResponse responses' data, and removes that data.
Chris@5 73 *
Chris@5 74 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
Chris@5 75 * The event to process.
Chris@5 76 */
Chris@5 77 public function onResponse(FilterResponseEvent $event) {
Chris@5 78 $response = $event->getResponse();
Chris@5 79 if (!$response instanceof ResourceResponse) {
Chris@5 80 return;
Chris@5 81 }
Chris@5 82
Chris@5 83 $request = $event->getRequest();
Chris@5 84 $format = 'api_json';
Chris@5 85 $this->renderResponseBody($request, $response, $this->serializer, $format);
Chris@5 86 $event->setResponse($this->flattenResponse($response, $request));
Chris@5 87 }
Chris@5 88
Chris@5 89 /**
Chris@5 90 * Renders a resource response body.
Chris@5 91 *
Chris@5 92 * Serialization can invoke rendering (e.g., generating URLs), but the
Chris@5 93 * serialization API does not provide a mechanism to collect the
Chris@5 94 * bubbleable metadata associated with that (e.g., language and other
Chris@5 95 * contexts), so instead, allow those to "leak" and collect them here in
Chris@5 96 * a render context.
Chris@5 97 *
Chris@5 98 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@5 99 * The request object.
Chris@5 100 * @param \Drupal\jsonapi\ResourceResponse $response
Chris@5 101 * The response from the JSON:API resource.
Chris@5 102 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
Chris@5 103 * The serializer to use.
Chris@5 104 * @param string|null $format
Chris@5 105 * The response format, or NULL in case the response does not need a format,
Chris@5 106 * for example for the response to a DELETE request.
Chris@5 107 *
Chris@5 108 * @todo Add test coverage for language negotiation contexts in
Chris@5 109 * https://www.drupal.org/node/2135829.
Chris@5 110 */
Chris@5 111 protected function renderResponseBody(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format) {
Chris@5 112 $data = $response->getResponseData();
Chris@5 113
Chris@5 114 // If there is data to send, serialize and set it as the response body.
Chris@5 115 if ($data !== NULL) {
Chris@5 116 // First normalize the data. Note that error responses do not need a
Chris@5 117 // normalization context, since there are no entities to normalize.
Chris@5 118 // @see \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber::isJsonApiExceptionEvent()
Chris@5 119 $context = !$response->isSuccessful() ? [] : static::generateContext($request);
Chris@5 120 $jsonapi_doc_object = $serializer->normalize($data, $format, $context);
Chris@5 121 // Having just normalized the data, we can associate its cacheability with
Chris@5 122 // the response object.
Chris@5 123 assert($jsonapi_doc_object instanceof CacheableNormalization);
Chris@5 124 $response->addCacheableDependency($jsonapi_doc_object);
Chris@5 125 // Finally, encode the normalized data (JSON:API's encoder rasterizes it
Chris@5 126 // automatically).
Chris@5 127 $response->setContent($serializer->encode($jsonapi_doc_object->getNormalization(), $format));
Chris@5 128 $response->headers->set('Content-Type', $request->getMimeType($format));
Chris@5 129 }
Chris@5 130 }
Chris@5 131
Chris@5 132 /**
Chris@5 133 * Generates a top-level JSON:API normalization context.
Chris@5 134 *
Chris@5 135 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@5 136 * The request from which the context can be derived.
Chris@5 137 *
Chris@5 138 * @return array
Chris@5 139 * The generated context.
Chris@5 140 */
Chris@5 141 protected static function generateContext(Request $request) {
Chris@5 142 // Build the expanded context.
Chris@5 143 $context = [
Chris@5 144 'account' => NULL,
Chris@5 145 'sparse_fieldset' => NULL,
Chris@5 146 ];
Chris@5 147 if ($request->query->get('fields')) {
Chris@5 148 $context['sparse_fieldset'] = array_map(function ($item) {
Chris@5 149 return explode(',', $item);
Chris@5 150 }, $request->query->get('fields'));
Chris@5 151 }
Chris@5 152 return $context;
Chris@5 153 }
Chris@5 154
Chris@5 155 /**
Chris@5 156 * Flattens a fully rendered resource response.
Chris@5 157 *
Chris@5 158 * Ensures that complex data structures in ResourceResponse::getResponseData()
Chris@5 159 * are not serialized. Not doing this means that caching this response object
Chris@5 160 * requires deserializing the PHP data when reading this response object from
Chris@5 161 * cache, which can be very costly, and is unnecessary.
Chris@5 162 *
Chris@5 163 * @param \Drupal\jsonapi\ResourceResponse $response
Chris@5 164 * A fully rendered resource response.
Chris@5 165 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@5 166 * The request for which this response is generated.
Chris@5 167 *
Chris@5 168 * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
Chris@5 169 * The flattened response.
Chris@5 170 */
Chris@5 171 protected static function flattenResponse(ResourceResponse $response, Request $request) {
Chris@5 172 $final_response = ($response instanceof CacheableResponseInterface && $request->isMethodCacheable()) ? new CacheableResponse() : new Response();
Chris@5 173 $final_response->setContent($response->getContent());
Chris@5 174 $final_response->setStatusCode($response->getStatusCode());
Chris@5 175 $final_response->setProtocolVersion($response->getProtocolVersion());
Chris@5 176 if ($charset = $response->getCharset()) {
Chris@5 177 $final_response->setCharset($charset);
Chris@5 178 }
Chris@5 179 $final_response->headers = clone $response->headers;
Chris@5 180 if ($final_response instanceof CacheableResponseInterface) {
Chris@5 181 $final_response->addCacheableDependency($response->getCacheableMetadata());
Chris@5 182 }
Chris@5 183 return $final_response;
Chris@5 184 }
Chris@5 185
Chris@5 186 }