Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. IncludeResolver.php

class IncludeResolver

Resolves included resources for an entity or collection of entities.

@internal JSON:API maintains no PHP API since its API is the HTTP API. This class may change at any time and this will break any dependencies on it.

Hierarchy

  • class \Drupal\jsonapi\IncludeResolver

Expanded class hierarchy of IncludeResolver

See also

https://www.drupal.org/project/drupal/issues/3032787

jsonapi.api.php

1 file declares its use of IncludeResolver
EntityResource.php in core/modules/jsonapi/src/Controller/EntityResource.php
1 string reference to 'IncludeResolver'
jsonapi.services.yml in core/modules/jsonapi/jsonapi.services.yml
core/modules/jsonapi/jsonapi.services.yml
1 service uses IncludeResolver
jsonapi.include_resolver in core/modules/jsonapi/jsonapi.services.yml
Drupal\jsonapi\IncludeResolver

File

core/modules/jsonapi/src/IncludeResolver.php, line 30

Namespace

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

}

Members

Title Sort descending Modifiers Object type Summary
IncludeResolver::$entityAccessChecker protected property The JSON:API entity access checker.
IncludeResolver::$entityTypeManager protected property The entity type manager.
IncludeResolver::buildTree protected static function Takes an array of exploded paths and builds a tree of field names.
IncludeResolver::resolve public function Resolves included resources.
IncludeResolver::resolveIncludeTree protected function Receives a tree of include field names and resolves resources for it.
IncludeResolver::resolveInternalIncludePaths protected static function Resolves an array of public field paths.
IncludeResolver::toIncludeTree protected static function Returns a tree of field names to include from an include parameter.
IncludeResolver::__construct public function IncludeResolver constructor.

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal