5 use Drupal\Component\Utility\ArgumentsResolver;
6 use Drupal\Core\Cache\CacheableResponseInterface;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\Core\Routing\RouteMatchInterface;
11 use Drupal\rest\Plugin\ResourceInterface;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13 use Symfony\Component\HttpFoundation\Request;
14 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
15 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
16 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
17 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
18 use Symfony\Component\Serializer\SerializerInterface;
21 * Acts as intermediate request forwarder for resource plugins.
23 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
25 class RequestHandler implements ContainerInjectionInterface {
30 * @var \Drupal\Core\Config\ConfigFactoryInterface
32 protected $configFactory;
37 * @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface
39 protected $serializer;
42 * Creates a new RequestHandler instance.
44 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
46 * @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer
49 public function __construct(ConfigFactoryInterface $config_factory, SerializerInterface $serializer) {
50 $this->configFactory = $config_factory;
51 $this->serializer = $serializer;
57 public static function create(ContainerInterface $container) {
59 $container->get('config.factory'),
60 $container->get('serializer')
65 * Handles a REST API request.
67 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
69 * @param \Symfony\Component\HttpFoundation\Request $request
70 * The HTTP request object.
71 * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
72 * REST resource config entity ID.
74 * @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
75 * The REST resource response.
77 public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
78 $response = $this->delegateToRestResourcePlugin($route_match, $request, $_rest_resource_config->getResourcePlugin());
80 if ($response instanceof CacheableResponseInterface) {
81 $response->addCacheableDependency($_rest_resource_config);
82 // Add global rest settings config's cache tag, for BC flags.
83 // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
84 // @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
85 // @todo Remove in https://www.drupal.org/node/2893804
86 $response->addCacheableDependency($this->configFactory->get('rest.settings'));
93 * Gets the normalized HTTP request method of the matched route.
95 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
99 * The normalized HTTP request method.
101 protected static function getNormalizedRequestMethod(RouteMatchInterface $route_match) {
102 // Symfony is built to transparently map HEAD requests to a GET request. In
103 // the case of the REST module's RequestHandler though, we essentially have
104 // our own light-weight routing system on top of the Drupal/symfony routing
105 // system. So, we have to respect the decision that the routing system made:
106 // we look not at the request method, but at the route's method. All REST
107 // routes are guaranteed to have _method set.
108 // Response::prepare() will transform it to a HEAD response at the very last
110 // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
111 // @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
112 // @see \Symfony\Component\HttpFoundation\Response::prepare()
113 $method = strtolower($route_match->getRouteObject()->getMethods()[0]);
114 assert(count($route_match->getRouteObject()->getMethods()) === 1);
119 * Deserializes request body, if any.
121 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
123 * @param \Symfony\Component\HttpFoundation\Request $request
124 * The HTTP request object.
125 * @param \Drupal\rest\Plugin\ResourceInterface $resource
126 * The REST resource plugin.
129 * An object normalization, ikf there is a valid request body. NULL if there
130 * is no request body.
132 * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
133 * Thrown if the request body cannot be decoded.
134 * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
135 * Thrown if the request body cannot be denormalized.
137 protected function deserialize(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
138 // Deserialize incoming data if available.
139 $received = $request->getContent();
140 $unserialized = NULL;
141 if (!empty($received)) {
142 $method = static::getNormalizedRequestMethod($route_match);
143 $format = $request->getContentType();
145 $definition = $resource->getPluginDefinition();
147 // First decode the request data. We can then determine if the
148 // serialized data was malformed.
150 $unserialized = $this->serializer->decode($received, $format, ['request_method' => $method]);
152 catch (UnexpectedValueException $e) {
153 // If an exception was thrown at this stage, there was a problem
154 // decoding the data. Throw a 400 http exception.
155 throw new BadRequestHttpException($e->getMessage());
158 // Then attempt to denormalize if there is a serialization class.
159 if (!empty($definition['serialization_class'])) {
161 $unserialized = $this->serializer->denormalize($unserialized, $definition['serialization_class'], $format, ['request_method' => $method]);
163 // These two serialization exception types mean there was a problem
164 // with the structure of the decoded data and it's not valid.
165 catch (UnexpectedValueException $e) {
166 throw new UnprocessableEntityHttpException($e->getMessage());
168 catch (InvalidArgumentException $e) {
169 throw new UnprocessableEntityHttpException($e->getMessage());
174 return $unserialized;
178 * Delegates an incoming request to the appropriate REST resource plugin.
180 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
182 * @param \Symfony\Component\HttpFoundation\Request $request
183 * The HTTP request object.
184 * @param \Drupal\rest\Plugin\ResourceInterface $resource
185 * The REST resource plugin.
187 * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
188 * The REST resource response.
190 protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
191 $unserialized = $this->deserialize($route_match, $request, $resource);
192 $method = static::getNormalizedRequestMethod($route_match);
194 // Determine the request parameters that should be passed to the resource
196 $argument_resolver = $this->createArgumentResolver($route_match, $unserialized, $request);
198 $arguments = $argument_resolver->getArguments([$resource, $method]);
200 catch (\RuntimeException $exception) {
201 @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);
202 $arguments = $this->getLegacyParameters($route_match, $unserialized, $request);
205 // Invoke the operation on the resource plugin.
206 return call_user_func_array([$resource, $method], $arguments);
210 * Creates an argument resolver, containing all REST parameters.
212 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
214 * @param mixed $unserialized
215 * The unserialized data.
216 * @param \Symfony\Component\HttpFoundation\Request $request
219 * @return \Drupal\Component\Utility\ArgumentsResolver
220 * An instance of the argument resolver containing information like the
221 * 'entity' we process and the 'unserialized' content from the request body.
223 protected function createArgumentResolver(RouteMatchInterface $route_match, $unserialized, Request $request) {
224 $route = $route_match->getRouteObject();
226 // Defaults for the parameters defined on the route object need to be added
227 // to the raw arguments.
228 $raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
230 $route_arguments = $route_match->getParameters()->all();
231 $upcasted_route_arguments = $route_arguments;
233 // For request methods that have request bodies, ResourceInterface plugin
234 // methods historically receive the unserialized request body as the N+1th
235 // method argument, where N is the number of route parameters specified on
236 // the accompanying route. To be able to use the argument resolver, which is
237 // not based on position but on name and typehint, specify commonly used
238 // names here. Similarly, those methods receive the original stored object
239 // as the first method argument.
241 $route_arguments_entity = NULL;
242 // Try to find a parameter which is an entity.
243 foreach ($route_arguments as $value) {
244 if ($value instanceof EntityInterface) {
245 $route_arguments_entity = $value;
250 if (in_array($request->getMethod(), ['PATCH', 'POST'], TRUE)) {
251 $upcasted_route_arguments['entity'] = $unserialized;
252 $upcasted_route_arguments['data'] = $unserialized;
253 $upcasted_route_arguments['unserialized'] = $unserialized;
254 $upcasted_route_arguments['original_entity'] = $route_arguments_entity;
257 $upcasted_route_arguments['entity'] = $route_arguments_entity;
260 // Parameters which are not defined on the route object, but still are
261 // essential for access checking are passed as wildcards to the argument
263 $wildcard_arguments = [$route, $route_match];
264 $wildcard_arguments[] = $request;
265 if (isset($unserialized)) {
266 $wildcard_arguments[] = $unserialized;
269 return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
273 * Provides the parameter usable without an argument resolver.
275 * This creates an list of parameters in a statically defined order.
277 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
279 * @param mixed $unserialized
280 * The unserialized data.
281 * @param \Symfony\Component\HttpFoundation\Request $request
284 * @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0.0. Use the
285 * argument resolver method instead, see ::createArgumentResolver().
287 * @see https://www.drupal.org/node/2894819
290 * An array of parameters.
292 protected function getLegacyParameters(RouteMatchInterface $route_match, $unserialized, Request $request) {
293 $route_parameters = $route_match->getParameters();
295 // Filter out all internal parameters starting with "_".
296 foreach ($route_parameters as $key => $parameter) {
297 if ($key{0} !== '_') {
298 $parameters[] = $parameter;
302 return array_merge($parameters, [$unserialized, $request]);