annotate core/modules/jsonapi/src/IncludeResolver.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
rev   line source
Chris@5 1 <?php
Chris@5 2
Chris@5 3 namespace Drupal\jsonapi;
Chris@5 4
Chris@5 5 use Drupal\Core\Access\AccessResult;
Chris@5 6 use Drupal\Core\Entity\EntityInterface;
Chris@5 7 use Drupal\Core\Entity\EntityTypeManagerInterface;
Chris@5 8 use Drupal\Core\Field\FieldItemListInterface;
Chris@5 9 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
Chris@5 10 use Drupal\jsonapi\Access\EntityAccessChecker;
Chris@5 11 use Drupal\jsonapi\Context\FieldResolver;
Chris@5 12 use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
Chris@5 13 use Drupal\jsonapi\JsonApiResource\Data;
Chris@5 14 use Drupal\jsonapi\JsonApiResource\IncludedData;
Chris@5 15 use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
Chris@5 16 use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
Chris@5 17 use Drupal\jsonapi\JsonApiResource\ResourceObject;
Chris@5 18 use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
Chris@5 19 use Drupal\jsonapi\ResourceType\ResourceType;
Chris@5 20
Chris@5 21 /**
Chris@5 22 * Resolves included resources for an entity or collection of entities.
Chris@5 23 *
Chris@5 24 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
Chris@5 25 * class may change at any time and this will break any dependencies on it.
Chris@5 26 *
Chris@5 27 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@5 28 * @see jsonapi.api.php
Chris@5 29 */
Chris@5 30 class IncludeResolver {
Chris@5 31
Chris@5 32 /**
Chris@5 33 * The entity type manager.
Chris@5 34 *
Chris@5 35 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
Chris@5 36 */
Chris@5 37 protected $entityTypeManager;
Chris@5 38
Chris@5 39 /**
Chris@5 40 * The JSON:API entity access checker.
Chris@5 41 *
Chris@5 42 * @var \Drupal\jsonapi\Access\EntityAccessChecker
Chris@5 43 */
Chris@5 44 protected $entityAccessChecker;
Chris@5 45
Chris@5 46 /**
Chris@5 47 * IncludeResolver constructor.
Chris@5 48 */
Chris@5 49 public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityAccessChecker $entity_access_checker) {
Chris@5 50 $this->entityTypeManager = $entity_type_manager;
Chris@5 51 $this->entityAccessChecker = $entity_access_checker;
Chris@5 52 }
Chris@5 53
Chris@5 54 /**
Chris@5 55 * Resolves included resources.
Chris@5 56 *
Chris@5 57 * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
Chris@5 58 * The resource(s) for which to resolve includes.
Chris@5 59 * @param string $include_parameter
Chris@5 60 * The include query parameter to resolve.
Chris@5 61 *
Chris@5 62 * @return \Drupal\jsonapi\JsonApiResource\IncludedData
Chris@5 63 * An IncludedData object of resolved resources to be included.
Chris@5 64 *
Chris@5 65 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
Chris@5 66 * Thrown if an included entity type doesn't exist.
Chris@5 67 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
Chris@5 68 * Thrown if a storage handler couldn't be loaded.
Chris@5 69 */
Chris@5 70 public function resolve($data, $include_parameter) {
Chris@5 71 assert($data instanceof ResourceObject || $data instanceof ResourceObjectData);
Chris@5 72 $data = $data instanceof ResourceObjectData ? $data : new ResourceObjectData([$data], 1);
Chris@5 73 $include_tree = static::toIncludeTree($data, $include_parameter);
Chris@5 74 return IncludedData::deduplicate($this->resolveIncludeTree($include_tree, $data));
Chris@5 75 }
Chris@5 76
Chris@5 77 /**
Chris@5 78 * Receives a tree of include field names and resolves resources for it.
Chris@5 79 *
Chris@5 80 * This method takes a tree of relationship field names and JSON:API Data
Chris@5 81 * object. For the top-level of the tree and for each entity in the
Chris@5 82 * collection, it gets the target entity type and IDs for each relationship
Chris@5 83 * field. The method then loads all of those targets and calls itself
Chris@5 84 * recursively with the next level of the tree and those loaded resources.
Chris@5 85 *
Chris@5 86 * @param array $include_tree
Chris@5 87 * The include paths, represented as a tree.
Chris@5 88 * @param \Drupal\jsonapi\JsonApiResource\Data $data
Chris@5 89 * The entity collection from which includes should be resolved.
Chris@5 90 * @param \Drupal\jsonapi\JsonApiResource\Data|null $includes
Chris@5 91 * (Internal use only) Any prior resolved includes.
Chris@5 92 *
Chris@5 93 * @return \Drupal\jsonapi\JsonApiResource\Data
Chris@5 94 * A JSON:API Data of included items.
Chris@5 95 *
Chris@5 96 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
Chris@5 97 * Thrown if an included entity type doesn't exist.
Chris@5 98 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
Chris@5 99 * Thrown if a storage handler couldn't be loaded.
Chris@5 100 */
Chris@5 101 protected function resolveIncludeTree(array $include_tree, Data $data, Data $includes = NULL) {
Chris@5 102 $includes = is_null($includes) ? new IncludedData([]) : $includes;
Chris@5 103 foreach ($include_tree as $field_name => $children) {
Chris@5 104 $references = [];
Chris@5 105 foreach ($data as $resource_object) {
Chris@5 106 // Some objects in the collection may be LabelOnlyResourceObjects or
Chris@5 107 // EntityAccessDeniedHttpException objects.
Chris@5 108 assert($resource_object instanceof ResourceIdentifierInterface);
Chris@5 109 if ($resource_object instanceof LabelOnlyResourceObject) {
Chris@5 110 $message = "The current user is not allowed to view this relationship.";
Chris@5 111 $exception = new EntityAccessDeniedHttpException($resource_object->getEntity(), AccessResult::forbidden("The user only has authorization for the 'view label' operation."), '', $message, $field_name);
Chris@5 112 $includes = IncludedData::merge($includes, new IncludedData([$exception]));
Chris@5 113 continue;
Chris@5 114 }
Chris@5 115 elseif (!$resource_object instanceof ResourceObject) {
Chris@5 116 continue;
Chris@5 117 }
Chris@5 118 $public_field_name = $resource_object->getResourceType()->getPublicName($field_name);
Chris@5 119 // Not all entities in $entity_collection will be of the same bundle and
Chris@5 120 // may not have all of the same fields. Therefore, calling
Chris@5 121 // $resource_object->get($a_missing_field_name) will result in an
Chris@5 122 // exception.
Chris@5 123 if (!$resource_object->hasField($public_field_name)) {
Chris@5 124 continue;
Chris@5 125 }
Chris@5 126 $field_list = $resource_object->getField($public_field_name);
Chris@5 127 // Config entities don't have real fields and can't have relationships.
Chris@5 128 if (!$field_list instanceof FieldItemListInterface) {
Chris@5 129 continue;
Chris@5 130 }
Chris@5 131 $field_access = $field_list->access('view', NULL, TRUE);
Chris@5 132 if (!$field_access->isAllowed()) {
Chris@5 133 $message = 'The current user is not allowed to view this relationship.';
Chris@5 134 $exception = new EntityAccessDeniedHttpException($field_list->getEntity(), $field_access, '', $message, $public_field_name);
Chris@5 135 $includes = IncludedData::merge($includes, new IncludedData([$exception]));
Chris@5 136 continue;
Chris@5 137 }
Chris@5 138 $target_type = $field_list->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
Chris@5 139 assert(!empty($target_type));
Chris@5 140 foreach ($field_list as $field_item) {
Chris@5 141 assert($field_item instanceof EntityReferenceItem);
Chris@5 142 $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue();
Chris@5 143 }
Chris@5 144 }
Chris@5 145 foreach ($references as $target_type => $ids) {
Chris@5 146 $entity_storage = $this->entityTypeManager->getStorage($target_type);
Chris@5 147 $targeted_entities = $entity_storage->loadMultiple(array_unique($ids));
Chris@5 148 $access_checked_entities = array_map(function (EntityInterface $entity) {
Chris@5 149 return $this->entityAccessChecker->getAccessCheckedResourceObject($entity);
Chris@5 150 }, $targeted_entities);
Chris@5 151 $targeted_collection = new IncludedData(array_filter($access_checked_entities, function (ResourceIdentifierInterface $resource_object) {
Chris@5 152 return !$resource_object->getResourceType()->isInternal();
Chris@5 153 }));
Chris@5 154 $includes = static::resolveIncludeTree($children, $targeted_collection, IncludedData::merge($includes, $targeted_collection));
Chris@5 155 }
Chris@5 156 }
Chris@5 157 return $includes;
Chris@5 158 }
Chris@5 159
Chris@5 160 /**
Chris@5 161 * Returns a tree of field names to include from an include parameter.
Chris@5 162 *
Chris@5 163 * @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
Chris@5 164 * The base resources for which includes should be resolved.
Chris@5 165 * @param string $include_parameter
Chris@5 166 * The raw include parameter value.
Chris@5 167 *
Chris@5 168 * @return array
Chris@5 169 * An multi-dimensional array representing a tree of field names to be
Chris@5 170 * included. Array keys are the field names. Leaves are empty arrays.
Chris@5 171 */
Chris@5 172 protected static function toIncludeTree(ResourceObjectData $data, $include_parameter) {
Chris@5 173 // $include_parameter: 'one.two.three, one.two.four'.
Chris@5 174 $include_paths = array_map('trim', explode(',', $include_parameter));
Chris@5 175 // $exploded_paths: [['one', 'two', 'three'], ['one', 'two', 'four']].
Chris@5 176 $exploded_paths = array_map(function ($include_path) {
Chris@5 177 return array_map('trim', explode('.', $include_path));
Chris@5 178 }, $include_paths);
Chris@5 179 $resolved_paths = [];
Chris@5 180 /* @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface $resource_object */
Chris@5 181 foreach ($data as $resource_object) {
Chris@5 182 $resolved_paths = array_merge($resolved_paths, static::resolveInternalIncludePaths($resource_object->getResourceType(), $exploded_paths));
Chris@5 183 }
Chris@5 184 return static::buildTree($resolved_paths);
Chris@5 185 }
Chris@5 186
Chris@5 187 /**
Chris@5 188 * Resolves an array of public field paths.
Chris@5 189 *
Chris@5 190 * @param \Drupal\jsonapi\ResourceType\ResourceType $base_resource_type
Chris@5 191 * The base resource type from which to resolve an internal include path.
Chris@5 192 * @param array $paths
Chris@5 193 * An array of exploded include paths.
Chris@5 194 *
Chris@5 195 * @return array
Chris@5 196 * An array of all possible internal include paths derived from the given
Chris@5 197 * public include paths.
Chris@5 198 *
Chris@5 199 * @see self::buildTree
Chris@5 200 */
Chris@5 201 protected static function resolveInternalIncludePaths(ResourceType $base_resource_type, array $paths) {
Chris@5 202 $internal_paths = array_map(function ($exploded_path) use ($base_resource_type) {
Chris@5 203 if (empty($exploded_path)) {
Chris@5 204 return [];
Chris@5 205 }
Chris@5 206 return FieldResolver::resolveInternalIncludePath($base_resource_type, $exploded_path);
Chris@5 207 }, $paths);
Chris@5 208 $flattened_paths = array_reduce($internal_paths, 'array_merge', []);
Chris@5 209 return $flattened_paths;
Chris@5 210 }
Chris@5 211
Chris@5 212 /**
Chris@5 213 * Takes an array of exploded paths and builds a tree of field names.
Chris@5 214 *
Chris@5 215 * Input example: [
Chris@5 216 * ['one', 'two', 'three'],
Chris@5 217 * ['one', 'two', 'four'],
Chris@5 218 * ['one', 'two', 'internal'],
Chris@5 219 * ]
Chris@5 220 *
Chris@5 221 * Output example: [
Chris@5 222 * 'one' => [
Chris@5 223 * 'two' [
Chris@5 224 * 'three' => [],
Chris@5 225 * 'four' => [],
Chris@5 226 * 'internal' => [],
Chris@5 227 * ],
Chris@5 228 * ],
Chris@5 229 * ]
Chris@5 230 *
Chris@5 231 * @param array $paths
Chris@5 232 * An array of exploded include paths.
Chris@5 233 *
Chris@5 234 * @return array
Chris@5 235 * An multi-dimensional array representing a tree of field names to be
Chris@5 236 * included. Array keys are the field names. Leaves are empty arrays.
Chris@5 237 */
Chris@5 238 protected static function buildTree(array $paths) {
Chris@5 239 $merged = [];
Chris@5 240 foreach ($paths as $parts) {
Chris@5 241 if (!$field_name = array_shift($parts)) {
Chris@5 242 continue;
Chris@5 243 }
Chris@5 244 $previous = isset($merged[$field_name]) ? $merged[$field_name] : [];
Chris@5 245 $merged[$field_name] = array_merge($previous, [$parts]);
Chris@5 246 }
Chris@5 247 return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged;
Chris@5 248 }
Chris@5 249
Chris@5 250 }