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

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\rest;
Chris@0 4
Chris@14 5 use Drupal\Component\Utility\ArgumentsResolver;
Chris@0 6 use Drupal\Core\Cache\CacheableResponseInterface;
Chris@14 7 use Drupal\Core\Config\ConfigFactoryInterface;
Chris@0 8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
Chris@14 9 use Drupal\Core\Entity\EntityInterface;
Chris@0 10 use Drupal\Core\Routing\RouteMatchInterface;
Chris@14 11 use Drupal\rest\Plugin\ResourceInterface;
Chris@0 12 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@0 13 use Symfony\Component\HttpFoundation\Request;
Chris@0 14 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
Chris@0 15 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
Chris@0 16 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
Chris@0 17 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
Chris@14 18 use Symfony\Component\Serializer\SerializerInterface;
Chris@0 19
Chris@0 20 /**
Chris@0 21 * Acts as intermediate request forwarder for resource plugins.
Chris@0 22 *
Chris@0 23 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
Chris@0 24 */
Chris@14 25 class RequestHandler implements ContainerInjectionInterface {
Chris@0 26
Chris@0 27 /**
Chris@14 28 * The config factory.
Chris@0 29 *
Chris@14 30 * @var \Drupal\Core\Config\ConfigFactoryInterface
Chris@0 31 */
Chris@14 32 protected $configFactory;
Chris@14 33
Chris@14 34 /**
Chris@14 35 * The serializer.
Chris@14 36 *
Chris@14 37 * @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface
Chris@14 38 */
Chris@14 39 protected $serializer;
Chris@0 40
Chris@0 41 /**
Chris@0 42 * Creates a new RequestHandler instance.
Chris@0 43 *
Chris@14 44 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
Chris@14 45 * The config factory.
Chris@14 46 * @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer
Chris@14 47 * The serializer.
Chris@0 48 */
Chris@14 49 public function __construct(ConfigFactoryInterface $config_factory, SerializerInterface $serializer) {
Chris@14 50 $this->configFactory = $config_factory;
Chris@14 51 $this->serializer = $serializer;
Chris@0 52 }
Chris@0 53
Chris@0 54 /**
Chris@0 55 * {@inheritdoc}
Chris@0 56 */
Chris@0 57 public static function create(ContainerInterface $container) {
Chris@14 58 return new static(
Chris@14 59 $container->get('config.factory'),
Chris@14 60 $container->get('serializer')
Chris@14 61 );
Chris@0 62 }
Chris@0 63
Chris@0 64 /**
Chris@14 65 * Handles a REST API request.
Chris@0 66 *
Chris@0 67 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@0 68 * The route match.
Chris@0 69 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 70 * The HTTP request object.
Chris@14 71 * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
Chris@17 72 * The REST resource config entity.
Chris@0 73 *
Chris@14 74 * @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
Chris@14 75 * The REST resource response.
Chris@0 76 */
Chris@14 77 public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
Chris@17 78 $resource = $_rest_resource_config->getResourcePlugin();
Chris@17 79 $unserialized = $this->deserialize($route_match, $request, $resource);
Chris@17 80 $response = $this->delegateToRestResourcePlugin($route_match, $request, $unserialized, $resource);
Chris@17 81 return $this->prepareResponse($response, $_rest_resource_config);
Chris@17 82 }
Chris@14 83
Chris@17 84 /**
Chris@17 85 * Handles a REST API request without deserializing the request body.
Chris@17 86 *
Chris@17 87 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@17 88 * The route match.
Chris@17 89 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@17 90 * The HTTP request object.
Chris@17 91 * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
Chris@17 92 * The REST resource config entity.
Chris@17 93 *
Chris@17 94 * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
Chris@17 95 * The REST resource response.
Chris@17 96 */
Chris@17 97 public function handleRaw(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
Chris@17 98 $resource = $_rest_resource_config->getResourcePlugin();
Chris@17 99 $response = $this->delegateToRestResourcePlugin($route_match, $request, NULL, $resource);
Chris@17 100 return $this->prepareResponse($response, $_rest_resource_config);
Chris@17 101 }
Chris@17 102
Chris@17 103 /**
Chris@17 104 * Prepares the REST resource response.
Chris@17 105 *
Chris@17 106 * @param \Drupal\rest\ResourceResponseInterface $response
Chris@17 107 * The REST resource response.
Chris@17 108 * @param \Drupal\rest\RestResourceConfigInterface $resource_config
Chris@17 109 * The REST resource config entity.
Chris@17 110 *
Chris@17 111 * @return \Drupal\rest\ResourceResponseInterface
Chris@17 112 * The prepared REST resource response.
Chris@17 113 */
Chris@17 114 protected function prepareResponse($response, RestResourceConfigInterface $resource_config) {
Chris@14 115 if ($response instanceof CacheableResponseInterface) {
Chris@17 116 $response->addCacheableDependency($resource_config);
Chris@14 117 // Add global rest settings config's cache tag, for BC flags.
Chris@14 118 // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
Chris@14 119 // @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
Chris@14 120 // @todo Remove in https://www.drupal.org/node/2893804
Chris@14 121 $response->addCacheableDependency($this->configFactory->get('rest.settings'));
Chris@14 122 }
Chris@14 123
Chris@14 124 return $response;
Chris@14 125 }
Chris@14 126
Chris@14 127 /**
Chris@14 128 * Gets the normalized HTTP request method of the matched route.
Chris@14 129 *
Chris@14 130 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@14 131 * The route match.
Chris@14 132 *
Chris@14 133 * @return string
Chris@14 134 * The normalized HTTP request method.
Chris@14 135 */
Chris@14 136 protected static function getNormalizedRequestMethod(RouteMatchInterface $route_match) {
Chris@0 137 // Symfony is built to transparently map HEAD requests to a GET request. In
Chris@0 138 // the case of the REST module's RequestHandler though, we essentially have
Chris@0 139 // our own light-weight routing system on top of the Drupal/symfony routing
Chris@0 140 // system. So, we have to respect the decision that the routing system made:
Chris@0 141 // we look not at the request method, but at the route's method. All REST
Chris@0 142 // routes are guaranteed to have _method set.
Chris@0 143 // Response::prepare() will transform it to a HEAD response at the very last
Chris@0 144 // moment.
Chris@0 145 // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
Chris@0 146 // @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
Chris@0 147 // @see \Symfony\Component\HttpFoundation\Response::prepare()
Chris@0 148 $method = strtolower($route_match->getRouteObject()->getMethods()[0]);
Chris@0 149 assert(count($route_match->getRouteObject()->getMethods()) === 1);
Chris@14 150 return $method;
Chris@14 151 }
Chris@0 152
Chris@14 153 /**
Chris@14 154 * Deserializes request body, if any.
Chris@14 155 *
Chris@14 156 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@14 157 * The route match.
Chris@14 158 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@14 159 * The HTTP request object.
Chris@14 160 * @param \Drupal\rest\Plugin\ResourceInterface $resource
Chris@14 161 * The REST resource plugin.
Chris@14 162 *
Chris@14 163 * @return array|null
Chris@14 164 * An object normalization, ikf there is a valid request body. NULL if there
Chris@14 165 * is no request body.
Chris@14 166 *
Chris@14 167 * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
Chris@14 168 * Thrown if the request body cannot be decoded.
Chris@14 169 * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
Chris@14 170 * Thrown if the request body cannot be denormalized.
Chris@14 171 */
Chris@14 172 protected function deserialize(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
Chris@0 173 // Deserialize incoming data if available.
Chris@0 174 $received = $request->getContent();
Chris@0 175 $unserialized = NULL;
Chris@0 176 if (!empty($received)) {
Chris@14 177 $method = static::getNormalizedRequestMethod($route_match);
Chris@0 178 $format = $request->getContentType();
Chris@0 179
Chris@0 180 $definition = $resource->getPluginDefinition();
Chris@0 181
Chris@0 182 // First decode the request data. We can then determine if the
Chris@0 183 // serialized data was malformed.
Chris@0 184 try {
Chris@14 185 $unserialized = $this->serializer->decode($received, $format, ['request_method' => $method]);
Chris@0 186 }
Chris@0 187 catch (UnexpectedValueException $e) {
Chris@0 188 // If an exception was thrown at this stage, there was a problem
Chris@0 189 // decoding the data. Throw a 400 http exception.
Chris@0 190 throw new BadRequestHttpException($e->getMessage());
Chris@0 191 }
Chris@0 192
Chris@0 193 // Then attempt to denormalize if there is a serialization class.
Chris@0 194 if (!empty($definition['serialization_class'])) {
Chris@0 195 try {
Chris@14 196 $unserialized = $this->serializer->denormalize($unserialized, $definition['serialization_class'], $format, ['request_method' => $method]);
Chris@0 197 }
Chris@0 198 // These two serialization exception types mean there was a problem
Chris@0 199 // with the structure of the decoded data and it's not valid.
Chris@0 200 catch (UnexpectedValueException $e) {
Chris@0 201 throw new UnprocessableEntityHttpException($e->getMessage());
Chris@0 202 }
Chris@0 203 catch (InvalidArgumentException $e) {
Chris@0 204 throw new UnprocessableEntityHttpException($e->getMessage());
Chris@0 205 }
Chris@0 206 }
Chris@0 207 }
Chris@0 208
Chris@14 209 return $unserialized;
Chris@14 210 }
Chris@14 211
Chris@14 212 /**
Chris@14 213 * Delegates an incoming request to the appropriate REST resource plugin.
Chris@14 214 *
Chris@14 215 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@14 216 * The route match.
Chris@14 217 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@14 218 * The HTTP request object.
Chris@17 219 * @param mixed|null $unserialized
Chris@17 220 * The unserialized request body, if any.
Chris@14 221 * @param \Drupal\rest\Plugin\ResourceInterface $resource
Chris@14 222 * The REST resource plugin.
Chris@14 223 *
Chris@14 224 * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
Chris@14 225 * The REST resource response.
Chris@14 226 */
Chris@17 227 protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, $unserialized, ResourceInterface $resource) {
Chris@14 228 $method = static::getNormalizedRequestMethod($route_match);
Chris@14 229
Chris@0 230 // Determine the request parameters that should be passed to the resource
Chris@0 231 // plugin.
Chris@14 232 $argument_resolver = $this->createArgumentResolver($route_match, $unserialized, $request);
Chris@14 233 try {
Chris@14 234 $arguments = $argument_resolver->getArguments([$resource, $method]);
Chris@14 235 }
Chris@14 236 catch (\RuntimeException $exception) {
Chris@14 237 @trigger_error('Passing in arguments the legacy way is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Provide the right parameter names in the method, similar to controllers. See https://www.drupal.org/node/2894819', E_USER_DEPRECATED);
Chris@14 238 $arguments = $this->getLegacyParameters($route_match, $unserialized, $request);
Chris@14 239 }
Chris@14 240
Chris@14 241 // Invoke the operation on the resource plugin.
Chris@14 242 return call_user_func_array([$resource, $method], $arguments);
Chris@14 243 }
Chris@14 244
Chris@14 245 /**
Chris@14 246 * Creates an argument resolver, containing all REST parameters.
Chris@14 247 *
Chris@14 248 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@14 249 * The route match.
Chris@14 250 * @param mixed $unserialized
Chris@14 251 * The unserialized data.
Chris@14 252 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@14 253 * The request.
Chris@14 254 *
Chris@14 255 * @return \Drupal\Component\Utility\ArgumentsResolver
Chris@14 256 * An instance of the argument resolver containing information like the
Chris@14 257 * 'entity' we process and the 'unserialized' content from the request body.
Chris@14 258 */
Chris@14 259 protected function createArgumentResolver(RouteMatchInterface $route_match, $unserialized, Request $request) {
Chris@14 260 $route = $route_match->getRouteObject();
Chris@14 261
Chris@14 262 // Defaults for the parameters defined on the route object need to be added
Chris@14 263 // to the raw arguments.
Chris@14 264 $raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
Chris@14 265
Chris@14 266 $route_arguments = $route_match->getParameters()->all();
Chris@14 267 $upcasted_route_arguments = $route_arguments;
Chris@14 268
Chris@14 269 // For request methods that have request bodies, ResourceInterface plugin
Chris@14 270 // methods historically receive the unserialized request body as the N+1th
Chris@14 271 // method argument, where N is the number of route parameters specified on
Chris@14 272 // the accompanying route. To be able to use the argument resolver, which is
Chris@14 273 // not based on position but on name and typehint, specify commonly used
Chris@14 274 // names here. Similarly, those methods receive the original stored object
Chris@14 275 // as the first method argument.
Chris@14 276
Chris@14 277 $route_arguments_entity = NULL;
Chris@14 278 // Try to find a parameter which is an entity.
Chris@14 279 foreach ($route_arguments as $value) {
Chris@14 280 if ($value instanceof EntityInterface) {
Chris@14 281 $route_arguments_entity = $value;
Chris@14 282 break;
Chris@14 283 }
Chris@14 284 }
Chris@14 285
Chris@14 286 if (in_array($request->getMethod(), ['PATCH', 'POST'], TRUE)) {
Chris@14 287 $upcasted_route_arguments['entity'] = $unserialized;
Chris@14 288 $upcasted_route_arguments['data'] = $unserialized;
Chris@14 289 $upcasted_route_arguments['unserialized'] = $unserialized;
Chris@14 290 $upcasted_route_arguments['original_entity'] = $route_arguments_entity;
Chris@14 291 }
Chris@14 292 else {
Chris@14 293 $upcasted_route_arguments['entity'] = $route_arguments_entity;
Chris@14 294 }
Chris@14 295
Chris@14 296 // Parameters which are not defined on the route object, but still are
Chris@14 297 // essential for access checking are passed as wildcards to the argument
Chris@14 298 // resolver.
Chris@14 299 $wildcard_arguments = [$route, $route_match];
Chris@14 300 $wildcard_arguments[] = $request;
Chris@14 301 if (isset($unserialized)) {
Chris@14 302 $wildcard_arguments[] = $unserialized;
Chris@14 303 }
Chris@14 304
Chris@14 305 return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
Chris@14 306 }
Chris@14 307
Chris@14 308 /**
Chris@14 309 * Provides the parameter usable without an argument resolver.
Chris@14 310 *
Chris@14 311 * This creates an list of parameters in a statically defined order.
Chris@14 312 *
Chris@14 313 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@14 314 * The route match
Chris@14 315 * @param mixed $unserialized
Chris@14 316 * The unserialized data.
Chris@14 317 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@14 318 * The request.
Chris@14 319 *
Chris@14 320 * @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0.0. Use the
Chris@14 321 * argument resolver method instead, see ::createArgumentResolver().
Chris@14 322 *
Chris@14 323 * @see https://www.drupal.org/node/2894819
Chris@14 324 *
Chris@14 325 * @return array
Chris@14 326 * An array of parameters.
Chris@14 327 */
Chris@14 328 protected function getLegacyParameters(RouteMatchInterface $route_match, $unserialized, Request $request) {
Chris@0 329 $route_parameters = $route_match->getParameters();
Chris@0 330 $parameters = [];
Chris@0 331 // Filter out all internal parameters starting with "_".
Chris@0 332 foreach ($route_parameters as $key => $parameter) {
Chris@0 333 if ($key{0} !== '_') {
Chris@0 334 $parameters[] = $parameter;
Chris@0 335 }
Chris@0 336 }
Chris@0 337
Chris@14 338 return array_merge($parameters, [$unserialized, $request]);
Chris@0 339 }
Chris@0 340
Chris@0 341 }