Chris@18: entityTypeManager = $entity_type_manager; Chris@18: $this->entityAccessChecker = $entity_access_checker; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Resolves included resources. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data Chris@18: * The resource(s) for which to resolve includes. Chris@18: * @param string $include_parameter Chris@18: * The include query parameter to resolve. Chris@18: * Chris@18: * @return \Drupal\jsonapi\JsonApiResource\IncludedData Chris@18: * An IncludedData object of resolved resources to be included. Chris@18: * Chris@18: * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException Chris@18: * Thrown if an included entity type doesn't exist. Chris@18: * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException Chris@18: * Thrown if a storage handler couldn't be loaded. Chris@18: */ Chris@18: public function resolve($data, $include_parameter) { Chris@18: assert($data instanceof ResourceObject || $data instanceof ResourceObjectData); Chris@18: $data = $data instanceof ResourceObjectData ? $data : new ResourceObjectData([$data], 1); Chris@18: $include_tree = static::toIncludeTree($data, $include_parameter); Chris@18: return IncludedData::deduplicate($this->resolveIncludeTree($include_tree, $data)); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Receives a tree of include field names and resolves resources for it. Chris@18: * Chris@18: * This method takes a tree of relationship field names and JSON:API Data Chris@18: * object. For the top-level of the tree and for each entity in the Chris@18: * collection, it gets the target entity type and IDs for each relationship Chris@18: * field. The method then loads all of those targets and calls itself Chris@18: * recursively with the next level of the tree and those loaded resources. Chris@18: * Chris@18: * @param array $include_tree Chris@18: * The include paths, represented as a tree. Chris@18: * @param \Drupal\jsonapi\JsonApiResource\Data $data Chris@18: * The entity collection from which includes should be resolved. Chris@18: * @param \Drupal\jsonapi\JsonApiResource\Data|null $includes Chris@18: * (Internal use only) Any prior resolved includes. Chris@18: * Chris@18: * @return \Drupal\jsonapi\JsonApiResource\Data Chris@18: * A JSON:API Data of included items. Chris@18: * Chris@18: * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException Chris@18: * Thrown if an included entity type doesn't exist. Chris@18: * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException Chris@18: * Thrown if a storage handler couldn't be loaded. Chris@18: */ Chris@18: protected function resolveIncludeTree(array $include_tree, Data $data, Data $includes = NULL) { Chris@18: $includes = is_null($includes) ? new IncludedData([]) : $includes; Chris@18: foreach ($include_tree as $field_name => $children) { Chris@18: $references = []; Chris@18: foreach ($data as $resource_object) { Chris@18: // Some objects in the collection may be LabelOnlyResourceObjects or Chris@18: // EntityAccessDeniedHttpException objects. Chris@18: assert($resource_object instanceof ResourceIdentifierInterface); Chris@18: if ($resource_object instanceof LabelOnlyResourceObject) { Chris@18: $message = "The current user is not allowed to view this relationship."; Chris@18: $exception = new EntityAccessDeniedHttpException($resource_object->getEntity(), AccessResult::forbidden("The user only has authorization for the 'view label' operation."), '', $message, $field_name); Chris@18: $includes = IncludedData::merge($includes, new IncludedData([$exception])); Chris@18: continue; Chris@18: } Chris@18: elseif (!$resource_object instanceof ResourceObject) { Chris@18: continue; Chris@18: } Chris@18: $public_field_name = $resource_object->getResourceType()->getPublicName($field_name); Chris@18: // Not all entities in $entity_collection will be of the same bundle and Chris@18: // may not have all of the same fields. Therefore, calling Chris@18: // $resource_object->get($a_missing_field_name) will result in an Chris@18: // exception. Chris@18: if (!$resource_object->hasField($public_field_name)) { Chris@18: continue; Chris@18: } Chris@18: $field_list = $resource_object->getField($public_field_name); Chris@18: // Config entities don't have real fields and can't have relationships. Chris@18: if (!$field_list instanceof FieldItemListInterface) { Chris@18: continue; Chris@18: } Chris@18: $field_access = $field_list->access('view', NULL, TRUE); Chris@18: if (!$field_access->isAllowed()) { Chris@18: $message = 'The current user is not allowed to view this relationship.'; Chris@18: $exception = new EntityAccessDeniedHttpException($field_list->getEntity(), $field_access, '', $message, $public_field_name); Chris@18: $includes = IncludedData::merge($includes, new IncludedData([$exception])); Chris@18: continue; Chris@18: } Chris@18: $target_type = $field_list->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type'); Chris@18: assert(!empty($target_type)); Chris@18: foreach ($field_list as $field_item) { Chris@18: assert($field_item instanceof EntityReferenceItem); Chris@18: $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue(); Chris@18: } Chris@18: } Chris@18: foreach ($references as $target_type => $ids) { Chris@18: $entity_storage = $this->entityTypeManager->getStorage($target_type); Chris@18: $targeted_entities = $entity_storage->loadMultiple(array_unique($ids)); Chris@18: $access_checked_entities = array_map(function (EntityInterface $entity) { Chris@18: return $this->entityAccessChecker->getAccessCheckedResourceObject($entity); Chris@18: }, $targeted_entities); Chris@18: $targeted_collection = new IncludedData(array_filter($access_checked_entities, function (ResourceIdentifierInterface $resource_object) { Chris@18: return !$resource_object->getResourceType()->isInternal(); Chris@18: })); Chris@18: $includes = static::resolveIncludeTree($children, $targeted_collection, IncludedData::merge($includes, $targeted_collection)); Chris@18: } Chris@18: } Chris@18: return $includes; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Returns a tree of field names to include from an include parameter. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData $data Chris@18: * The base resources for which includes should be resolved. Chris@18: * @param string $include_parameter Chris@18: * The raw include parameter value. Chris@18: * Chris@18: * @return array Chris@18: * An multi-dimensional array representing a tree of field names to be Chris@18: * included. Array keys are the field names. Leaves are empty arrays. Chris@18: */ Chris@18: protected static function toIncludeTree(ResourceObjectData $data, $include_parameter) { Chris@18: // $include_parameter: 'one.two.three, one.two.four'. Chris@18: $include_paths = array_map('trim', explode(',', $include_parameter)); Chris@18: // $exploded_paths: [['one', 'two', 'three'], ['one', 'two', 'four']]. Chris@18: $exploded_paths = array_map(function ($include_path) { Chris@18: return array_map('trim', explode('.', $include_path)); Chris@18: }, $include_paths); Chris@18: $resolved_paths = []; Chris@18: /* @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface $resource_object */ Chris@18: foreach ($data as $resource_object) { Chris@18: $resolved_paths = array_merge($resolved_paths, static::resolveInternalIncludePaths($resource_object->getResourceType(), $exploded_paths)); Chris@18: } Chris@18: return static::buildTree($resolved_paths); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Resolves an array of public field paths. Chris@18: * Chris@18: * @param \Drupal\jsonapi\ResourceType\ResourceType $base_resource_type Chris@18: * The base resource type from which to resolve an internal include path. Chris@18: * @param array $paths Chris@18: * An array of exploded include paths. Chris@18: * Chris@18: * @return array Chris@18: * An array of all possible internal include paths derived from the given Chris@18: * public include paths. Chris@18: * Chris@18: * @see self::buildTree Chris@18: */ Chris@18: protected static function resolveInternalIncludePaths(ResourceType $base_resource_type, array $paths) { Chris@18: $internal_paths = array_map(function ($exploded_path) use ($base_resource_type) { Chris@18: if (empty($exploded_path)) { Chris@18: return []; Chris@18: } Chris@18: return FieldResolver::resolveInternalIncludePath($base_resource_type, $exploded_path); Chris@18: }, $paths); Chris@18: $flattened_paths = array_reduce($internal_paths, 'array_merge', []); Chris@18: return $flattened_paths; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Takes an array of exploded paths and builds a tree of field names. Chris@18: * Chris@18: * Input example: [ Chris@18: * ['one', 'two', 'three'], Chris@18: * ['one', 'two', 'four'], Chris@18: * ['one', 'two', 'internal'], Chris@18: * ] Chris@18: * Chris@18: * Output example: [ Chris@18: * 'one' => [ Chris@18: * 'two' [ Chris@18: * 'three' => [], Chris@18: * 'four' => [], Chris@18: * 'internal' => [], Chris@18: * ], Chris@18: * ], Chris@18: * ] Chris@18: * Chris@18: * @param array $paths Chris@18: * An array of exploded include paths. Chris@18: * Chris@18: * @return array Chris@18: * An multi-dimensional array representing a tree of field names to be Chris@18: * included. Array keys are the field names. Leaves are empty arrays. Chris@18: */ Chris@18: protected static function buildTree(array $paths) { Chris@18: $merged = []; Chris@18: foreach ($paths as $parts) { Chris@18: if (!$field_name = array_shift($parts)) { Chris@18: continue; Chris@18: } Chris@18: $previous = isset($merged[$field_name]) ? $merged[$field_name] : []; Chris@18: $merged[$field_name] = array_merge($previous, [$parts]); Chris@18: } Chris@18: return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged; Chris@18: } Chris@18: Chris@18: }