Skip to content
Snippets Groups Projects
Commit 7e702b87 authored by Matt Glaman's avatar Matt Glaman
Browse files

Merge branch '3172884-include-parameter-doesnt' into '8.x-1.x'

Support include parameter with mixed resource object types

See merge request !11
parents 92fb9db1 1a971099
No related branches found
No related tags found
No related merge requests found
Pipeline #162680 passed with warnings
......@@ -48,6 +48,7 @@ include:
################
variables:
OPT_IN_TEST_PREVIOUS_MAJOR: 1
CORE_PREVIOUS_PHP_MIN: 8.0
OPT_IN_TEST_PREVIOUS_MINOR: 1
OPT_IN_TEST_NEXT_MINOR: 1
......
......@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\jsonapi_resources\Unstable;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
use Drupal\Core\Url;
use Drupal\jsonapi\CacheableResourceResponse;
use Drupal\jsonapi\IncludeResolver;
......@@ -13,9 +14,11 @@ use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\JsonApiResource\Link;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\NullIncludedData;
use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceResponse;
use Drupal\jsonapi\ResourceType\ResourceType;
use Symfony\Component\HttpFoundation\Request;
/**
......@@ -97,21 +100,87 @@ final class ResourceResponseFactory {
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
* @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData|\Drupal\jsonapi\JsonApiResource\ResourceObject $data
* The response data from which to resolve includes.
*
* @return \Drupal\jsonapi\JsonApiResource\IncludedData
* A Data object to be included or a NullData object if the request does not
* A Data object to be included or a NullData object if the request does
* not
* specify any include paths.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
private function getIncludes(Request $request, $data): IncludedData {
private function getIncludes(Request $request, ResourceObjectData|ResourceObject $data): IncludedData {
assert($data instanceof ResourceObject || $data instanceof ResourceObjectData);
return $request->query->has('include') && ($include_parameter = $request->query->get('include')) && !empty($include_parameter)
? $this->includeResolver->resolve($data, $include_parameter)
: new NullIncludedData();
if (!$request->query->has('include')) {
return new NullIncludedData();
}
$include_parameter = $request->query->get('include');
if (empty($include_parameter)) {
return new NullIncludedData();
}
if ($data instanceof ResourceObject) {
return $this->includeResolver->resolve($data, $include_parameter);
}
/** @var \Drupal\jsonapi\ResourceType\ResourceType[] $route_resource_types */
$route_resource_types = $request->attributes->get('resource_types');
$relatable_resource_types = array_map(
static fn (ResourceType $type) => array_keys($type->getRelatableResourceTypes()),
$route_resource_types
);
// Group resource objects to optimize IncludeResolver::toIncludeTree.
$resource_objects_by_type = [];
foreach ($data as $resource_object) {
assert($resource_object instanceof ResourceIdentifierInterface);
$resource_objects_by_type[$resource_object->getTypeName()][] = $resource_object;
}
$include_paths = array_map('trim', explode(',', $include_parameter));
$unresolved_include_paths = [];
$included_data = [];
foreach ($resource_objects_by_type as $resource_objects) {
foreach ($include_paths as $include_path) {
try {
$included_data[] = $this->includeResolver->resolve(
new ResourceObjectData($resource_objects),
$include_path
);
$unresolved_include_paths[$include_path] = FALSE;
}
catch (\Exception) {
if (!isset($unresolved_include_paths[$include_path])) {
$unresolved_include_paths[$include_path] = TRUE;
}
}
}
}
if (count(array_filter($unresolved_include_paths)) > 0) {
// Throw an error if invalid include paths provided.
// @see \Drupal\jsonapi\Context\FieldResolver::resolveInternalIncludePath().
$message = sprintf(
'%s are not valid relationship names.',
implode(',', array_map(static fn (string $path) => "`$path`", array_keys($unresolved_include_paths)))
);
if (count($relatable_resource_types) > 0) {
$message .= sprintf(' Possible values: %s', implode(', ', array_unique(array_merge(...$relatable_resource_types))));
}
throw new CacheableBadRequestHttpException(
(new CacheableMetadata())->addCacheContexts(['url.query_args:include']),
$message
);
}
$included_data = array_reduce(
$included_data,
[IncludedData::class, 'merge'],
new IncludedData([])
);
return IncludedData::deduplicate($included_data);
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\jsonapi_resources\Kernel;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\jsonapi\CacheableResourceResponse;
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests ResourceResponseFactory.
*
* @coversDefaultClass \Drupal\jsonapi_resources\Unstable\ResourceResponseFactory
* @group jsonapi_resources
*/
final class ResourceResponseFactoryTest extends KernelTestBase {
use EntityReferenceTestTrait;
use UserCreationTrait;
private const NODE_TYPE_ARTICLE_UUID = 'e5da5021-d7a0-4606-a21c-9586a8cf79a4';
private const NODE_TYPE_PAGE_UUID = '8378b97d-36fd-4515-b2eb-22e90dfdc8dc';
private const NODE_TYPE_EVENT_UUID = '12cce39f-fa9c-4c64-b7f6-a0ec511ba1e7';
private const NODE_ARTICLE_1_UUID = '7bf77016-93d2-4098-84e4-c2634c4d8ecf';
private const NODE_ARTICLE_2_UUID = '36405873-6b42-44ec-9f47-b771d83149b1';
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'field',
'file',
'serialization',
'jsonapi',
'jsonapi_resources',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->account = $this->createUser();
$this->container->get('current_user')->setAccount($this->account);
NodeType::create([
'uuid' => self::NODE_TYPE_ARTICLE_UUID,
'name' => 'article',
'type' => 'article',
])->save();
NodeType::create([
'uuid' => self::NODE_TYPE_PAGE_UUID,
'name' => 'page',
'type' => 'page',
])->save();
NodeType::create([
'uuid' => self::NODE_TYPE_EVENT_UUID,
'name' => 'event',
'type' => 'event',
])->save();
$this->createEntityReferenceField(
'node',
'page',
'field_related_articles',
'Related articles',
'node',
'default',
[
'target_bundles' => [
'reminder' => 'article',
],
],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
$this->container->get('router.builder')->rebuildIfNeeded();
}
/**
* @covers ::create
* @dataProvider createData
*/
public function testCreate(
array $query,
array $expected_includes,
string $expected_error = '',
): void {
if ($expected_error !== '') {
$this->expectExceptionMessage($expected_error);
}
$article1 = Node::create([
'uuid' => self::NODE_ARTICLE_1_UUID,
'type' => 'article',
'title' => $this->randomString(),
'status' => 1,
]);
$article1->save();
$article2 = Node::create([
'uuid' => self::NODE_ARTICLE_2_UUID,
'type' => 'article',
'title' => $this->randomString(),
'status' => 1,
]);
$article2->save();
$page = Node::create([
'type' => 'page',
'title' => $this->randomString(),
'status' => 1,
'field_related_articles' => [$article1->id(), $article2->id()],
]);
$page->save();
$event = Node::create([
'type' => 'event',
'title' => $this->randomString(),
'status' => 1,
]);
$event->save();
$resource_type_repository = $this->container->get('jsonapi.resource_type.repository');
self::assertInstanceOf(ResourceTypeRepositoryInterface::class, $resource_type_repository);
$entities = [$article1, $page, $article2, $event];
$resource_types = [];
$resource_objects = [];
foreach ($entities as $entity) {
$resource_type = $resource_type_repository->get($entity->getEntityTypeId(), $entity->bundle());
$resource_types[$resource_type->getTypeName()] = $resource_type;
$resource_objects[] = ResourceObject::createFromEntity($resource_type, $entity);
}
$request = Request::create('/foo?' . http_build_query($query));
$request->attributes->set('resource_types', array_values($resource_types));
$sut = $this->container->get('jsonapi_resources.resource_response_factory');
$response = $sut->create(
new ResourceObjectData($resource_objects),
$request
);
self::assertInstanceOf(CacheableResourceResponse::class, $response);
$document_top_level = $response->getResponseData();
self::assertInstanceOf(JsonApiDocumentTopLevel::class, $document_top_level);
/** @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface[] $includes_data */
$includes_data = $document_top_level->getIncludes()->toArray();
$includes_data = array_map(
static fn (ResourceIdentifierInterface $identifier) => [
'id' => $identifier->getId(),
'type' => $identifier->getTypeName(),
],
$includes_data
);
self::assertEquals($expected_includes, $includes_data);
}
/**
* Test data for testCreate.
*
* @return array[]
* The test data.
*/
public static function createData(): array {
return [
'mixed resource objects with same include' => [
['include' => 'node_type'],
[
[
'id' => self::NODE_TYPE_ARTICLE_UUID,
'type' => 'node_type--node_type',
],
[
'id' => self::NODE_TYPE_PAGE_UUID,
'type' => 'node_type--node_type',
],
[
'id' => self::NODE_TYPE_EVENT_UUID,
'type' => 'node_type--node_type',
],
],
],
'mixed resource objects with mismatched includes' => [
['include' => 'field_related_articles'],
[
[
'id' => self::NODE_ARTICLE_1_UUID,
'type' => 'node--article',
],
[
'id' => self::NODE_ARTICLE_2_UUID,
'type' => 'node--article',
],
],
],
'missing relationship in includes' => [
['include' => 'field_foobar'],
[],
'field_foobar` are not valid relationship names. Possible values: node_type, revision_uid, uid, field_related_articles',
],
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment