From 23f88207e298ee4f061432a8d8cbe181322b7e66 Mon Sep 17 00:00:00 2001 From: Bertrand Dunogier Date: Fri, 31 Jan 2020 11:03:01 +0100 Subject: [PATCH] Implemented pagination --- doc/howto/customize_rendering.md | 23 +++- .../ContentQueryFieldDefinitionMapperSpec.php | 24 +++- src/API/QueryFieldPaginationService.php | 21 ++++ src/API/QueryFieldService.php | 78 ++++++++++--- src/Controller/QueryFieldRestController.php | 21 +++- .../ContentQueryFieldDefinitionMapper.php | 46 +++++++- src/GraphQL/QueryFieldResolver.php | 25 +++++ .../Resources/config/services/graphql.yml | 3 + .../views/content/contentquery.html.twig | 4 + .../views/fieldtype/field_view.html.twig | 5 +- .../fieldtype/fielddefinition_edit.html.twig | 12 ++ src/eZ/ContentView/QueryResultsInjector.php | 103 ++++++++++++++++-- .../QueryResultsPagerFantaAdapter.php | 51 +++++++++ src/eZ/FieldType/Mapper/QueryFormMapper.php | 13 +++ src/eZ/FieldType/Query/Type.php | 14 +++ .../FieldValue/Converter/QueryConverter.php | 4 + 16 files changed, 405 insertions(+), 42 deletions(-) create mode 100644 src/API/QueryFieldPaginationService.php create mode 100644 src/eZ/ContentView/QueryResultsPagerFantaAdapter.php diff --git a/doc/howto/customize_rendering.md b/doc/howto/customize_rendering.md index e728c70..0f95fe1 100644 --- a/doc/howto/customize_rendering.md +++ b/doc/howto/customize_rendering.md @@ -4,11 +4,17 @@ In this how-to, you will learn how to customize all the rendering phases. ## Rendering process Content query fields are rendered using `ez_render_field()`. -The query is executed, the items iterated on, and each is be displayed using the `line` content view. +The query is executed, the items iterated on, and each is rendered using the `line` content view. That template renders the content, the view controller, with a custom view type (`content_query_view`). A custom view builder executes execute the query, and assigns the results to the view as `items`. The default template for that view (`query_field_view.html.twig`) iterates on each item resulting from the query, and renders each with the `line` view. +The field renderer for a query field supports the following parameters: +- `bool enablePagination`: force pagination enabled, even if it is disabled for that field definition +- `bool disablePagination`: force pagination disabled, even if it is disabled for that field definition +- `int itemsPerPage`: sets how many items are displayed per page with pagination. Required if `enablePagination` is + used and pagination is disabled in the field definition + ### Summary 1. Your template: `ez_render_field(content, 'queryfield')` 2. Field view template: `fieldtype_ui.html.twig`: @@ -41,6 +47,7 @@ As with any content view, a custom controller can also be defined to further cus Use the `items` iterable to loop over the field's content items: ``` +
{% for item in items %} {{ render(controller("ez_content:viewAction", { "contentId": item.id, @@ -48,9 +55,19 @@ Use the `items` iterable to loop over the field's content items: "viewType": itemViewType })) }} {% endfor %} +
+ +{% if isPaginationEnabled %} + {{ pagerfanta( items, 'ez', {'routeName': location, 'pageParameter': pageParameter } ) }} +{% endif %} ``` -The usual [content view templates variables](https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables) are also available, including `content`, the item that contains the query field. +In addition to the [usual content view templates variables](https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables), the following variables are available: +- `Content content`: the item that contains the query field. +- `bool isPaginationEnabled`: indicates if pagination is enabled. When it is, `items` is a `PagerFanta` instance. +- `string pageParameter`: when pagination is enabled, contains the page parameter to use as the pager fanta + `pageParameter` argument (important, as it makes every pager unique, required if there are several query + fields in the same item) ## Customizing the line view template The line view template, used to render each result, can be customized by creating `line` view configuration rules. @@ -69,7 +86,7 @@ ezplatform: template: "path/to/template.html.twig" ``` -The variables are the same as any view template ([documentation]((https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables))). +The variables are the same than other view template ([documentation]((https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables))). ## Advanced diff --git a/spec/GraphQL/ContentQueryFieldDefinitionMapperSpec.php b/spec/GraphQL/ContentQueryFieldDefinitionMapperSpec.php index a9fd0e2..46a089c 100644 --- a/spec/GraphQL/ContentQueryFieldDefinitionMapperSpec.php +++ b/spec/GraphQL/ContentQueryFieldDefinitionMapperSpec.php @@ -79,24 +79,38 @@ function it_delegates_field_definition_type_to_the_parent_mapper_for_a_non_query ->shouldBe('FieldValue'); } - function it_delegates_the_value_resolver_to_the_parent_mapper(FieldDefinitionMapper $innerMapper) + function it_maps_the_field_value_when_pagination_is_disabled(FieldDefinitionMapper $innerMapper) { $fieldDefinition = $this->fieldDefinition(); - $innerMapper->mapToFieldValueResolver($fieldDefinition)->willReturn('resolver'); + $innerMapper->mapToFieldValueResolver($fieldDefinition)->shouldNotBeCalled(); $this ->mapToFieldValueResolver($fieldDefinition) - ->shouldBe('resolver'); + ->shouldBe('@=resolver("QueryFieldValue", [field, content])'); + } + + function it_maps_the_field_value_when_pagination_is_enabled(FieldDefinitionMapper $innerMapper) + { + $fieldDefinition = $this->fieldDefinition(true); + $innerMapper->mapToFieldValueResolver($fieldDefinition)->shouldNotBeCalled(); + $this + ->mapToFieldValueResolver($fieldDefinition) + ->shouldBe('@=resolver("QueryFieldValueConnection", [args, field, content])'); } /** + * @param bool $enablePagination + * * @return FieldDefinition */ - private function fieldDefinition(): FieldDefinition + private function fieldDefinition(bool $enablePagination = false): FieldDefinition { return new FieldDefinition([ 'identifier' => self::FIELD_IDENTIFIER, 'fieldTypeIdentifier' => self::FIELD_TYPE_IDENTIFIER, - 'fieldSettings' => ['ReturnedType' => self::RETURNED_CONTENT_TYPE_IDENTIFIER] + 'fieldSettings' => [ + 'ReturnedType' => self::RETURNED_CONTENT_TYPE_IDENTIFIER, + 'EnablePagination' => $enablePagination + ] ]); } diff --git a/src/API/QueryFieldPaginationService.php b/src/API/QueryFieldPaginationService.php new file mode 100644 index 0000000..6f6910a --- /dev/null +++ b/src/API/QueryFieldPaginationService.php @@ -0,0 +1,21 @@ +searchService->findContent($query)->totalCount; } + public function loadContentItemsSlice(Content $content, string $fieldDefinitionIdentifier, int $offset, int $limit): iterable + { + $query = $this->prepareQuery($content, $fieldDefinitionIdentifier); + $query->offset = $offset; + $query->limit = $limit; + + return array_map( + function (SearchHit $searchHit) { + return $searchHit->valueObject; + }, + $this->searchService->findContent($query)->searchHits + ); + } + + public function getPaginationConfiguration(Content $content, string $fieldDefinitionIdentifier): int + { + $fieldDefinition = $this->loadFieldDefinition($content, $fieldDefinitionIdentifier); + + if ($fieldDefinition->fieldSettings['EnablePagination'] === false) { + return false; + } + + return $fieldDefinition->fieldSettings['ItemsPerPage']; + } + /** - * @param array $parameters parameters that may include expressions to be resolved - * @param \eZ\Publish\API\Repository\Values\Content\Content $content + * @param array $expressions parameters that may include expressions to be resolved + * @param array $variables * * @return array */ - private function resolveParameters(array $parameters, array $variables): array + private function resolveParameters(array $expressions, array $variables): array { - foreach ($parameters as $key => $expression) { + foreach ($expressions as $key => $expression) { if (is_array($expression)) { - $parameters[$key] = $this->resolveParameters($expression, $variables); + $expressions[$key] = $this->resolveParameters($expression, $variables); } else { - $parameters[$key] = $this->resolveExpression($expression, $variables); + $expressions[$key] = $this->resolveExpression($expression, $variables); } } - return $parameters; + return $expressions; } private function resolveExpression(string $expression, array $variables) @@ -110,24 +136,40 @@ private function resolveExpression(string $expression, array $variables) * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException */ - private function prepareQuery(Content $content, string $fieldDefinitionIdentifier): Query + private function prepareQuery(Content $content, string $fieldDefinitionIdentifier, array $extraParameters = []): Query { - $fieldDefinition = $this - ->contentTypeService->loadContentType($content->contentInfo->contentTypeId) - ->getFieldDefinition($fieldDefinitionIdentifier); + $fieldDefinition = $this->loadFieldDefinition($content, $fieldDefinitionIdentifier); $location = $this->locationService->loadLocation($content->contentInfo->mainLocationId); $queryType = $this->queryTypeRegistry->getQueryType($fieldDefinition->fieldSettings['QueryType']); $parameters = $this->resolveParameters( $fieldDefinition->fieldSettings['Parameters'], - [ - 'content' => $content, - 'contentInfo' => $content->contentInfo, - 'mainLocation' => $location, - 'returnedType' => $fieldDefinition->fieldSettings['ReturnedType'], - ] + array_merge( + $extraParameters, + [ + 'content' => $content, + 'contentInfo' => $content->contentInfo, + 'mainLocation' => $location, + 'returnedType' => $fieldDefinition->fieldSettings['ReturnedType'], + ] + ) ); return $queryType->getQuery($parameters); } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Content $content + * @param string $fieldDefinitionIdentifier + * + * @return \eZ\Publish\API\Repository\Values\ContentType\FieldDefinition|null + * + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + */ + private function loadFieldDefinition(Content $content, string $fieldDefinitionIdentifier): FieldDefinition + { + return $fieldDefinition = $this + ->contentTypeService->loadContentType($content->contentInfo->contentTypeId) + ->getFieldDefinition($fieldDefinitionIdentifier); + } } diff --git a/src/Controller/QueryFieldRestController.php b/src/Controller/QueryFieldRestController.php index 6656569..d1cbc11 100644 --- a/src/Controller/QueryFieldRestController.php +++ b/src/Controller/QueryFieldRestController.php @@ -15,6 +15,7 @@ use eZ\Publish\API\Repository\Values\ContentType\ContentType; use eZ\Publish\Core\REST\Server\Values\ContentList; use eZ\Publish\Core\REST\Server\Values\RestContent; +use Symfony\Component\HttpFoundation\Request; final class QueryFieldRestController { @@ -42,11 +43,19 @@ public function __construct( $this->locationService = $locationService; } - public function getResults($contentId, $versionNumber, $fieldDefinitionIdentifier): ContentList + public function getResults(Request $request, $contentId, $versionNumber, $fieldDefinitionIdentifier): ContentList { + $offset = (int)$request->query->get('offset', 0); + $limit = (int)$request->query->get('limit', -1); + $content = $this->contentService->loadContent($contentId, null, $versionNumber); + if ($limit === -1 || !method_exists($this->queryFieldService, 'loadContentItemsSlice')) { + $items = $this->queryFieldService->loadContentItems($content, $fieldDefinitionIdentifier); + } else { + $items = $this->queryFieldService->loadContentItemsSlice($content, $fieldDefinitionIdentifier, $offset, $limit); + } - return new ContentList( + $list = new ContentList( array_map( function (Content $content) { return new RestContent( @@ -57,9 +66,15 @@ function (Content $content) { $this->contentService->loadRelations($content->getVersionInfo()) ); }, - $this->queryFieldService->loadContentItems($content, $fieldDefinitionIdentifier) + $items ) ); + + if (property_exists($list, 'totalCount')) { + $list->totalCount = $this->queryFieldService->countContentItems($content, $fieldDefinitionIdentifier); + } + + return $list; } private function getContentType(ContentInfo $contentInfo): ContentType diff --git a/src/GraphQL/ContentQueryFieldDefinitionMapper.php b/src/GraphQL/ContentQueryFieldDefinitionMapper.php index 24621a1..53a4e10 100644 --- a/src/GraphQL/ContentQueryFieldDefinitionMapper.php +++ b/src/GraphQL/ContentQueryFieldDefinitionMapper.php @@ -9,10 +9,11 @@ use eZ\Publish\API\Repository\ContentTypeService; use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition; use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\DecoratingFieldDefinitionMapper; +use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionArgsBuilderMapper; use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper; use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\NameHelper; -final class ContentQueryFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper +final class ContentQueryFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper, FieldDefinitionArgsBuilderMapper { /** @var NameHelper */ private $nameHelper; @@ -38,7 +39,26 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string $fieldSettings = $fieldDefinition->getFieldSettings(); - return '[' . $this->getDomainTypeName($fieldSettings['ReturnedType']) . ']'; + if ($fieldSettings['EnablePagination']) { + return $this->nameValueConnectionType($fieldSettings['ReturnedType']); + } else { + return '[' . $this->nameValueType($fieldSettings['ReturnedType']) . ']'; + } + } + + public function mapToFieldValueResolver(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueType($fieldDefinition); + } + + $fieldSettings = $fieldDefinition->getFieldSettings(); + + if ($fieldSettings['EnablePagination']) { + return '@=resolver("QueryFieldValueConnection", [args, field, content])'; + } else { + return '@=resolver("QueryFieldValue", [field, content])'; + } } public function mapToFieldDefinitionType(FieldDefinition $fieldDefinition): ?string @@ -50,15 +70,35 @@ public function mapToFieldDefinitionType(FieldDefinition $fieldDefinition): ?str return 'ContentQueryFieldDefinition'; } + public function mapToFieldValueArgsBuilder(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + + if ($fieldDefinition->fieldSettings['EnablePagination']) { + return 'Relay::Connection'; + } else { + return null; + } + } + protected function getFieldTypeIdentifier(): string { return 'ezcontentquery'; } - private function getDomainTypeName($typeIdentifier) + private function nameValueType($typeIdentifier): string { return $this->nameHelper->domainContentName( $this->contentTypeService->loadContentTypeByIdentifier($typeIdentifier) ); } + + private function nameValueConnectionType($typeIdentifier): string + { + return $this->nameHelper->domainContentConnection( + $this->contentTypeService->loadContentTypeByIdentifier($typeIdentifier) + ); + } } diff --git a/src/GraphQL/QueryFieldResolver.php b/src/GraphQL/QueryFieldResolver.php index b3b82ed..55f699e 100644 --- a/src/GraphQL/QueryFieldResolver.php +++ b/src/GraphQL/QueryFieldResolver.php @@ -6,9 +6,12 @@ */ namespace EzSystems\EzPlatformQueryFieldType\GraphQL; +use EzSystems\EzPlatformQueryFieldType\API\QueryFieldPaginationService; use EzSystems\EzPlatformQueryFieldType\API\QueryFieldServiceInterface; use eZ\Publish\API\Repository\Values\Content\Content; use EzSystems\EzPlatformGraphQL\GraphQL\Value\Field; +use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Relay\Connection\Paginator; final class QueryFieldResolver { @@ -25,6 +28,28 @@ public function resolveQueryField(Field $field, Content $content) return $this->queryFieldService->loadContentItems($content, $field->fieldDefIdentifier); } + public function resolveQueryFieldConnection(Argument $args, Field $field, Content $content) + { + if (!$this->queryFieldService instanceof QueryFieldPaginationService) { + throw new \Exception("The QueryFieldService isn't able to handle pagination, this should not happen"); + } + + if (!isset($args['first'])) { + $args['first'] = $this->queryFieldService->getPaginationConfiguration($content, $field->fieldDefIdentifier); + } + + $paginator = new Paginator(function ($offset, $limit) use ($content, $field) { + return $this->queryFieldService->loadContentItemsSlice($content, $field->fieldDefIdentifier, $offset, $limit); + }); + + return $paginator->auto( + $args, + function () use ($content, $field) { + return $this->queryFieldService->countContentItems($content, $field->fieldDefIdentifier); + } + ); + } + public function resolveQueryFieldDefinitionParameters(array $parameters): array { $return = []; diff --git a/src/Symfony/Resources/config/services/graphql.yml b/src/Symfony/Resources/config/services/graphql.yml index b5b5cd4..65ecf00 100644 --- a/src/Symfony/Resources/config/services/graphql.yml +++ b/src/Symfony/Resources/config/services/graphql.yml @@ -7,9 +7,12 @@ services: EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldResolver: tags: - { name: overblog_graphql.resolver, alias: "QueryFieldValue", method: "resolveQueryField" } + - { name: overblog_graphql.resolver, alias: "QueryFieldValueConnection", method: "resolveQueryFieldConnection" } - { name: overblog_graphql.resolver, alias: "QueryFieldDefinitionParameters", method: "resolveQueryFieldDefinitionParameters" } EzSystems\EzPlatformQueryFieldType\GraphQL\ContentQueryFieldDefinitionMapper: decorates: EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper arguments: $innerMapper: '@EzSystems\EzPlatformQueryFieldType\GraphQL\ContentQueryFieldDefinitionMapper.inner' + tags: + - { name: ezplatform_graphql.field_definition_args_builder_mapper, fieldtype: 'ezcontentquery' } diff --git a/src/Symfony/Resources/views/content/contentquery.html.twig b/src/Symfony/Resources/views/content/contentquery.html.twig index f47e5a4..628b8e9 100644 --- a/src/Symfony/Resources/views/content/contentquery.html.twig +++ b/src/Symfony/Resources/views/content/contentquery.html.twig @@ -6,3 +6,7 @@ "viewType": itemViewType })) }} {% endfor %} + +{% if isPaginationEnabled %} + {{ pagerfanta( items, 'ez', {'routeName': location, 'pageParameter': pageParameter } ) }} +{% endif %} diff --git a/src/Symfony/Resources/views/fieldtype/field_view.html.twig b/src/Symfony/Resources/views/fieldtype/field_view.html.twig index 9a3219d..c3d46ae 100644 --- a/src/Symfony/Resources/views/fieldtype/field_view.html.twig +++ b/src/Symfony/Resources/views/fieldtype/field_view.html.twig @@ -7,7 +7,10 @@ "contentId": contentInfo.id, "queryFieldDefinitionIdentifier": field.fieldDefIdentifier, "viewType": ezContentQueryViews['field'], - "itemViewType": itemViewType|default(ezContentQueryViews['item']) + "itemViewType": itemViewType|default(ezContentQueryViews['item']), + "enablePagination": parameters.enablePagination|default(false), + "disablePagination": parameters.disablePagination|default(false), + "itemsPerPage": parameters.itemsPerPage|default(false) } )) }} {% endblock %} diff --git a/src/Symfony/Resources/views/fieldtype/fielddefinition_edit.html.twig b/src/Symfony/Resources/views/fieldtype/fielddefinition_edit.html.twig index bcabc5f..4f357a2 100644 --- a/src/Symfony/Resources/views/fieldtype/fielddefinition_edit.html.twig +++ b/src/Symfony/Resources/views/fieldtype/fielddefinition_edit.html.twig @@ -13,6 +13,18 @@ {{- form_widget(form.ReturnedType) -}} +
+ {{- form_label(form.EnablePagination) -}} + {{- form_errors(form.EnablePagination) -}} + {{- form_widget(form.EnablePagination) -}} +
+ +
+ {{- form_label(form.ItemsPerPage) -}} + {{- form_errors(form.ItemsPerPage) -}} + {{- form_widget(form.ItemsPerPage) -}} +
+
{{- form_label(form.Parameters) -}} {{- form_errors(form.Parameters) -}} diff --git a/src/eZ/ContentView/QueryResultsInjector.php b/src/eZ/ContentView/QueryResultsInjector.php index 5837859..9a3a364 100644 --- a/src/eZ/ContentView/QueryResultsInjector.php +++ b/src/eZ/ContentView/QueryResultsInjector.php @@ -8,8 +8,11 @@ use eZ\Publish\Core\MVC\Symfony\View\Event\FilterViewParametersEvent; use eZ\Publish\Core\MVC\Symfony\View\ViewEvents; +use EzSystems\EzPlatformQueryFieldType\API\QueryFieldPaginationService; use EzSystems\EzPlatformQueryFieldType\API\QueryFieldService; +use Pagerfanta\Pagerfanta; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\RequestStack; class QueryResultsInjector implements EventSubscriberInterface { @@ -19,13 +22,18 @@ class QueryResultsInjector implements EventSubscriberInterface /** @var array */ private $views; - public function __construct(QueryFieldService $queryFieldService, array $views) + /** @var \Symfony\Component\HttpFoundation\RequestStack */ + private $requestStack; + + public function __construct(QueryFieldService $queryFieldService, array $views, RequestStack $requestStack) { - $this->queryFieldService = $queryFieldService; if (!isset($views['item']) || !isset($views['field'])) { throw new \InvalidArgumentException("Both 'item' and 'field' views must be provided"); } + + $this->queryFieldService = $queryFieldService; $this->views = $views; + $this->requestStack = $requestStack; } public static function getSubscribedEvents() @@ -41,14 +49,91 @@ public function injectQueryResults(FilterViewParametersEvent $event) $viewType = $event->getView()->getViewType(); if ($viewType === $this->views['field']) { - $event->getParameterBag()->add([ + $parameters = [ 'itemViewType' => $this->views['item'], - 'items' => $this->queryFieldService->loadContentItems( - $event->getView()->getContent(), - // @todo error handling if parameter not set - $event->getBuilderParameters()['queryFieldDefinitionIdentifier'] - ), - ]); + 'items' => $this->buildResults($event), + ]; + $parameters['isPaginationEnabled'] = ($parameters['items'] instanceof Pagerfanta); + if ($parameters['isPaginationEnabled']) { + $fieldDefinitionIdentifier = $event->getBuilderParameters()['queryFieldDefinitionIdentifier']; + $parameters['pageParameter'] = sprintf('[%s_page]', $fieldDefinitionIdentifier); + } + $event->getParameterBag()->add($parameters); + } + } + + /** + * @param \eZ\Publish\Core\MVC\Symfony\View\Event\FilterViewParametersEvent $event + * + * @return iterable + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + protected function buildResults(FilterViewParametersEvent $event): iterable + { + $view = $event->getView(); + $content = $view->getContent(); + $viewParameters = $event->getBuilderParameters(); + $fieldDefinitionIdentifier = $viewParameters['queryFieldDefinitionIdentifier']; + + $paginationLimit = false; + + if ($this->queryFieldService instanceof QueryFieldPaginationService) { + $paginationLimit = $this->queryFieldService->getPaginationConfiguration($content, $fieldDefinitionIdentifier); + } + + $enablePagination = ($viewParameters['enablePagination'] === true); + $disablePagination = ($viewParameters['disablePagination'] === true); + + if ($enablePagination === true && $disablePagination === true) { + // @todo custom exception + throw new \InvalidArgumentException("the 'enablePagination' and 'disablePagination' parameters can not both be true"); + } + + if (is_numeric($viewParameters['itemsPerPage'])) { + // @todo custom exception + if ($viewParameters['itemsPerPage'] <= 0) { + throw new \InvalidArgumentException('itemsPerPage must be a positive integer'); + } + $paginationLimit = $viewParameters['itemsPerPage']; + } + + if (($enablePagination === true) && (!is_numeric($paginationLimit) || $paginationLimit === 0)) { + throw new \InvalidArgumentException("The 'itemsPerPage' parameter must be given with a positive integer value if 'enablePagination' is set"); + } + + if ($paginationLimit !== 0 && $disablePagination !== true) { + if (!$this->queryFieldService instanceof QueryFieldPaginationService) { + throw new \Exception( + "Pagination was requested, but the QueryFieldService isn't an instance of %s", + QueryFieldPaginationService::class + ); + } + + $request = $this->requestStack->getMasterRequest(); + + $queryParameters = $view->hasParameter('query') ? $view->getParameter('query') : []; + + $limit = $queryParameters['limit'] ?? $paginationLimit; + $pageParam = sprintf('%s_page', $fieldDefinitionIdentifier); + $page = isset($request) ? $request->get($pageParam, 1) : 1; + + $pager = new Pagerfanta( + new QueryResultsPagerFantaAdapter( + $this->queryFieldService, $content, $fieldDefinitionIdentifier + ) + ); + + $pager->setMaxPerPage($limit); + $pager->setCurrentPage($page); + + return $pager; + } else { + // @todo error handling if parameter not set + return $this->queryFieldService->loadContentItems( + $content, + $fieldDefinitionIdentifier + ); } } } diff --git a/src/eZ/ContentView/QueryResultsPagerFantaAdapter.php b/src/eZ/ContentView/QueryResultsPagerFantaAdapter.php new file mode 100644 index 0000000..076d68a --- /dev/null +++ b/src/eZ/ContentView/QueryResultsPagerFantaAdapter.php @@ -0,0 +1,51 @@ +queryFieldService = $queryFieldService; + $this->content = $content; + $this->fieldDefinitionIdentifier = $fieldDefinitionIdentifier; + } + + public function getNbResults() + { + return $this->queryFieldService->countContentItems( + $this->content, + $this->fieldDefinitionIdentifier + ); + } + + public function getSlice($offset, $length) + { + return $this->queryFieldService->loadContentItemsSlice( + $this->content, + $this->fieldDefinitionIdentifier, + $offset, + $length + ); + } +} diff --git a/src/eZ/FieldType/Mapper/QueryFormMapper.php b/src/eZ/FieldType/Mapper/QueryFormMapper.php index fad96d5..04948be 100644 --- a/src/eZ/FieldType/Mapper/QueryFormMapper.php +++ b/src/eZ/FieldType/Mapper/QueryFormMapper.php @@ -63,6 +63,19 @@ public function mapFieldDefinitionForm(FormInterface $fieldDefinitionForm, Field 'required' => true, ] ) + ->add('EnablePagination', Type\CheckboxType::class, + [ + 'label' => 'Enable pagination', + 'property_path' => 'fieldSettings[EnablePagination]', + 'required' => false, + ] + ) + ->add('ItemsPerPage', Type\NumberType::class, + [ + 'label' => 'Items per page', + 'property_path' => 'fieldSettings[ItemsPerPage]', + ] + ) ->add($parametersForm); } diff --git a/src/eZ/FieldType/Query/Type.php b/src/eZ/FieldType/Query/Type.php index ab955df..1259b71 100644 --- a/src/eZ/FieldType/Query/Type.php +++ b/src/eZ/FieldType/Query/Type.php @@ -25,6 +25,8 @@ final class Type extends FieldType 'QueryType' => ['type' => 'string', 'default' => ''], 'Parameters' => ['type' => 'array', 'default' => []], 'ReturnedType' => ['type' => 'string', 'default' => ''], + 'EnablePagination' => ['type' => 'boolean', 'default' => true], + 'ItemsPerPage' => ['type' => 'integer', 'default' => 10], ]; /** @var \eZ\Publish\Core\QueryType\QueryTypeRegistry */ @@ -215,6 +217,18 @@ public function validateFieldSettings($fieldSettings) } } + if (isset($fieldSettings['EnablePagination'])) { + if (!is_bool($fieldSettings['EnablePagination'])) { + $errors[] = new ValidationError('EnablePagination is not a boolean'); + } + } + + if (isset($fieldSettings['ItemsPerPage'])) { + if (!is_numeric($fieldSettings['ItemsPerPage'])) { + $errors[] = new ValidationError('ItemsPerPage is not an integer'); + } + } + if (isset($fieldSettings['Parameters'])) { if (!is_array($fieldSettings['Parameters'])) { $errors[] = new ValidationError('Parameters is not a valid YAML string'); diff --git a/src/eZ/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php b/src/eZ/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php index a083efd..6e31662 100644 --- a/src/eZ/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php +++ b/src/eZ/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php @@ -63,6 +63,8 @@ public function toStorageFieldDefinition(FieldDefinition $fieldDef, StorageField $storageDef->dataText1 = $fieldDef->fieldTypeConstraints->fieldSettings['QueryType']; $storageDef->dataText2 = $fieldDef->fieldTypeConstraints->fieldSettings['ReturnedType']; $storageDef->dataText5 = \json_encode($fieldDef->fieldTypeConstraints->fieldSettings['Parameters']); + $storageDef->dataInt1 = (int)$fieldDef->fieldTypeConstraints->fieldSettings['EnablePagination']; + $storageDef->dataInt2 = $fieldDef->fieldTypeConstraints->fieldSettings['ItemsPerPage']; } /** @@ -77,6 +79,8 @@ public function toFieldDefinition(StorageFieldDefinition $storageDef, FieldDefin 'QueryType' => $storageDef->dataText1 ?: null, 'ReturnedType' => $storageDef->dataText2 ?: null, 'Parameters' => \json_decode($storageDef->dataText5, true), + 'EnablePagination' => (bool)$storageDef->dataInt1, + 'ItemsPerPage' => $storageDef->dataInt2, ]; }