From b02ba5ca2c7624a196372b2e1ca94f414188a614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20D=C4=99bi=C5=84ski?= Date: Wed, 28 May 2025 10:46:13 +0200 Subject: [PATCH 1/4] IBX-8417: Added the ability to enable pagination for ezobjectrelationlist field --- .../RelationFieldDefinitionMapperSpec.php | 12 +-- .../Resources/config/default_settings.yaml | 1 + .../Resources/config/graphql/Field.types.yaml | 10 +++ .../Resources/config/services/resolvers.yaml | 2 + .../Resources/config/services/schema.yaml | 1 + src/lib/Resolver/RelationFieldResolver.php | 73 +++++++++++++++---- .../RelationFieldDefinitionMapper.php | 44 ++++++++--- 7 files changed, 110 insertions(+), 33 deletions(-) diff --git a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php index a2ae8df4..7abec4f6 100644 --- a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php +++ b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php @@ -18,7 +18,7 @@ class RelationFieldDefinitionMapperSpec extends ObjectBehavior function let(NameHelper $nameHelper, ContentTypeService $contentTypeService, FieldDefinitionMapper $innerMapper) { - $this->beConstructedWith($innerMapper, $nameHelper, $contentTypeService); + $this->beConstructedWith($innerMapper, $nameHelper, $contentTypeService, true); $articleContentType = new ContentType(['identifier' => 'article']); $folderContentType = new ContentType(['identifier' => 'folder']); @@ -56,19 +56,19 @@ function it_maps_single_selection_with_a_unique_type_limitations_to_a_single_ite function it_maps_multi_selection_without_type_limitations_to_an_array_of_generic_content() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, []); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_maps_multi_selection_with_multiple_type_limitations_to_an_array_of_generic_content() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_NONE, ['article', 'blog_post']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_maps_multi_selection_with_a_unique_type_limitations_to_an_array_of_that_type() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, ['article']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[ArticleItem]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_delegates_the_field_definition_type_to_the_inner_mapper(FieldDefinitionMapper $innerMapper) @@ -81,13 +81,13 @@ function it_delegates_the_field_definition_type_to_the_inner_mapper(FieldDefinit function it_maps_multi_selection_to_resolve_multiple() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, true])'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, true, args])'); } function it_maps_single_selection_to_resolve_single() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_SINGLE); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, false])'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, false, args])'); } private function createFieldDefinition($selectionLimit = 0, $selectionContentTypes = []) diff --git a/src/bundle/Resources/config/default_settings.yaml b/src/bundle/Resources/config/default_settings.yaml index b2de9003..b6ec036e 100644 --- a/src/bundle/Resources/config/default_settings.yaml +++ b/src/bundle/Resources/config/default_settings.yaml @@ -1,5 +1,6 @@ parameters: ibexa.graphql.schema.should.extend.ezurl: false + ibexa.graphql.schema.ezobjectrelationlist.enable_pagination: false ibexa.graphql.schema.content.field_name.override: id: id_ ibexa.graphql.schema.content.mapping.field_definition_type: diff --git a/src/bundle/Resources/config/graphql/Field.types.yaml b/src/bundle/Resources/config/graphql/Field.types.yaml index f4138721..75115435 100644 --- a/src/bundle/Resources/config/graphql/Field.types.yaml +++ b/src/bundle/Resources/config/graphql/Field.types.yaml @@ -279,3 +279,13 @@ UrlFieldValue: type: String description: "The link's name or description" resolve: "@=value.text" + +RelationsConnection: + type: relay-connection + config: + nodeType: Item + connectionFields: + sliceSize: + type: Int! + orderBy: + type: String diff --git a/src/bundle/Resources/config/services/resolvers.yaml b/src/bundle/Resources/config/services/resolvers.yaml index 8b554697..1a51eefb 100644 --- a/src/bundle/Resources/config/services/resolvers.yaml +++ b/src/bundle/Resources/config/services/resolvers.yaml @@ -49,6 +49,8 @@ services: - { name: overblog_graphql.resolver, alias: "ItemsOfTypeAsConnection", method: "resolveItemsOfTypeAsConnection" } Ibexa\GraphQL\Resolver\RelationFieldResolver: + arguments: + $enablePagination: '%ibexa.graphql.schema.ezobjectrelationlist.enable_pagination%' tags: - { name: overblog_graphql.resolver, alias: "RelationFieldValue", method: "resolveRelationFieldValue" } diff --git a/src/bundle/Resources/config/services/schema.yaml b/src/bundle/Resources/config/services/schema.yaml index 14741e47..66a0953d 100644 --- a/src/bundle/Resources/config/services/schema.yaml +++ b/src/bundle/Resources/config/services/schema.yaml @@ -43,6 +43,7 @@ services: arguments: $contentTypeService: '@ibexa.siteaccessaware.service.content_type' $innerMapper: '@Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\RelationFieldDefinitionMapper.inner' + $enablePagination: '%ibexa.graphql.schema.ezobjectrelationlist.enable_pagination%' Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\SelectionFieldDefinitionMapper: decorates: Ibexa\Contracts\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper diff --git a/src/lib/Resolver/RelationFieldResolver.php b/src/lib/Resolver/RelationFieldResolver.php index f55dd3bc..64d71066 100644 --- a/src/lib/Resolver/RelationFieldResolver.php +++ b/src/lib/Resolver/RelationFieldResolver.php @@ -11,23 +11,32 @@ use Ibexa\Core\FieldType; use Ibexa\GraphQL\DataLoader\ContentLoader; use Ibexa\GraphQL\ItemFactory; +use Ibexa\GraphQL\Relay\PageAwareConnection; use Ibexa\GraphQL\Value\Field; +use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Relay\Connection\Paginator; final class RelationFieldResolver { - /** @var \Ibexa\GraphQL\DataLoader\ContentLoader */ - private $contentLoader; + public const DEFAULT_LIMIT = 25; - /** @var \Ibexa\GraphQL\ItemFactory */ - private $itemFactory; + private ContentLoader $contentLoader; - public function __construct(ContentLoader $contentLoader, ItemFactory $relatedContentItemFactory) - { + private ItemFactory $itemFactory; + + private bool $enablePagination; + + public function __construct( + ContentLoader $contentLoader, + ItemFactory $relatedContentItemFactory, + bool $enablePagination + ) { $this->contentLoader = $contentLoader; $this->itemFactory = $relatedContentItemFactory; + $this->enablePagination = $enablePagination; } - public function resolveRelationFieldValue(Field $field, $multiple = false) + public function resolveRelationFieldValue(Field $field, $multiple = false, Argument $args) { $destinationContentIds = $this->getContentIds($field); @@ -35,21 +44,53 @@ public function resolveRelationFieldValue(Field $field, $multiple = false) return $multiple ? [] : null; } - $contentItems = $this->contentLoader->find(new Query( + $query = new Query( ['filter' => new Query\Criterion\ContentId($destinationContentIds)] - )); + ); if ($multiple) { - return array_map( - function ($contentId) use ($contentItems) { - return $this->itemFactory->fromContent( - $contentItems[array_search($contentId, array_column($contentItems, 'id'))] - ); - }, - $destinationContentIds + if (!$this->enablePagination) { + $contentItems = $this->contentLoader->find($query); + + return array_map( + function ($contentId) use ($contentItems) { + return $this->itemFactory->fromContent( + $contentItems[array_search($contentId, array_column($contentItems, 'id'))] + ); + }, + $destinationContentIds + ); + } + + $paginator = new Paginator(function ($offset, $limit) use ($query) { + $query->offset = $offset; + $query->limit = $limit ?? self::DEFAULT_LIMIT; + $contentItems = $this->contentLoader->find($query); + + return array_map( + function ($content) { + return $this->itemFactory->fromContent( + $content + ); + }, + $contentItems + ); + }); + + return PageAwareConnection::fromConnection( + $paginator->auto( + $args, + function () use ($query) { + return $this->contentLoader->count($query); + } + ), + $args ); } + $query->limit = 1; + $contentItems = $this->contentLoader->find($query); + return $contentItems[0] ? $this->itemFactory->fromContent($contentItems[0]) : null; } diff --git a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php index dc6a4471..3daf535b 100644 --- a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php +++ b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php @@ -14,24 +14,22 @@ class RelationFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper { - /** - * @var \Ibexa\GraphQL\Schema\Domain\Content\NameHelper - */ - private $nameHelper; + private NameHelper $nameHelper; - /** - * @var \Ibexa\Contracts\Core\Repository\ContentTypeService - */ - private $contentTypeService; + private ContentTypeService $contentTypeService; + + private bool $enablePagination; public function __construct( FieldDefinitionMapper $innerMapper, NameHelper $nameHelper, - ContentTypeService $contentTypeService + ContentTypeService $contentTypeService, + bool $enablePagination ) { parent::__construct($innerMapper); $this->nameHelper = $nameHelper; $this->contentTypeService = $contentTypeService; + $this->enablePagination = $enablePagination; } public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string @@ -54,7 +52,18 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string } if ($this->isMultiple($fieldDefinition)) { - $type = "[$type]"; + if ($this->enablePagination) { + $type = 'RelationsConnection'; + } else { + @trigger_error( + 'Disable pagination for ezobjectrelationlist has been deprecated since version 4.6 ' . + 'and will be removed in version 5.0. To start receiving `RelationsConnection` instead of the deprecated ' . + '`[' . $type . ']`, set the parameter `ibexa.graphql.schema.ezobjectrelationlist.enable_pagination` to `true`.', + E_USER_DEPRECATED + ); + + $type = "[$type]"; + } } return $type; @@ -68,7 +77,7 @@ public function mapToFieldValueResolver(FieldDefinition $fieldDefinition): ?stri $isMultiple = $this->isMultiple($fieldDefinition) ? 'true' : 'false'; - return sprintf('@=resolver("RelationFieldValue", [field, %s])', $isMultiple); + return sprintf('@=resolver("RelationFieldValue", [field, %s, args])', $isMultiple); } protected function canMap(FieldDefinition $fieldDefinition) @@ -76,6 +85,19 @@ protected function canMap(FieldDefinition $fieldDefinition) return in_array($fieldDefinition->fieldTypeIdentifier, ['ezobjectrelation', 'ezobjectrelationlist']); } + public function mapToFieldValueArgsBuilder(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + + if ($this->isMultiple($fieldDefinition) && $this->enablePagination) { + return 'Relay::Connection'; + } + + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + /** * Not implemented since we don't use it (canMap is overridden). */ From a4b64ddc1cd33e0819abcfaace1410878857ff9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20D=C4=99bi=C5=84ski?= Date: Fri, 4 Jul 2025 10:45:49 +0200 Subject: [PATCH 2/4] Changed parameter name, made $args nullable --- .../Resources/config/default_settings.yaml | 2 +- .../Resources/config/services/schema.yaml | 2 +- src/lib/Resolver/RelationFieldResolver.php | 19 +++++++++++-------- .../RelationFieldDefinitionMapper.php | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/bundle/Resources/config/default_settings.yaml b/src/bundle/Resources/config/default_settings.yaml index b6ec036e..d1189da0 100644 --- a/src/bundle/Resources/config/default_settings.yaml +++ b/src/bundle/Resources/config/default_settings.yaml @@ -1,6 +1,6 @@ parameters: ibexa.graphql.schema.should.extend.ezurl: false - ibexa.graphql.schema.ezobjectrelationlist.enable_pagination: false + ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination: false ibexa.graphql.schema.content.field_name.override: id: id_ ibexa.graphql.schema.content.mapping.field_definition_type: diff --git a/src/bundle/Resources/config/services/schema.yaml b/src/bundle/Resources/config/services/schema.yaml index 66a0953d..f58d7499 100644 --- a/src/bundle/Resources/config/services/schema.yaml +++ b/src/bundle/Resources/config/services/schema.yaml @@ -43,7 +43,7 @@ services: arguments: $contentTypeService: '@ibexa.siteaccessaware.service.content_type' $innerMapper: '@Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\RelationFieldDefinitionMapper.inner' - $enablePagination: '%ibexa.graphql.schema.ezobjectrelationlist.enable_pagination%' + $enablePagination: '%ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination%' Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\SelectionFieldDefinitionMapper: decorates: Ibexa\Contracts\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper diff --git a/src/lib/Resolver/RelationFieldResolver.php b/src/lib/Resolver/RelationFieldResolver.php index 64d71066..245294c2 100644 --- a/src/lib/Resolver/RelationFieldResolver.php +++ b/src/lib/Resolver/RelationFieldResolver.php @@ -7,6 +7,7 @@ namespace Ibexa\GraphQL\Resolver; use GraphQL\Error\UserError; +use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Core\FieldType; use Ibexa\GraphQL\DataLoader\ContentLoader; @@ -36,7 +37,7 @@ public function __construct( $this->enablePagination = $enablePagination; } - public function resolveRelationFieldValue(Field $field, $multiple = false, Argument $args) + public function resolveRelationFieldValue(Field $field, $multiple = false, ?Argument $args = null) { $destinationContentIds = $this->getContentIds($field); @@ -49,13 +50,13 @@ public function resolveRelationFieldValue(Field $field, $multiple = false, Argum ); if ($multiple) { - if (!$this->enablePagination) { + if (!$this->enablePagination || $args === null) { $contentItems = $this->contentLoader->find($query); return array_map( - function ($contentId) use ($contentItems) { + function (int $contentId) use ($contentItems) { return $this->itemFactory->fromContent( - $contentItems[array_search($contentId, array_column($contentItems, 'id'))] + $contentItems[array_search($contentId, array_column($contentItems, 'id'), true)] ); }, $destinationContentIds @@ -68,7 +69,7 @@ function ($contentId) use ($contentItems) { $contentItems = $this->contentLoader->find($query); return array_map( - function ($content) { + function (Content $content) { return $this->itemFactory->fromContent( $content ); @@ -103,11 +104,13 @@ private function getContentIds(Field $field): array { if ($field->value instanceof FieldType\RelationList\Value) { return $field->value->destinationContentIds; - } elseif ($field->value instanceof FieldType\Relation\Value) { + } + + if ($field->value instanceof FieldType\Relation\Value) { return [$field->value->destinationContentId]; - } else { - throw new UserError('\$field does not contain a RelationList or Relation Field value'); } + + throw new UserError('\$field does not contain a RelationList or Relation Field value'); } } diff --git a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php index 3daf535b..010e9b1b 100644 --- a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php +++ b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php @@ -58,7 +58,7 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string @trigger_error( 'Disable pagination for ezobjectrelationlist has been deprecated since version 4.6 ' . 'and will be removed in version 5.0. To start receiving `RelationsConnection` instead of the deprecated ' . - '`[' . $type . ']`, set the parameter `ibexa.graphql.schema.ezobjectrelationlist.enable_pagination` to `true`.', + '`[' . $type . ']`, set the parameter `ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination` to `true`.', E_USER_DEPRECATED ); From f000e864d4b17a5b8e840ded3c914ea549692345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20D=C4=99bi=C5=84ski?= Date: Mon, 14 Jul 2025 12:45:33 +0200 Subject: [PATCH 3/4] Added missing return type --- src/lib/Resolver/RelationFieldResolver.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/Resolver/RelationFieldResolver.php b/src/lib/Resolver/RelationFieldResolver.php index 245294c2..5bb220dd 100644 --- a/src/lib/Resolver/RelationFieldResolver.php +++ b/src/lib/Resolver/RelationFieldResolver.php @@ -14,6 +14,7 @@ use Ibexa\GraphQL\ItemFactory; use Ibexa\GraphQL\Relay\PageAwareConnection; use Ibexa\GraphQL\Value\Field; +use Ibexa\GraphQL\Value\Item; use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\Relay\Connection\Paginator; @@ -54,7 +55,7 @@ public function resolveRelationFieldValue(Field $field, $multiple = false, ?Argu $contentItems = $this->contentLoader->find($query); return array_map( - function (int $contentId) use ($contentItems) { + function (int $contentId) use ($contentItems): Item { return $this->itemFactory->fromContent( $contentItems[array_search($contentId, array_column($contentItems, 'id'), true)] ); @@ -63,13 +64,13 @@ function (int $contentId) use ($contentItems) { ); } - $paginator = new Paginator(function ($offset, $limit) use ($query) { + $paginator = new Paginator(function ($offset, $limit) use ($query): array { $query->offset = $offset; $query->limit = $limit ?? self::DEFAULT_LIMIT; $contentItems = $this->contentLoader->find($query); return array_map( - function (Content $content) { + function (Content $content): Item { return $this->itemFactory->fromContent( $content ); @@ -81,7 +82,7 @@ function (Content $content) { return PageAwareConnection::fromConnection( $paginator->auto( $args, - function () use ($query) { + function () use ($query): int { return $this->contentLoader->count($query); } ), From 4ad3ca85583134aa2f5787bf1325211fbfaa299b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20D=C4=99bi=C5=84ski?= Date: Wed, 30 Jul 2025 14:15:56 +0200 Subject: [PATCH 4/4] Corrected parameter name --- src/bundle/Resources/config/services/resolvers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Resources/config/services/resolvers.yaml b/src/bundle/Resources/config/services/resolvers.yaml index 1a51eefb..3b708959 100644 --- a/src/bundle/Resources/config/services/resolvers.yaml +++ b/src/bundle/Resources/config/services/resolvers.yaml @@ -50,7 +50,7 @@ services: Ibexa\GraphQL\Resolver\RelationFieldResolver: arguments: - $enablePagination: '%ibexa.graphql.schema.ezobjectrelationlist.enable_pagination%' + $enablePagination: '%ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination%' tags: - { name: overblog_graphql.resolver, alias: "RelationFieldValue", method: "resolveRelationFieldValue" }