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