Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.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\JsonApiResource; | |
4 | |
5 use Drupal\Core\Entity\EntityInterface; | |
6 use Drupal\Core\Field\EntityReferenceFieldItemListInterface; | |
7 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; | |
8 use Drupal\Core\TypedData\DataReferenceDefinitionInterface; | |
9 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper; | |
10 use Drupal\jsonapi\ResourceType\ResourceType; | |
11 | |
12 /** | |
13 * Represents a JSON:API resource identifier object. | |
14 * | |
15 * The official JSON:API JSON-Schema document requires that no two resource | |
16 * identifier objects are duplicates, however Drupal allows multiple entity | |
17 * reference items to the same entity. Here, these are termed "parallel" | |
18 * relationships (as in "parallel edges" of a graph). | |
19 * | |
20 * This class adds a concept of an @code arity @endcode member under each its | |
21 * @code meta @endcode object. The value of this member is an integer that is | |
22 * incremented by 1 (starting from 0) for each repeated resource identifier | |
23 * sharing a common @code type @endcode and @code id @endcode. | |
24 * | |
25 * There are a number of helper methods to process the logic of dealing with | |
26 * resource identifies with and without arity. | |
27 * | |
28 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class | |
29 * may change at any time and could break any dependencies on it. | |
30 * | |
31 * @see https://www.drupal.org/project/jsonapi/issues/3032787 | |
32 * @see jsonapi.api.php | |
33 * | |
34 * @see http://jsonapi.org/format/#document-resource-object-relationships | |
35 * @see https://github.com/json-api/json-api/pull/1156#issuecomment-325377995 | |
36 * @see https://www.drupal.org/project/jsonapi/issues/2864680 | |
37 */ | |
38 class ResourceIdentifier implements ResourceIdentifierInterface { | |
39 | |
40 const ARITY_KEY = 'arity'; | |
41 | |
42 /** | |
43 * The JSON:API resource type name. | |
44 * | |
45 * @var string | |
46 */ | |
47 protected $resourceTypeName; | |
48 | |
49 /** | |
50 * The JSON:API resource type. | |
51 * | |
52 * @var \Drupal\jsonapi\ResourceType\ResourceType | |
53 */ | |
54 protected $resourceType; | |
55 | |
56 /** | |
57 * The resource ID. | |
58 * | |
59 * @var string | |
60 */ | |
61 protected $id; | |
62 | |
63 /** | |
64 * The relationship's metadata. | |
65 * | |
66 * @var array | |
67 */ | |
68 protected $meta; | |
69 | |
70 /** | |
71 * ResourceIdentifier constructor. | |
72 * | |
73 * @param \Drupal\jsonapi\ResourceType\ResourceType|string $resource_type | |
74 * The JSON:API resource type or a JSON:API resource type name. | |
75 * @param string $id | |
76 * The resource ID. | |
77 * @param array $meta | |
78 * Any metadata for the ResourceIdentifier. | |
79 */ | |
80 public function __construct($resource_type, $id, array $meta = []) { | |
81 assert(is_string($resource_type) || $resource_type instanceof ResourceType); | |
82 assert(!isset($meta[static::ARITY_KEY]) || is_int($meta[static::ARITY_KEY]) && $meta[static::ARITY_KEY] >= 0); | |
83 $this->resourceTypeName = is_string($resource_type) ? $resource_type : $resource_type->getTypeName(); | |
84 $this->id = $id; | |
85 $this->meta = $meta; | |
86 if (!is_string($resource_type)) { | |
87 $this->resourceType = $resource_type; | |
88 } | |
89 } | |
90 | |
91 /** | |
92 * Gets the ResourceIdentifier's JSON:API resource type name. | |
93 * | |
94 * @return string | |
95 * The JSON:API resource type name. | |
96 */ | |
97 public function getTypeName() { | |
98 return $this->resourceTypeName; | |
99 } | |
100 | |
101 /** | |
102 * {@inheritdoc} | |
103 */ | |
104 public function getResourceType() { | |
105 if (!isset($this->resourceType)) { | |
106 /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ | |
107 $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); | |
108 $this->resourceType = $resource_type_repository->getByTypeName($this->getTypeName()); | |
109 } | |
110 return $this->resourceType; | |
111 } | |
112 | |
113 /** | |
114 * Gets the ResourceIdentifier's ID. | |
115 * | |
116 * @return string | |
117 * The ID. | |
118 */ | |
119 public function getId() { | |
120 return $this->id; | |
121 } | |
122 | |
123 /** | |
124 * Whether this ResourceIdentifier has an arity. | |
125 * | |
126 * @return int | |
127 * TRUE if the ResourceIdentifier has an arity, FALSE otherwise. | |
128 */ | |
129 public function hasArity() { | |
130 return isset($this->meta[static::ARITY_KEY]); | |
131 } | |
132 | |
133 /** | |
134 * Gets the ResourceIdentifier's arity. | |
135 * | |
136 * One must check self::hasArity() before calling this method. | |
137 * | |
138 * @return int | |
139 * The arity. | |
140 */ | |
141 public function getArity() { | |
142 assert($this->hasArity()); | |
143 return $this->meta[static::ARITY_KEY]; | |
144 } | |
145 | |
146 /** | |
147 * Returns a copy of the given ResourceIdentifier with the given arity. | |
148 * | |
149 * @param int $arity | |
150 * The new arity; must be a non-negative integer. | |
151 * | |
152 * @return static | |
153 * A newly created ResourceIdentifier with the given arity, otherwise | |
154 * the same. | |
155 */ | |
156 public function withArity($arity) { | |
157 return new static($this->getResourceType(), $this->getId(), [static::ARITY_KEY => $arity] + $this->getMeta()); | |
158 } | |
159 | |
160 /** | |
161 * Gets the resource identifier objects metadata. | |
162 * | |
163 * @return array | |
164 * The metadata. | |
165 */ | |
166 public function getMeta() { | |
167 return $this->meta; | |
168 } | |
169 | |
170 /** | |
171 * Determines if two ResourceIdentifiers are the same. | |
172 * | |
173 * This method does not consider parallel relationships with different arity | |
174 * values to be duplicates. For that, use the isParallel() method. | |
175 * | |
176 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a | |
177 * The first ResourceIdentifier object. | |
178 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b | |
179 * The second ResourceIdentifier object. | |
180 * | |
181 * @return bool | |
182 * TRUE if both relationships reference the same resource and do not have | |
183 * two distinct arity's, FALSE otherwise. | |
184 * | |
185 * For example, if $a and $b both reference the same resource identifier, | |
186 * they can only be distinct if they *both* have an arity and those values | |
187 * are not the same. If $a or $b does not have an arity, they will be | |
188 * considered duplicates. | |
189 */ | |
190 public static function isDuplicate(ResourceIdentifier $a, ResourceIdentifier $b) { | |
191 return static::compare($a, $b) === 0; | |
192 } | |
193 | |
194 /** | |
195 * Determines if two ResourceIdentifiers identify the same resource object. | |
196 * | |
197 * This method does not consider arity. | |
198 * | |
199 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a | |
200 * The first ResourceIdentifier object. | |
201 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b | |
202 * The second ResourceIdentifier object. | |
203 * | |
204 * @return bool | |
205 * TRUE if both relationships reference the same resource, even when they | |
206 * have differing arity values, FALSE otherwise. | |
207 */ | |
208 public static function isParallel(ResourceIdentifier $a, ResourceIdentifier $b) { | |
209 return static::compare($a->withArity(0), $b->withArity(0)) === 0; | |
210 } | |
211 | |
212 /** | |
213 * Compares ResourceIdentifier objects. | |
214 * | |
215 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a | |
216 * The first ResourceIdentifier object. | |
217 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b | |
218 * The second ResourceIdentifier object. | |
219 * | |
220 * @return int | |
221 * Returns 0 if $a and $b are duplicate ResourceIdentifiers. If $a and $b | |
222 * identify the same resource but have distinct arity values, then the | |
223 * return value will be arity $a minus arity $b. -1 otherwise. | |
224 */ | |
225 public static function compare(ResourceIdentifier $a, ResourceIdentifier $b) { | |
226 $result = strcmp(sprintf('%s:%s', $a->getTypeName(), $a->getId()), sprintf('%s:%s', $b->getTypeName(), $b->getId())); | |
227 // If type and ID do not match, return their ordering. | |
228 if ($result !== 0) { | |
229 return $result; | |
230 } | |
231 // If both $a and $b have an arity, then return the order by arity. | |
232 // Otherwise, they are considered equal. | |
233 return $a->hasArity() && $b->hasArity() | |
234 ? $a->getArity() - $b->getArity() | |
235 : 0; | |
236 } | |
237 | |
238 /** | |
239 * Deduplicates an array of ResourceIdentifier objects. | |
240 * | |
241 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers | |
242 * The list of ResourceIdentifiers to deduplicate. | |
243 * | |
244 * @return \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] | |
245 * A deduplicated array of ResourceIdentifier objects. | |
246 * | |
247 * @see self::isDuplicate() | |
248 */ | |
249 public static function deduplicate(array $resource_identifiers) { | |
250 return array_reduce(array_slice($resource_identifiers, 1), function ($deduplicated, $current) { | |
251 assert($current instanceof static); | |
252 return array_merge($deduplicated, array_reduce($deduplicated, function ($duplicate, $previous) use ($current) { | |
253 return $duplicate ?: static::isDuplicate($previous, $current); | |
254 }, FALSE) ? [] : [$current]); | |
255 }, array_slice($resource_identifiers, 0, 1)); | |
256 } | |
257 | |
258 /** | |
259 * Determines if an array of ResourceIdentifier objects is duplicate free. | |
260 * | |
261 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers | |
262 * The list of ResourceIdentifiers to assess. | |
263 * | |
264 * @return bool | |
265 * Whether all the given resource identifiers are unique. | |
266 */ | |
267 public static function areResourceIdentifiersUnique(array $resource_identifiers) { | |
268 return count($resource_identifiers) === count(static::deduplicate($resource_identifiers)); | |
269 } | |
270 | |
271 /** | |
272 * Creates a ResourceIdentifier object. | |
273 * | |
274 * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item | |
275 * The entity reference field item from which to create the relationship. | |
276 * @param int $arity | |
277 * (optional) The arity of the relationship. | |
278 * | |
279 * @return self | |
280 * A new ResourceIdentifier object. | |
281 */ | |
282 public static function toResourceIdentifier(EntityReferenceItem $item, $arity = NULL) { | |
283 $property_name = static::getDataReferencePropertyName($item); | |
284 $target = $item->get($property_name)->getValue(); | |
285 if ($target === NULL) { | |
286 return static::getVirtualOrMissingResourceIdentifier($item); | |
287 } | |
288 assert($target instanceof EntityInterface); | |
289 /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ | |
290 $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); | |
291 $resource_type = $resource_type_repository->get($target->getEntityTypeId(), $target->bundle()); | |
292 // Remove unwanted properties from the meta value, usually 'entity' | |
293 // and 'target_id'. | |
294 $properties = TypedDataInternalPropertiesHelper::getNonInternalProperties($item); | |
295 $meta = array_diff_key($properties, array_flip([$property_name, $item->getDataDefinition()->getMainPropertyName()])); | |
296 if (!is_null($arity)) { | |
297 $meta[static::ARITY_KEY] = $arity; | |
298 } | |
299 return new static($resource_type, $target->uuid(), $meta); | |
300 } | |
301 | |
302 /** | |
303 * Creates an array of ResourceIdentifier objects. | |
304 * | |
305 * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items | |
306 * The entity reference field items from which to create the relationship | |
307 * array. | |
308 * | |
309 * @return self[] | |
310 * An array of new ResourceIdentifier objects with appropriate arity values. | |
311 */ | |
312 public static function toResourceIdentifiers(EntityReferenceFieldItemListInterface $items) { | |
313 $relationships = []; | |
314 foreach ($items as $item) { | |
315 // Create a ResourceIdentifier from the field item. This will make it | |
316 // comparable with all previous field items. Here, it is assumed that the | |
317 // resource identifier is unique so it has no arity. If a parallel | |
318 // relationship is encountered, it will be assigned later. | |
319 $relationship = static::toResourceIdentifier($item); | |
320 // Now, iterate over the previously seen resource identifiers in reverse | |
321 // order. Reverse order is important so that when a parallel relationship | |
322 // is encountered, it will have the highest arity value so the current | |
323 // relationship's arity value can simply be incremented by one. | |
324 /* @var self $existing */ | |
325 foreach (array_reverse($relationships, TRUE) as $index => $existing) { | |
326 $is_parallel = static::isParallel($existing, $relationship); | |
327 if ($is_parallel) { | |
328 // A parallel relationship has been found. If the previous | |
329 // relationship does not have an arity, it must now be assigned an | |
330 // arity of 0. | |
331 if (!$existing->hasArity()) { | |
332 $relationships[$index] = $existing->withArity(0); | |
333 } | |
334 // Since the new ResourceIdentifier is parallel, it must have an arity | |
335 // assigned to it that is the arity of the last parallel | |
336 // relationship's arity + 1. | |
337 $relationship = $relationship->withArity($relationships[$index]->getArity() + 1); | |
338 break; | |
339 } | |
340 } | |
341 // Finally, append the relationship to the list of ResourceIdentifiers. | |
342 $relationships[] = $relationship; | |
343 } | |
344 return $relationships; | |
345 } | |
346 | |
347 /** | |
348 * Creates an array of ResourceIdentifier objects with arity on every value. | |
349 * | |
350 * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items | |
351 * The entity reference field items from which to create the relationship | |
352 * array. | |
353 * | |
354 * @return self[] | |
355 * An array of new ResourceIdentifier objects with appropriate arity values. | |
356 * Unlike self::toResourceIdentifiers(), this method does not omit arity | |
357 * when an identifier is not parallel to any other identifier. | |
358 */ | |
359 public static function toResourceIdentifiersWithArityRequired(EntityReferenceFieldItemListInterface $items) { | |
360 return array_map(function (ResourceIdentifier $identifier) { | |
361 return $identifier->hasArity() ? $identifier : $identifier->withArity(0); | |
362 }, static::toResourceIdentifiers($items)); | |
363 } | |
364 | |
365 /** | |
366 * Creates a ResourceIdentifier object. | |
367 * | |
368 * @param \Drupal\Core\Entity\EntityInterface $entity | |
369 * The entity from which to create the resource identifier. | |
370 * | |
371 * @return self | |
372 * A new ResourceIdentifier object. | |
373 */ | |
374 public static function fromEntity(EntityInterface $entity) { | |
375 /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ | |
376 $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); | |
377 $resource_type = $resource_type_repository->get($entity->getEntityTypeId(), $entity->bundle()); | |
378 return new static($resource_type, $entity->uuid()); | |
379 } | |
380 | |
381 /** | |
382 * Helper method to determine which field item property contains an entity. | |
383 * | |
384 * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item | |
385 * The entity reference item for which to determine the entity property | |
386 * name. | |
387 * | |
388 * @return string | |
389 * The property name which has an entity as its value. | |
390 */ | |
391 protected static function getDataReferencePropertyName(EntityReferenceItem $item) { | |
392 foreach ($item->getDataDefinition()->getPropertyDefinitions() as $property_name => $property_definition) { | |
393 if ($property_definition instanceof DataReferenceDefinitionInterface) { | |
394 return $property_name; | |
395 } | |
396 } | |
397 } | |
398 | |
399 /** | |
400 * Creates a ResourceIdentifier for a NULL or FALSE entity reference item. | |
401 * | |
402 * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item | |
403 * The entity reference field item. | |
404 * | |
405 * @return self | |
406 * A new ResourceIdentifier object. | |
407 */ | |
408 protected static function getVirtualOrMissingResourceIdentifier(EntityReferenceItem $item) { | |
409 $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); | |
410 $property_name = static::getDataReferencePropertyName($item); | |
411 $value = $item->get($property_name)->getValue(); | |
412 assert($value === NULL); | |
413 $field = $item->getParent(); | |
414 assert($field instanceof EntityReferenceFieldItemListInterface); | |
415 $host_entity = $field->getEntity(); | |
416 assert($host_entity instanceof EntityInterface); | |
417 $resource_type = $resource_type_repository->get($host_entity->getEntityTypeId(), $host_entity->bundle()); | |
418 assert($resource_type instanceof ResourceType); | |
419 $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($field->getName()); | |
420 $get_metadata = function ($type) { | |
421 return [ | |
422 'links' => [ | |
423 'help' => [ | |
424 'href' => "https://www.drupal.org/docs/8/modules/json-api/core-concepts#$type", | |
425 'meta' => [ | |
426 'about' => "Usage and meaning of the '$type' resource identifier.", | |
427 ], | |
428 ], | |
429 ], | |
430 ]; | |
431 }; | |
432 $resource_type = reset($relatable_resource_types); | |
433 // A non-empty entity reference field that refers to a non-existent entity | |
434 // is not a data integrity problem. For example, Term entities' "parent" | |
435 // entity reference field uses target_id zero to refer to the non-existent | |
436 // "<root>" term. And references to entities that no longer exist are not | |
437 // cleaned up by Drupal; hence we map it to a "missing" resource. | |
438 if ($field->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term' && $item->get('target_id')->getCastedValue() === 0) { | |
439 if (count($relatable_resource_types) !== 1) { | |
440 throw new \RuntimeException('Relationships to virtual resources are possible only if a single resource type is relatable.'); | |
441 } | |
442 return new static($resource_type, 'virtual', $get_metadata('virtual')); | |
443 } | |
444 else { | |
445 // In case of a dangling reference, it is impossible to determine which | |
446 // resource type it used to reference, because that requires knowing the | |
447 // referenced bundle, which Drupal does not store. | |
448 // If we can reliably determine the resource type of the dangling | |
449 // reference, use it; otherwise conjure a fake resource type out of thin | |
450 // air, one that indicates we don't know the bundle. | |
451 $resource_type = count($relatable_resource_types) > 1 | |
452 ? new ResourceType('?', '?', '') | |
453 : reset($relatable_resource_types); | |
454 return new static($resource_type, 'missing', $get_metadata('missing')); | |
455 } | |
456 } | |
457 | |
458 } |