diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php	Thu May 09 15:34:47 2019 +0100
@@ -0,0 +1,186 @@
+<?php
+
+namespace Drupal\jsonapi\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
+use Drupal\jsonapi\ResourceResponse;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Response subscriber that serializes and removes ResourceResponses' data.
+ *
+ * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
+ *   may change at any time and could break any dependencies on it.
+ *
+ * @see https://www.drupal.org/project/jsonapi/issues/3032787
+ * @see jsonapi.api.php
+ *
+ * This is 99% identical to:
+ *
+ * \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+ *
+ * but with a few differences:
+ * 1. It has the @jsonapi.serializer service injected instead of @serializer
+ * 2. It has the @current_route_match service no longer injected
+ * 3. It hardcodes the format to 'api_json'
+ * 4. It adds the CacheableNormalization object returned by JSON:API
+ *    normalization to the response object.
+ * 5. It flattens only to a cacheable response if the HTTP method is cacheable.
+ *
+ * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+ */
+class ResourceResponseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected $serializer;
+
+  /**
+   * Constructs a ResourceResponseSubscriber object.
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer.
+   */
+  public function __construct(SerializerInterface $serializer) {
+    $this->serializer = $serializer;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getSubscribedEvents()
+   * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
+   */
+  public static function getSubscribedEvents() {
+    // Run before the dynamic page cache subscriber (priority 100), so that
+    // Dynamic Page Cache can cache flattened responses.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 128];
+    return $events;
+  }
+
+  /**
+   * Serializes ResourceResponse responses' data, and removes that data.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    if (!$response instanceof ResourceResponse) {
+      return;
+    }
+
+    $request = $event->getRequest();
+    $format = 'api_json';
+    $this->renderResponseBody($request, $response, $this->serializer, $format);
+    $event->setResponse($this->flattenResponse($response, $request));
+  }
+
+  /**
+   * Renders a resource response body.
+   *
+   * Serialization can invoke rendering (e.g., generating URLs), but the
+   * serialization API does not provide a mechanism to collect the
+   * bubbleable metadata associated with that (e.g., language and other
+   * contexts), so instead, allow those to "leak" and collect them here in
+   * a render context.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param \Drupal\jsonapi\ResourceResponse $response
+   *   The response from the JSON:API resource.
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer to use.
+   * @param string|null $format
+   *   The response format, or NULL in case the response does not need a format,
+   *   for example for the response to a DELETE request.
+   *
+   * @todo Add test coverage for language negotiation contexts in
+   *   https://www.drupal.org/node/2135829.
+   */
+  protected function renderResponseBody(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format) {
+    $data = $response->getResponseData();
+
+    // If there is data to send, serialize and set it as the response body.
+    if ($data !== NULL) {
+      // First normalize the data. Note that error responses do not need a
+      // normalization context, since there are no entities to normalize.
+      // @see \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber::isJsonApiExceptionEvent()
+      $context = !$response->isSuccessful() ? [] : static::generateContext($request);
+      $jsonapi_doc_object = $serializer->normalize($data, $format, $context);
+      // Having just normalized the data, we can associate its cacheability with
+      // the response object.
+      assert($jsonapi_doc_object instanceof CacheableNormalization);
+      $response->addCacheableDependency($jsonapi_doc_object);
+      // Finally, encode the normalized data (JSON:API's encoder rasterizes it
+      // automatically).
+      $response->setContent($serializer->encode($jsonapi_doc_object->getNormalization(), $format));
+      $response->headers->set('Content-Type', $request->getMimeType($format));
+    }
+  }
+
+  /**
+   * Generates a top-level JSON:API normalization context.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request from which the context can be derived.
+   *
+   * @return array
+   *   The generated context.
+   */
+  protected static function generateContext(Request $request) {
+    // Build the expanded context.
+    $context = [
+      'account' => NULL,
+      'sparse_fieldset' => NULL,
+    ];
+    if ($request->query->get('fields')) {
+      $context['sparse_fieldset'] = array_map(function ($item) {
+        return explode(',', $item);
+      }, $request->query->get('fields'));
+    }
+    return $context;
+  }
+
+  /**
+   * Flattens a fully rendered resource response.
+   *
+   * Ensures that complex data structures in ResourceResponse::getResponseData()
+   * are not serialized. Not doing this means that caching this response object
+   * requires deserializing the PHP data when reading this response object from
+   * cache, which can be very costly, and is unnecessary.
+   *
+   * @param \Drupal\jsonapi\ResourceResponse $response
+   *   A fully rendered resource response.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for which this response is generated.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
+   *   The flattened response.
+   */
+  protected static function flattenResponse(ResourceResponse $response, Request $request) {
+    $final_response = ($response instanceof CacheableResponseInterface && $request->isMethodCacheable()) ? new CacheableResponse() : new Response();
+    $final_response->setContent($response->getContent());
+    $final_response->setStatusCode($response->getStatusCode());
+    $final_response->setProtocolVersion($response->getProtocolVersion());
+    if ($charset = $response->getCharset()) {
+      $final_response->setCharset($charset);
+    }
+    $final_response->headers = clone $response->headers;
+    if ($final_response instanceof CacheableResponseInterface) {
+      $final_response->addCacheableDependency($response->getCacheableMetadata());
+    }
+    return $final_response;
+  }
+
+}