Skip to content

Commit a314825

Browse files
author
Bertrand Dunogier
authored
Merge pull request #20 from ezsystems/ezp31316-pagination_support-1.0
Two new settings are added to the field type: - a boolean, `EnablePagination` - an integer, `ItemsPerPage` # PHP The `QueryFieldService` is extended with a `QueryFieldPaginationService` interface, with two new methods: - `loadContentItemsSlice(Content $content, string $fieldDefinitionIdentifier, int $offset, int $limit)`: loads a slice of the results - `getPaginationConfiguration(Content $content, string $fieldDefinitionIdentifier)`: returns either 0 if pagination is disabled, or the default page size # Twig When pagination is enabled for a field, the `QueryResultsInjector` (that adds the Query Field's results to the content view) will set `items` to a `PagerFanta` object (that uses a `QueryResultsPagerFantaAdapter`). In addition, it will set two extra variables: - `bool isPaginationEnabled`: used to conditionnally show the pager - `string pageParameter`: set to the unique page parameter used by the pager. It is named after the field definition identifier. Example: `"[images_page]"`. # REST The REST route for query field results now supports offset and limit parameters. # GraphQL If a field definition has pagination enabled, the field will return a `Connection` of that type, with the usual arguments (`first`, `last`, `after`, `before`). If no parameter is specified, it will use the items per page option from the field definition.
2 parents 642b225 + d342d21 commit a314825

File tree

17 files changed

+406
-43
lines changed

17 files changed

+406
-43
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"require": {
1818
"php": ">=7.1",
1919
"ext-json": "*",
20-
"ezsystems/ezplatform-graphql": "^1.0||^2.0",
20+
"ezsystems/ezplatform-graphql": "^1.0@dev",
2121
"ezsystems/ezpublish-kernel": "^7.0||^8.0"
2222
},
2323
"autoload": {

doc/howto/customize_rendering.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ In this how-to, you will learn how to customize all the rendering phases.
44

55
## Rendering process
66
Content query fields are rendered using `ez_render_field()`.
7-
The query is executed, the items iterated on, and each is be displayed using the `line` content view.
7+
The query is executed, the items iterated on, and each is rendered using the `line` content view.
88

99
That template renders the content, the view controller, with a custom view type (`content_query_view`). A custom view
1010
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.
1111

12+
The field renderer for a query field supports the following parameters:
13+
- `bool enablePagination`: force pagination enabled, even if it is disabled for that field definition
14+
- `bool disablePagination`: force pagination disabled, even if it is disabled for that field definition
15+
- `int itemsPerPage`: sets how many items are displayed per page with pagination. Required if `enablePagination` is
16+
used and pagination is disabled in the field definition
17+
1218
### Summary
1319
1. Your template: `ez_render_field(content, 'queryfield')`
1420
2. Field view template: `fieldtype_ui.html.twig`:
@@ -41,16 +47,27 @@ As with any content view, a custom controller can also be defined to further cus
4147
4248
Use the `items` iterable to loop over the field's content items:
4349
```
50+
<div class="my-list">
4451
{% for item in items %}
4552
{{ render(controller("ez_content:viewAction", {
4653
"contentId": item.id,
4754
"content": item,
4855
"viewType": itemViewType
4956
})) }}
5057
{% endfor %}
58+
</div>
59+
60+
{% if isPaginationEnabled %}
61+
{{ pagerfanta( items, 'ez', {'routeName': location, 'pageParameter': pageParameter } ) }}
62+
{% endif %}
5163
```
5264
53-
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.
65+
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:
66+
- `Content content`: the item that contains the query field.
67+
- `bool isPaginationEnabled`: indicates if pagination is enabled. When it is, `items` is a `PagerFanta` instance.
68+
- `string pageParameter`: when pagination is enabled, contains the page parameter to use as the pager fanta
69+
`pageParameter` argument (important, as it makes every pager unique, required if there are several query
70+
fields in the same item)
5471
5572
## Customizing the line view template
5673
The line view template, used to render each result, can be customized by creating `line` view configuration rules.
@@ -69,7 +86,7 @@ ezplatform:
6986
template: "path/to/template.html.twig"
7087
```
7188

72-
The variables are the same as any view template ([documentation]((https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables))).
89+
The variables are the same than other view template ([documentation]((https://doc.ezplatform.com/en/latest/api/field_type_form_and_template/#template-variables))).
7390

7491
## Advanced
7592

spec/GraphQL/ContentQueryFieldDefinitionMapperSpec.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,38 @@ function it_delegates_field_definition_type_to_the_parent_mapper_for_a_non_query
7979
->shouldBe('FieldValue');
8080
}
8181

82-
function it_delegates_the_value_resolver_to_the_parent_mapper(FieldDefinitionMapper $innerMapper)
82+
function it_maps_the_field_value_when_pagination_is_disabled(FieldDefinitionMapper $innerMapper)
8383
{
8484
$fieldDefinition = $this->fieldDefinition();
85-
$innerMapper->mapToFieldValueResolver($fieldDefinition)->willReturn('resolver');
85+
$innerMapper->mapToFieldValueResolver($fieldDefinition)->shouldNotBeCalled();
8686
$this
8787
->mapToFieldValueResolver($fieldDefinition)
88-
->shouldBe('resolver');
88+
->shouldBe('@=resolver("QueryFieldValue", [field, content])');
89+
}
90+
91+
function it_maps_the_field_value_when_pagination_is_enabled(FieldDefinitionMapper $innerMapper)
92+
{
93+
$fieldDefinition = $this->fieldDefinition(true);
94+
$innerMapper->mapToFieldValueResolver($fieldDefinition)->shouldNotBeCalled();
95+
$this
96+
->mapToFieldValueResolver($fieldDefinition)
97+
->shouldBe('@=resolver("QueryFieldValueConnection", [args, field, content])');
8998
}
9099

91100
/**
101+
* @param bool $enablePagination
102+
*
92103
* @return FieldDefinition
93104
*/
94-
private function fieldDefinition(): FieldDefinition
105+
private function fieldDefinition(bool $enablePagination = false): FieldDefinition
95106
{
96107
return new FieldDefinition([
97108
'identifier' => self::FIELD_IDENTIFIER,
98109
'fieldTypeIdentifier' => self::FIELD_TYPE_IDENTIFIER,
99-
'fieldSettings' => ['ReturnedType' => self::RETURNED_CONTENT_TYPE_IDENTIFIER]
110+
'fieldSettings' => [
111+
'ReturnedType' => self::RETURNED_CONTENT_TYPE_IDENTIFIER,
112+
'EnablePagination' => $enablePagination
113+
]
100114
]);
101115
}
102116

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
namespace EzSystems\EzPlatformQueryFieldType\API;
8+
9+
use eZ\Publish\API\Repository\Values\Content\Content;
10+
11+
/**
12+
* Pagination related methods for v1.0.
13+
*
14+
* @deprecated since 1.0, will be part of the regular QueryFieldService interface in 2.0.
15+
*/
16+
interface QueryFieldPaginationService
17+
{
18+
public function getPaginationConfiguration(Content $content, string $fieldDefinitionIdentifier): int;
19+
20+
public function loadContentItemsSlice(Content $content, string $fieldDefinitionIdentifier, int $offset, int $limit): iterable;
21+
}

src/API/QueryFieldService.php

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
use eZ\Publish\API\Repository\Values\Content\Content;
1313
use eZ\Publish\API\Repository\Values\Content\Query;
1414
use eZ\Publish\API\Repository\Values\Content\Search\SearchHit;
15+
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
1516
use eZ\Publish\Core\QueryType\QueryTypeRegistry;
1617
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
1718

1819
/**
1920
* Executes a query and returns the results.
2021
*/
21-
final class QueryFieldService implements QueryFieldServiceInterface
22+
final class QueryFieldService implements QueryFieldServiceInterface, QueryFieldPaginationService
2223
{
2324
/** @var \eZ\Publish\Core\QueryType\QueryTypeRegistry */
2425
private $queryTypeRegistry;
@@ -73,23 +74,48 @@ public function countContentItems(Content $content, string $fieldDefinitionIdent
7374
return $this->searchService->findContent($query)->totalCount;
7475
}
7576

77+
public function loadContentItemsSlice(Content $content, string $fieldDefinitionIdentifier, int $offset, int $limit): iterable
78+
{
79+
$query = $this->prepareQuery($content, $fieldDefinitionIdentifier);
80+
$query->offset = $offset;
81+
$query->limit = $limit;
82+
83+
return array_map(
84+
function (SearchHit $searchHit) {
85+
return $searchHit->valueObject;
86+
},
87+
$this->searchService->findContent($query)->searchHits
88+
);
89+
}
90+
91+
public function getPaginationConfiguration(Content $content, string $fieldDefinitionIdentifier): int
92+
{
93+
$fieldDefinition = $this->loadFieldDefinition($content, $fieldDefinitionIdentifier);
94+
95+
if ($fieldDefinition->fieldSettings['EnablePagination'] === false) {
96+
return false;
97+
}
98+
99+
return $fieldDefinition->fieldSettings['ItemsPerPage'];
100+
}
101+
76102
/**
77-
* @param array $parameters parameters that may include expressions to be resolved
78-
* @param \eZ\Publish\API\Repository\Values\Content\Content $content
103+
* @param array $expressions parameters that may include expressions to be resolved
104+
* @param array $variables
79105
*
80106
* @return array
81107
*/
82-
private function resolveParameters(array $parameters, array $variables): array
108+
private function resolveParameters(array $expressions, array $variables): array
83109
{
84-
foreach ($parameters as $key => $expression) {
110+
foreach ($expressions as $key => $expression) {
85111
if (is_array($expression)) {
86-
$parameters[$key] = $this->resolveParameters($expression, $variables);
112+
$expressions[$key] = $this->resolveParameters($expression, $variables);
87113
} else {
88-
$parameters[$key] = $this->resolveExpression($expression, $variables);
114+
$expressions[$key] = $this->resolveExpression($expression, $variables);
89115
}
90116
}
91117

92-
return $parameters;
118+
return $expressions;
93119
}
94120

95121
private function resolveExpression(string $expression, array $variables)
@@ -110,24 +136,40 @@ private function resolveExpression(string $expression, array $variables)
110136
* @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
111137
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
112138
*/
113-
private function prepareQuery(Content $content, string $fieldDefinitionIdentifier): Query
139+
private function prepareQuery(Content $content, string $fieldDefinitionIdentifier, array $extraParameters = []): Query
114140
{
115-
$fieldDefinition = $this
116-
->contentTypeService->loadContentType($content->contentInfo->contentTypeId)
117-
->getFieldDefinition($fieldDefinitionIdentifier);
141+
$fieldDefinition = $this->loadFieldDefinition($content, $fieldDefinitionIdentifier);
118142

119143
$location = $this->locationService->loadLocation($content->contentInfo->mainLocationId);
120144
$queryType = $this->queryTypeRegistry->getQueryType($fieldDefinition->fieldSettings['QueryType']);
121145
$parameters = $this->resolveParameters(
122146
$fieldDefinition->fieldSettings['Parameters'],
123-
[
124-
'content' => $content,
125-
'contentInfo' => $content->contentInfo,
126-
'mainLocation' => $location,
127-
'returnedType' => $fieldDefinition->fieldSettings['ReturnedType'],
128-
]
147+
array_merge(
148+
$extraParameters,
149+
[
150+
'content' => $content,
151+
'contentInfo' => $content->contentInfo,
152+
'mainLocation' => $location,
153+
'returnedType' => $fieldDefinition->fieldSettings['ReturnedType'],
154+
]
155+
)
129156
);
130157

131158
return $queryType->getQuery($parameters);
132159
}
160+
161+
/**
162+
* @param \eZ\Publish\API\Repository\Values\Content\Content $content
163+
* @param string $fieldDefinitionIdentifier
164+
*
165+
* @return \eZ\Publish\API\Repository\Values\ContentType\FieldDefinition|null
166+
*
167+
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
168+
*/
169+
private function loadFieldDefinition(Content $content, string $fieldDefinitionIdentifier): FieldDefinition
170+
{
171+
return $fieldDefinition = $this
172+
->contentTypeService->loadContentType($content->contentInfo->contentTypeId)
173+
->getFieldDefinition($fieldDefinitionIdentifier);
174+
}
133175
}

src/Controller/QueryFieldRestController.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
1616
use eZ\Publish\Core\REST\Server\Values\ContentList;
1717
use eZ\Publish\Core\REST\Server\Values\RestContent;
18+
use Symfony\Component\HttpFoundation\Request;
1819

1920
final class QueryFieldRestController
2021
{
@@ -42,11 +43,19 @@ public function __construct(
4243
$this->locationService = $locationService;
4344
}
4445

45-
public function getResults($contentId, $versionNumber, $fieldDefinitionIdentifier): ContentList
46+
public function getResults(Request $request, $contentId, $versionNumber, $fieldDefinitionIdentifier): ContentList
4647
{
48+
$offset = (int)$request->query->get('offset', 0);
49+
$limit = (int)$request->query->get('limit', -1);
50+
4751
$content = $this->contentService->loadContent($contentId, null, $versionNumber);
52+
if ($limit === -1 || !method_exists($this->queryFieldService, 'loadContentItemsSlice')) {
53+
$items = $this->queryFieldService->loadContentItems($content, $fieldDefinitionIdentifier);
54+
} else {
55+
$items = $this->queryFieldService->loadContentItemsSlice($content, $fieldDefinitionIdentifier, $offset, $limit);
56+
}
4857

49-
return new ContentList(
58+
$list = new ContentList(
5059
array_map(
5160
function (Content $content) {
5261
return new RestContent(
@@ -57,9 +66,15 @@ function (Content $content) {
5766
$this->contentService->loadRelations($content->getVersionInfo())
5867
);
5968
},
60-
$this->queryFieldService->loadContentItems($content, $fieldDefinitionIdentifier)
69+
$items
6170
)
6271
);
72+
73+
if (property_exists($list, 'totalCount')) {
74+
$list->totalCount = $this->queryFieldService->countContentItems($content, $fieldDefinitionIdentifier);
75+
}
76+
77+
return $list;
6378
}
6479

6580
private function getContentType(ContentInfo $contentInfo): ContentType

src/GraphQL/ContentQueryFieldDefinitionMapper.php

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
use eZ\Publish\API\Repository\ContentTypeService;
1010
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
1111
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\DecoratingFieldDefinitionMapper;
12+
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionArgsBuilderMapper;
1213
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper;
1314
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\NameHelper;
1415

15-
final class ContentQueryFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper
16+
final class ContentQueryFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper, FieldDefinitionArgsBuilderMapper
1617
{
1718
/** @var NameHelper */
1819
private $nameHelper;
@@ -38,7 +39,26 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string
3839

3940
$fieldSettings = $fieldDefinition->getFieldSettings();
4041

41-
return '[' . $this->getDomainTypeName($fieldSettings['ReturnedType']) . ']';
42+
if ($fieldSettings['EnablePagination']) {
43+
return $this->nameValueConnectionType($fieldSettings['ReturnedType']);
44+
} else {
45+
return '[' . $this->nameValueType($fieldSettings['ReturnedType']) . ']';
46+
}
47+
}
48+
49+
public function mapToFieldValueResolver(FieldDefinition $fieldDefinition): ?string
50+
{
51+
if (!$this->canMap($fieldDefinition)) {
52+
return parent::mapToFieldValueType($fieldDefinition);
53+
}
54+
55+
$fieldSettings = $fieldDefinition->getFieldSettings();
56+
57+
if ($fieldSettings['EnablePagination']) {
58+
return '@=resolver("QueryFieldValueConnection", [args, field, content])';
59+
} else {
60+
return '@=resolver("QueryFieldValue", [field, content])';
61+
}
4262
}
4363

4464
public function mapToFieldDefinitionType(FieldDefinition $fieldDefinition): ?string
@@ -50,15 +70,35 @@ public function mapToFieldDefinitionType(FieldDefinition $fieldDefinition): ?str
5070
return 'ContentQueryFieldDefinition';
5171
}
5272

73+
public function mapToFieldValueArgsBuilder(FieldDefinition $fieldDefinition): ?string
74+
{
75+
if (!$this->canMap($fieldDefinition)) {
76+
return parent::mapToFieldValueArgsBuilder($fieldDefinition);
77+
}
78+
79+
if ($fieldDefinition->fieldSettings['EnablePagination']) {
80+
return 'Relay::Connection';
81+
} else {
82+
return null;
83+
}
84+
}
85+
5386
protected function getFieldTypeIdentifier(): string
5487
{
5588
return 'ezcontentquery';
5689
}
5790

58-
private function getDomainTypeName($typeIdentifier)
91+
private function nameValueType($typeIdentifier): string
5992
{
6093
return $this->nameHelper->domainContentName(
6194
$this->contentTypeService->loadContentTypeByIdentifier($typeIdentifier)
6295
);
6396
}
97+
98+
private function nameValueConnectionType($typeIdentifier): string
99+
{
100+
return $this->nameHelper->domainContentConnection(
101+
$this->contentTypeService->loadContentTypeByIdentifier($typeIdentifier)
102+
);
103+
}
64104
}

0 commit comments

Comments
 (0)