Mercurial > hg > cmmr2012-drupal-site
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 } |