comparison core/modules/jsonapi/src/Context/FieldResolver.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
comparison
equal deleted inserted replaced
4:a9cd425dd02b 5:12f9dff5fda9
1 <?php
2
3 namespace Drupal\jsonapi\Context;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Access\AccessResult;
7 use Drupal\Core\Access\AccessResultInterface;
8 use Drupal\Core\Access\AccessResultReasonInterface;
9 use Drupal\Core\Cache\CacheableMetadata;
10 use Drupal\Core\Entity\EntityFieldManagerInterface;
11 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
12 use Drupal\Core\Entity\EntityTypeManagerInterface;
13 use Drupal\Core\Entity\FieldableEntityInterface;
14 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
15 use Drupal\Core\Extension\ModuleHandlerInterface;
16 use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
17 use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
18 use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
19 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
20 use Drupal\Core\TypedData\DataReferenceTargetDefinition;
21 use Drupal\jsonapi\ResourceType\ResourceType;
22 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
23 use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
24
25 /**
26 * A service that evaluates external path expressions against Drupal fields.
27 *
28 * This class performs 3 essential functions, path resolution, path validation
29 * and path expansion.
30 *
31 * Path resolution:
32 * Path resolution refers to the ability to map a set of external field names to
33 * their internal counterparts. This is necessary because a resource type can
34 * provide aliases for its field names. For example, the resource type @code
35 * node--article @endcode might "alias" the internal field name @code
36 * uid @endcode to the external field name @code author @endcode. This permits
37 * an API consumer to request @code
38 * /jsonapi/node/article?include=author @endcode for a better developer
39 * experience.
40 *
41 * Path validation:
42 * Path validation refers to the ability to ensure that a requested path
43 * corresponds to a valid set of internal fields. For example, if an API
44 * consumer may send a @code GET @endcode request to @code
45 * /jsonapi/node/article?sort=author.field_first_name @endcode. The field
46 * resolver ensures that @code uid @endcode (which would have been resolved
47 * from @code author @endcode) exists on article nodes and that @code
48 * field_first_name @endcode exists on user entities. However, in the case of
49 * an @code include @endcode path, the field resolver would raise a client error
50 * because @code field_first_name @endcode is not an entity reference field,
51 * meaning it does not identify any related resources that can be included in a
52 * compound document.
53 *
54 * Path expansion:
55 * Path expansion refers to the ability to expand a path to an entity query
56 * compatible field expression. For example, a request URL might have a query
57 * string like @code ?filter[field_tags.name]=aviation @endcode, before
58 * constructing the appropriate entity query, the entity query system needs the
59 * path expression to be "expanded" into @code field_tags.entity.name @endcode.
60 * In some rare cases, the entity query system needs this to be expanded to
61 * @code field_tags.entity:taxonomy_term.name @endcode; the field resolver
62 * simply does this by default for every path.
63 *
64 * *Note:* path expansion is *not* performed for @code include @endcode paths.
65 *
66 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
67 * may change at any time and could break any dependencies on it.
68 *
69 * @see https://www.drupal.org/project/jsonapi/issues/3032787
70 * @see jsonapi.api.php
71 */
72 class FieldResolver {
73
74 /**
75 * The entity type manager.
76 *
77 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
78 */
79 protected $entityTypeManager;
80
81 /**
82 * The field manager.
83 *
84 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
85 */
86 protected $fieldManager;
87
88 /**
89 * The entity type bundle information service.
90 *
91 * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
92 */
93 protected $entityTypeBundleInfo;
94
95 /**
96 * The JSON:API resource type repository service.
97 *
98 * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
99 */
100 protected $resourceTypeRepository;
101
102 /**
103 * The module handler.
104 *
105 * @var \Drupal\Core\Extension\ModuleHandlerInterface
106 */
107 protected $moduleHandler;
108
109 /**
110 * Creates a FieldResolver instance.
111 *
112 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
113 * The entity type manager.
114 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
115 * The field manager.
116 * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
117 * The bundle info service.
118 * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
119 * The resource type repository.
120 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
121 * The module handler.
122 */
123 public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ResourceTypeRepositoryInterface $resource_type_repository, ModuleHandlerInterface $module_handler) {
124 $this->entityTypeManager = $entity_type_manager;
125 $this->fieldManager = $field_manager;
126 $this->entityTypeBundleInfo = $entity_type_bundle_info;
127 $this->resourceTypeRepository = $resource_type_repository;
128 $this->moduleHandler = $module_handler;
129 }
130
131 /**
132 * Validates and resolves an include path into its internal possibilities.
133 *
134 * Each resource type may define its own external names for its internal
135 * field names. As a result, a single external include path may target
136 * multiple internal paths.
137 *
138 * This can happen when an entity reference field has different allowed entity
139 * types *per bundle* (as is possible with comment entities) or when
140 * different resource types share an external field name but resolve to
141 * different internal fields names.
142 *
143 * Example 1:
144 * An installation may have three comment types for three different entity
145 * types, two of which have a file field and one of which does not. In that
146 * case, a path like @code field_comments.entity_id.media @endcode might be
147 * resolved to both @code field_comments.entity_id.field_audio @endcode
148 * and @code field_comments.entity_id.field_image @endcode.
149 *
150 * Example 2:
151 * A path of @code field_author_profile.account @endcode might
152 * resolve to @code field_author_profile.uid @endcode and @code
153 * field_author_profile.field_user @endcode if @code
154 * field_author_profile @endcode can relate to two different JSON:API resource
155 * types (like `node--profile` and `node--migrated_profile`) which have the
156 * external field name @code account @endcode aliased to different internal
157 * field names.
158 *
159 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
160 * The resource type for which the path should be validated.
161 * @param string[] $path_parts
162 * The include path as an array of strings. For example, the include query
163 * parameter string of @code field_tags.uid @endcode should be given
164 * as @code ['field_tags', 'uid'] @endcode.
165 * @param int $depth
166 * (internal) Used to track recursion depth in order to generate better
167 * exception messages.
168 *
169 * @return string[]
170 * The resolved internal include paths.
171 *
172 * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
173 * Thrown if the path contains invalid specifiers.
174 */
175 public static function resolveInternalIncludePath(ResourceType $resource_type, array $path_parts, $depth = 0) {
176 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:include']);
177 if (empty($path_parts[0])) {
178 throw new CacheableBadRequestHttpException($cacheability, 'Empty include path.');
179 }
180 $public_field_name = $path_parts[0];
181 $internal_field_name = $resource_type->getInternalName($public_field_name);
182 $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($public_field_name);
183 if (empty($relatable_resource_types)) {
184 $message = "`$public_field_name` is not a valid relationship field name.";
185 if (!empty(($possible = implode(', ', array_keys($resource_type->getRelatableResourceTypes()))))) {
186 $message .= " Possible values: $possible.";
187 }
188 throw new CacheableBadRequestHttpException($cacheability, $message);
189 }
190 $remaining_parts = array_slice($path_parts, 1);
191 if (empty($remaining_parts)) {
192 return [[$internal_field_name]];
193 }
194 $exceptions = [];
195 $resolved = [];
196 foreach ($relatable_resource_types as $relatable_resource_type) {
197 try {
198 // Each resource type may resolve the path differently and may return
199 // multiple possible resolutions.
200 $resolved = array_merge($resolved, static::resolveInternalIncludePath($relatable_resource_type, $remaining_parts, $depth + 1));
201 }
202 catch (CacheableBadRequestHttpException $e) {
203 $exceptions[] = $e;
204 }
205 }
206 if (!empty($exceptions) && count($exceptions) === count($relatable_resource_types)) {
207 $previous_messages = implode(' ', array_unique(array_map(function (CacheableBadRequestHttpException $e) {
208 return $e->getMessage();
209 }, $exceptions)));
210 // Only add the full include path on the first level of recursion so that
211 // the invalid path phrase isn't repeated at every level.
212 throw new CacheableBadRequestHttpException($cacheability, $depth === 0
213 ? sprintf("`%s` is not a valid include path. $previous_messages", implode('.', $path_parts))
214 : $previous_messages
215 );
216 }
217 // Remove duplicates by converting to strings and then using array_unique().
218 $resolved_as_strings = array_map(function ($possibility) {
219 return implode('.', $possibility);
220 }, $resolved);
221 $resolved_as_strings = array_unique($resolved_as_strings);
222
223 // The resolved internal paths do not include the current field name because
224 // resolution happens in a recursive process. Convert back from strings.
225 return array_map(function ($possibility) use ($internal_field_name) {
226 return array_merge([$internal_field_name], explode('.', $possibility));
227 }, $resolved_as_strings);
228 }
229
230 /**
231 * Resolves external field expressions into entity query compatible paths.
232 *
233 * It is often required to reference data which may exist across a
234 * relationship. For example, you may want to sort a list of articles by
235 * a field on the article author's representative entity. Or you may wish
236 * to filter a list of content by the name of referenced taxonomy terms.
237 *
238 * In an effort to simplify the referenced paths and align them with the
239 * structure of JSON:API responses and the structure of the hypothetical
240 * "reference document" (see link), it is possible to alias field names and
241 * elide the "entity" keyword from them (this word is used by the entity query
242 * system to traverse entity references).
243 *
244 * This method takes this external field expression and and attempts to
245 * resolve any aliases and/or abbreviations into a field expression that will
246 * be compatible with the entity query system.
247 *
248 * @link http://jsonapi.org/recommendations/#urls-reference-document
249 *
250 * Example:
251 * 'uid.field_first_name' -> 'uid.entity.field_first_name'.
252 * 'author.firstName' -> 'field_author.entity.field_first_name'
253 *
254 * @param string $entity_type_id
255 * The type of the entity for which to resolve the field name.
256 * @param string $bundle
257 * The bundle of the entity for which to resolve the field name.
258 * @param string $external_field_name
259 * The public field name to map to a Drupal field name.
260 *
261 * @return string
262 * The mapped field name.
263 *
264 * @throws \Drupal\Core\Http\Exception\CacheableBadRequestHttpException
265 */
266 public function resolveInternalEntityQueryPath($entity_type_id, $bundle, $external_field_name) {
267 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']);
268 $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
269 if (empty($external_field_name)) {
270 throw new CacheableBadRequestHttpException($cacheability, 'No field name was provided for the filter.');
271 }
272
273 // Turns 'uid.categories.name' into
274 // 'uid.entity.field_category.entity.name'. This may be too simple, but it
275 // works for the time being.
276 $parts = explode('.', $external_field_name);
277 $unresolved_path_parts = $parts;
278 $reference_breadcrumbs = [];
279 /* @var \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types */
280 $resource_types = [$resource_type];
281 // This complex expression is needed to handle the string, "0", which would
282 // otherwise be evaluated as FALSE.
283 while (!is_null(($part = array_shift($parts)))) {
284 if (!$this->isMemberFilterable($part, $resource_types)) {
285 throw new CacheableBadRequestHttpException($cacheability, sprintf(
286 'Invalid nested filtering. The field `%s`, given in the path `%s`, does not exist.',
287 $part,
288 $external_field_name
289 ));
290 }
291
292 $field_name = $this->getInternalName($part, $resource_types);
293
294 // If none of the resource types are traversable, assume that the
295 // remaining path parts are targeting field deltas and/or field
296 // properties.
297 if (!$this->resourceTypesAreTraversable($resource_types)) {
298 $reference_breadcrumbs[] = $field_name;
299 return $this->constructInternalPath($reference_breadcrumbs, $parts);
300 }
301
302 // Different resource types have different field definitions.
303 $candidate_definitions = $this->getFieldItemDefinitions($resource_types, $field_name);
304 assert(!empty($candidate_definitions));
305
306 // We have a valid field, so add it to the validated trail of path parts.
307 $reference_breadcrumbs[] = $field_name;
308
309 // Remove resource types which do not have a candidate definition.
310 $resource_types = array_filter($resource_types, function (ResourceType $resource_type) use ($candidate_definitions) {
311 return isset($candidate_definitions[$resource_type->getTypeName()]);
312 });
313
314 // Check access to execute a query for each field per resource type since
315 // field definitions are bundle-specific.
316 foreach ($resource_types as $resource_type) {
317 $field_access = $this->getFieldAccess($resource_type, $field_name);
318 $cacheability->addCacheableDependency($field_access);
319 if (!$field_access->isAllowed()) {
320 $message = sprintf('The current user is not authorized to filter by the `%s` field, given in the path `%s`.', $field_name, implode('.', $reference_breadcrumbs));
321 if ($field_access instanceof AccessResultReasonInterface && ($reason = $field_access->getReason()) && !empty($reason)) {
322 $message .= ' ' . $reason;
323 }
324 throw new CacheableAccessDeniedHttpException($cacheability, $message);
325 }
326 }
327
328 // Get all of the referenceable resource types.
329 $resource_types = $this->getReferenceableResourceTypes($candidate_definitions);
330
331 $at_least_one_entity_reference_field = FALSE;
332 $candidate_property_names = array_unique(NestedArray::mergeDeepArray(array_map(function (FieldItemDataDefinitionInterface $definition) use (&$at_least_one_entity_reference_field) {
333 $property_definitions = $definition->getPropertyDefinitions();
334 return array_reduce(array_keys($property_definitions), function ($property_names, $property_name) use ($property_definitions, &$at_least_one_entity_reference_field) {
335 $property_definition = $property_definitions[$property_name];
336 $is_data_reference_definition = $property_definition instanceof DataReferenceTargetDefinition;
337 if (!$property_definition->isInternal()) {
338 // Entity reference fields are special: their reference property
339 // (usually `target_id`) is never exposed in the JSON:API
340 // representation. Hence it must also not be exposed in 400
341 // responses' error messages.
342 $property_names[] = $is_data_reference_definition ? 'id' : $property_name;
343 }
344 if ($is_data_reference_definition) {
345 $at_least_one_entity_reference_field = TRUE;
346 }
347 return $property_names;
348 }, []);
349 }, $candidate_definitions)));
350
351 // Determine if the specified field has one property or many in its
352 // JSON:API representation, or if it is an relationship (an entity
353 // reference field), in which case the `id` of the related resource must
354 // always be specified.
355 $property_specifier_needed = $at_least_one_entity_reference_field || count($candidate_property_names) > 1;
356
357 // If there are no remaining path parts, the process is finished unless
358 // the field has multiple properties, in which case one must be specified.
359 if (empty($parts)) {
360 if ($property_specifier_needed) {
361 $possible_specifiers = array_map(function ($specifier) use ($at_least_one_entity_reference_field) {
362 return $at_least_one_entity_reference_field && $specifier !== 'id' ? "meta.$specifier" : $specifier;
363 }, $candidate_property_names);
364 throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The field `%s`, given in the path `%s` is incomplete, it must end with one of the following specifiers: `%s`.', $part, $external_field_name, implode('`, `', $possible_specifiers)));
365 }
366 return $this->constructInternalPath($reference_breadcrumbs);
367 }
368
369 // If the next part is a delta, as in "body.0.value", then we add it to
370 // the breadcrumbs and remove it from the parts that still must be
371 // processed.
372 if (static::isDelta($parts[0])) {
373 $reference_breadcrumbs[] = array_shift($parts);
374 }
375
376 // If there are no remaining path parts, the process is finished.
377 if (empty($parts)) {
378 return $this->constructInternalPath($reference_breadcrumbs);
379 }
380
381 // JSON:API outputs entity reference field properties under a meta object
382 // on a relationship. If the filter specifies one of these properties, it
383 // must prefix the property name with `meta`. The only exception is if the
384 // next path part is the same as the name for the reference property
385 // (typically `entity`), this is permitted to disambiguate the case of a
386 // field name on the target entity which is the same a property name on
387 // the entity reference field.
388 if ($at_least_one_entity_reference_field && $parts[0] !== 'id') {
389 if ($parts[0] === 'meta') {
390 array_shift($parts);
391 }
392 elseif (in_array($parts[0], $candidate_property_names) && !static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
393 throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s` belongs to the meta object of a relationship and must be preceded by `meta`.', $parts[0], $external_field_name));
394 }
395 }
396
397 // Determine if the next part is not a property of $field_name.
398 if (!static::isCandidateDefinitionProperty($parts[0], $candidate_definitions) && !empty(static::getAllDataReferencePropertyNames($candidate_definitions))) {
399 // The next path part is neither a delta nor a field property, so it
400 // must be a field on a targeted resource type. We need to guess the
401 // intermediate reference property since one was not provided.
402 //
403 // For example, the path `uid.name` for a `node--article` resource type
404 // will be resolved into `uid.entity.name`.
405 $reference_breadcrumbs[] = static::getDataReferencePropertyName($candidate_definitions, $parts, $unresolved_path_parts);
406 }
407 else {
408 // If the property is not a reference property, then all
409 // remaining parts must be further property specifiers.
410 if (!static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
411 // If a field property is specified on a field with only one property
412 // defined, throw an error because in the JSON:API output, it does not
413 // exist. This is because JSON:API elides single-value properties;
414 // respecting it would leak this Drupalism out.
415 if (count($candidate_property_names) === 1) {
416 throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Filter by `%s`, not `%s` (the JSON:API module elides property names from single-property fields).', $parts[0], $external_field_name, substr($external_field_name, 0, strlen($external_field_name) - strlen($parts[0]) - 1), $external_field_name));
417 }
418 elseif (!in_array($parts[0], $candidate_property_names, TRUE)) {
419 throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Must be one of the following property names: `%s`.', $parts[0], $external_field_name, implode('`, `', $candidate_property_names)));
420 }
421 return $this->constructInternalPath($reference_breadcrumbs, $parts);
422 }
423 // The property is a reference, so add it to the breadcrumbs and
424 // continue resolving fields.
425 $reference_breadcrumbs[] = array_shift($parts);
426 }
427 }
428
429 // Reconstruct the full path to the final reference field.
430 return $this->constructInternalPath($reference_breadcrumbs);
431 }
432
433 /**
434 * Expands the internal path with the "entity" keyword.
435 *
436 * @param string[] $references
437 * The resolved internal field names of all entity references.
438 * @param string[] $property_path
439 * (optional) A sub-property path for the last field in the path.
440 *
441 * @return string
442 * The expanded and imploded path.
443 */
444 protected function constructInternalPath(array $references, array $property_path = []) {
445 // Reconstruct the path parts that are referencing sub-properties.
446 $field_path = implode('.', $property_path);
447
448 // This rebuilds the path from the real, internal field names that have
449 // been traversed so far. It joins them with the "entity" keyword as
450 // required by the entity query system.
451 $entity_path = implode('.', $references);
452
453 // Reconstruct the full path to the final reference field.
454 return (empty($field_path)) ? $entity_path : $entity_path . '.' . $field_path;
455 }
456
457 /**
458 * Get all item definitions from a set of resources types by a field name.
459 *
460 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
461 * The resource types on which the field might exist.
462 * @param string $field_name
463 * The field for which to retrieve field item definitions.
464 *
465 * @return \Drupal\Core\TypedData\ComplexDataDefinitionInterface[]
466 * The found field item definitions.
467 */
468 protected function getFieldItemDefinitions(array $resource_types, $field_name) {
469 return array_reduce($resource_types, function ($result, ResourceType $resource_type) use ($field_name) {
470 /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
471 $entity_type = $resource_type->getEntityTypeId();
472 $bundle = $resource_type->getBundle();
473 $definitions = $this->fieldManager->getFieldDefinitions($entity_type, $bundle);
474 if (isset($definitions[$field_name])) {
475 $result[$resource_type->getTypeName()] = $definitions[$field_name]->getItemDefinition();
476 }
477 return $result;
478 }, []);
479 }
480
481 /**
482 * Resolves the UUID field name for a resource type.
483 *
484 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
485 * The resource type for which to get the UUID field name.
486 *
487 * @return string
488 * The resolved internal name.
489 */
490 protected function getIdFieldName(ResourceType $resource_type) {
491 $entity_type = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId());
492 return $entity_type->getKey('uuid');
493 }
494
495 /**
496 * Resolves the internal field name based on a collection of resource types.
497 *
498 * @param string $field_name
499 * The external field name.
500 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
501 * The resource types from which to get an internal name.
502 *
503 * @return string
504 * The resolved internal name.
505 */
506 protected function getInternalName($field_name, array $resource_types) {
507 return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($field_name) {
508 if ($carry != $field_name) {
509 // We already found the internal name.
510 return $carry;
511 }
512 return $field_name === 'id' ? $this->getIdFieldName($resource_type) : $resource_type->getInternalName($field_name);
513 }, $field_name);
514 }
515
516 /**
517 * Determines if the given field or member name is filterable.
518 *
519 * @param string $external_name
520 * The external field or member name.
521 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
522 * The resource types to test.
523 *
524 * @return bool
525 * Whether the given field is present as a filterable member of the targeted
526 * resource objects.
527 */
528 protected function isMemberFilterable($external_name, array $resource_types) {
529 return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($external_name) {
530 // @todo: remove the next line and uncomment the following one in https://www.drupal.org/project/jsonapi/issues/3017047.
531 return $carry ?: $external_name === 'id' || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name));
532 /*return $carry ?: in_array($external_name, ['id', 'type']) || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name));*/
533 }, FALSE);
534 }
535
536 /**
537 * Get the referenceable ResourceTypes for a set of field definitions.
538 *
539 * @param \Drupal\Core\Field\FieldDefinitionInterface[] $definitions
540 * The resource types on which the reference field might exist.
541 *
542 * @return \Drupal\jsonapi\ResourceType\ResourceType[]
543 * The referenceable target resource types.
544 */
545 protected function getReferenceableResourceTypes(array $definitions) {
546 return array_reduce($definitions, function ($result, $definition) {
547 $resource_types = array_filter(
548 $this->collectResourceTypesForReference($definition)
549 );
550 $type_names = array_map(function ($resource_type) {
551 /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
552 return $resource_type->getTypeName();
553 }, $resource_types);
554 return array_merge($result, array_combine($type_names, $resource_types));
555 }, []);
556 }
557
558 /**
559 * Build a list of resource types depending on which bundles are referenced.
560 *
561 * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $item_definition
562 * The reference definition.
563 *
564 * @return \Drupal\jsonapi\ResourceType\ResourceType[]
565 * The list of resource types.
566 */
567 protected function collectResourceTypesForReference(FieldItemDataDefinitionInterface $item_definition) {
568 $main_property_definition = $item_definition->getPropertyDefinition(
569 $item_definition->getMainPropertyName()
570 );
571
572 // Check if the field is a flavor of an Entity Reference field.
573 if (!$main_property_definition instanceof DataReferenceTargetDefinition) {
574 return [];
575 }
576 $entity_type_id = $item_definition->getSetting('target_type');
577 $handler_settings = $item_definition->getSetting('handler_settings');
578
579 $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']);
580 $target_bundles = $has_target_bundles ?
581 $handler_settings['target_bundles']
582 : $this->getAllBundlesForEntityType($entity_type_id);
583
584 return array_map(function ($bundle) use ($entity_type_id) {
585 return $this->resourceTypeRepository->get($entity_type_id, $bundle);
586 }, $target_bundles);
587 }
588
589 /**
590 * Whether the given resources can be traversed to other resources.
591 *
592 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
593 * The resources types to evaluate.
594 *
595 * @return bool
596 * TRUE if any one of the given resource types is traversable.
597 *
598 * @todo This class shouldn't be aware of entity types and their definitions.
599 * Whether a resource can have relationships to other resources is information
600 * we ought to be able to discover on the ResourceType. However, we cannot
601 * reliably determine this information with existing APIs. Entities may be
602 * backed by various storages that are unable to perform queries across
603 * references and certain storages may not be able to store references at all.
604 */
605 protected function resourceTypesAreTraversable(array $resource_types) {
606 foreach ($resource_types as $resource_type) {
607 $entity_type_definition = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId());
608 if ($entity_type_definition->entityClassImplements(FieldableEntityInterface::class)) {
609 return TRUE;
610 }
611 }
612 return FALSE;
613 }
614
615 /**
616 * Gets all bundle IDs for a given entity type.
617 *
618 * @param string $entity_type_id
619 * The entity type for which to get bundles.
620 *
621 * @return string[]
622 * The bundle IDs.
623 */
624 protected function getAllBundlesForEntityType($entity_type_id) {
625 return array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
626 }
627
628 /**
629 * Gets all unique reference property names from the given field definitions.
630 *
631 * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
632 * A list of targeted field item definitions specified by the path.
633 *
634 * @return string[]
635 * The reference property names, if any.
636 */
637 protected static function getAllDataReferencePropertyNames(array $candidate_definitions) {
638 $reference_property_names = array_reduce($candidate_definitions, function (array $reference_property_names, ComplexDataDefinitionInterface $definition) {
639 $property_definitions = $definition->getPropertyDefinitions();
640 foreach ($property_definitions as $property_name => $property_definition) {
641 if ($property_definition instanceof DataReferenceDefinitionInterface) {
642 $target_definition = $property_definition->getTargetDefinition();
643 assert($target_definition instanceof EntityDataDefinitionInterface, 'Entity reference fields should only be able to reference entities.');
644 $reference_property_names[] = $property_name . ':' . $target_definition->getEntityTypeId();
645 }
646 }
647 return $reference_property_names;
648 }, []);
649 return array_unique($reference_property_names);
650 }
651
652 /**
653 * Determines the reference property name for the remaining unresolved parts.
654 *
655 * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
656 * A list of targeted field item definitions specified by the path.
657 * @param string[] $remaining_parts
658 * The remaining path parts.
659 * @param string[] $unresolved_path_parts
660 * The unresolved path parts.
661 *
662 * @return string
663 * The reference name.
664 */
665 protected static function getDataReferencePropertyName(array $candidate_definitions, array $remaining_parts, array $unresolved_path_parts) {
666 $unique_reference_names = static::getAllDataReferencePropertyNames($candidate_definitions);
667 if (count($unique_reference_names) > 1) {
668 $choices = array_map(function ($reference_name) use ($unresolved_path_parts, $remaining_parts) {
669 $prior_parts = array_slice($unresolved_path_parts, 0, count($unresolved_path_parts) - count($remaining_parts));
670 return implode('.', array_merge($prior_parts, [$reference_name], $remaining_parts));
671 }, $unique_reference_names);
672 // @todo Add test coverage for this in https://www.drupal.org/project/jsonapi/issues/2971281
673 $message = sprintf('Ambiguous path. Try one of the following: %s, in place of the given path: %s', implode(', ', $choices), implode('.', $unresolved_path_parts));
674 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']);
675 throw new CacheableBadRequestHttpException($cacheability, $message);
676 }
677 return $unique_reference_names[0];
678 }
679
680 /**
681 * Determines if a path part targets a specific field delta.
682 *
683 * @param string $part
684 * The path part.
685 *
686 * @return bool
687 * TRUE if the part is an integer, FALSE otherwise.
688 */
689 protected static function isDelta($part) {
690 return (bool) preg_match('/^[0-9]+$/', $part);
691 }
692
693 /**
694 * Determines if a path part targets a field property, not a subsequent field.
695 *
696 * @param string $part
697 * The path part.
698 * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
699 * A list of targeted field item definitions which are specified by the
700 * path.
701 *
702 * @return bool
703 * TRUE if the part is a property of one of the candidate definitions, FALSE
704 * otherwise.
705 */
706 protected static function isCandidateDefinitionProperty($part, array $candidate_definitions) {
707 $part = static::getPathPartPropertyName($part);
708 foreach ($candidate_definitions as $definition) {
709 if ($definition->getPropertyDefinition($part)) {
710 return TRUE;
711 }
712 }
713 return FALSE;
714 }
715
716 /**
717 * Determines if a path part targets a reference property.
718 *
719 * @param string $part
720 * The path part.
721 * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
722 * A list of targeted field item definitions which are specified by the
723 * path.
724 *
725 * @return bool
726 * TRUE if the part is a property of one of the candidate definitions, FALSE
727 * otherwise.
728 */
729 protected static function isCandidateDefinitionReferenceProperty($part, array $candidate_definitions) {
730 $part = static::getPathPartPropertyName($part);
731 foreach ($candidate_definitions as $definition) {
732 $property = $definition->getPropertyDefinition($part);
733 if ($property && $property instanceof DataReferenceDefinitionInterface) {
734 return TRUE;
735 }
736 }
737 return FALSE;
738 }
739
740 /**
741 * Gets the property name from an entity typed or untyped path part.
742 *
743 * A path part may contain an entity type specifier like `entity:node`. This
744 * extracts the actual property name. If an entity type is not specified, then
745 * the path part is simply returned. For example, both `foo` and `foo:bar`
746 * will return `foo`.
747 *
748 * @param string $part
749 * A path part.
750 *
751 * @return string
752 * The property name from a path part.
753 */
754 protected static function getPathPartPropertyName($part) {
755 return strpos($part, ':') !== FALSE ? explode(':', $part)[0] : $part;
756 }
757
758 /**
759 * Gets the field access result for the 'view' operation.
760 *
761 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
762 * The JSON:API resource type on which the field exists.
763 * @param string $internal_field_name
764 * The field name for which access should be checked.
765 *
766 * @return \Drupal\Core\Access\AccessResultInterface
767 * The 'view' access result.
768 */
769 protected function getFieldAccess(ResourceType $resource_type, $internal_field_name) {
770 $definitions = $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle());
771 assert(isset($definitions[$internal_field_name]), 'The field name should have already been validated.');
772 $field_definition = $definitions[$internal_field_name];
773 $filter_access_results = $this->moduleHandler->invokeAll('jsonapi_entity_field_filter_access', [$field_definition, \Drupal::currentUser()]);
774 $filter_access_result = array_reduce($filter_access_results, function (AccessResultInterface $combined_result, AccessResultInterface $result) {
775 return $combined_result->orIf($result);
776 }, AccessResult::neutral());
777 if (!$filter_access_result->isNeutral()) {
778 return $filter_access_result;
779 }
780 $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($resource_type->getEntityTypeId());
781 $field_access = $entity_access_control_handler->fieldAccess('view', $field_definition, NULL, NULL, TRUE);
782 return $filter_access_result->orIf($field_access);
783 }
784
785 }