IncludeResolver.php in Drupal 10
Same filename and directory in other branches
Namespace
Drupal\jsonapiFile
core/modules/jsonapi/src/IncludeResolver.phpView source
<?php
namespace Drupal\jsonapi;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\jsonapi\Access\EntityAccessChecker;
use Drupal\jsonapi\Context\FieldResolver;
use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
use Drupal\jsonapi\JsonApiResource\Data;
use Drupal\jsonapi\JsonApiResource\IncludedData;
use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceType\ResourceType;
/**
* 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.
*
* @see https://www.drupal.org/project/drupal/issues/3032787
* @see jsonapi.api.php
*/
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;
}
$target_type = $field_list
->getFieldDefinition()
->getFieldStorageDefinition()
->getSetting('target_type');
assert(!empty($target_type));
foreach ($field_list as $field_item) {
assert($field_item instanceof EntityReferenceItem);
$references[$target_type][] = $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;
}
}
Classes
Name | Description |
---|---|
IncludeResolver | Resolves included resources for an entity or collection of entities. |