Skip to content

Commit 4256023

Browse files
authored
Merge pull request #3 from itk-dev/feature/418-filter-and-pagination
418: Added paginator to search results
2 parents b9df9df + 3d148d0 commit 4256023

14 files changed

+292
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ See [keep a changelog] for information about writing changes to this log.
1515
- Added basic index service.
1616
- Basic events DTO added.
1717
- Added tags filter
18+
- Added pagination and match filter
1819

1920
[keep a changelog]: https://keepachangelog.com/en/1.1.0/
2021
[unreleased]: https://github.com/itk-dev/event-database-imports/compare/main...develop

baseline.xml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
</file>
88
<file src="src/Api/State/EventRepresentationProvider.php">
99
<InvalidReturnStatement>
10-
<code><![CDATA[$this->index->getAll(IndexNames::Events->value, $filters, $from, self::PAGE_SIZE)]]></code>
10+
<code><![CDATA[[$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]]]></code>
1111
</InvalidReturnStatement>
1212
<MissingTemplateParam>
1313
<code>ProviderInterface</code>
1414
</MissingTemplateParam>
1515
</file>
1616
<file src="src/Api/State/OrganizationRepresentationProvider.php">
1717
<InvalidReturnStatement>
18-
<code><![CDATA[$this->index->getAll(IndexNames::Organization->value, $filters, $from, self::PAGE_SIZE)]]></code>
1918
<code><![CDATA[[$this->index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']]]]></code>
2019
</InvalidReturnStatement>
2120
<MissingTemplateParam>
@@ -51,10 +50,16 @@
5150
<code>\HttpException</code>
5251
</UndefinedDocblockClass>
5352
</file>
54-
<file src="src/Service/ElasticSearchIndex.php">
53+
<file src="src/Service/ElasticSearch/ElasticSearchIndex.php">
5554
<InvalidArgument>
5655
<code>$params</code>
5756
<code><![CDATA[['name' => $indexName]]]></code>
5857
</InvalidArgument>
5958
</file>
59+
<file src="src/Service/ElasticSearch/ElasticSearchPaginator.php">
60+
<MissingTemplateParam>
61+
<code>PaginatorInterface</code>
62+
<code>\IteratorAggregate</code>
63+
</MissingTemplateParam>
64+
</file>
6065
</files>

config/packages/api_platform.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ api_platform:
88

99
docs_formats:
1010
jsonld: ['application/ld+json']
11-
jsonopenapi: ['application/vnd.openapi+json']
1211
html: ['text/html']
1312

1413
mapping:

public/spec.yaml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ paths:
4343
style: form
4444
explode: false
4545
allowReserved: false
46+
-
47+
name: itemsPerPage
48+
in: query
49+
description: 'The number of items per page'
50+
required: false
51+
deprecated: false
52+
allowEmptyValue: true
53+
schema:
54+
type: integer
55+
default: 10
56+
minimum: 0
57+
maximum: 50
58+
style: form
59+
explode: false
60+
allowReserved: false
4661
-
4762
name: tags
4863
in: query
@@ -119,6 +134,33 @@ paths:
119134
style: form
120135
explode: false
121136
allowReserved: false
137+
-
138+
name: itemsPerPage
139+
in: query
140+
description: 'The number of items per page'
141+
required: false
142+
deprecated: false
143+
allowEmptyValue: true
144+
schema:
145+
type: integer
146+
default: 20
147+
minimum: 0
148+
maximum: 100
149+
style: form
150+
explode: false
151+
allowReserved: false
152+
-
153+
name: name
154+
in: query
155+
description: 'Search field based on value given'
156+
required: false
157+
deprecated: false
158+
allowEmptyValue: true
159+
schema:
160+
type: string
161+
style: form
162+
explode: false
163+
allowReserved: false
122164
deprecated: false
123165
parameters: []
124166
'/api/v2/organizations/{id}':
@@ -129,7 +171,7 @@ paths:
129171
responses:
130172
200:
131173
description: 'Single organization'
132-
summary: 'Get single organization base on identifier'
174+
summary: 'Get single organization based on identifier'
133175
description: 'Retrieves a Organization resource.'
134176
parameters:
135177
-

src/Api/Dto/Event.php

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,38 @@
1010
use App\Api\Filter\EventTagFilter;
1111
use App\Api\State\EventRepresentationProvider;
1212

13-
#[ApiResource(operations: [
14-
new Get(
15-
openapiContext: [
16-
'parameters' => [
17-
[
18-
'name' => 'id',
19-
'in' => 'path',
20-
'required' => true,
21-
'schema' => [
22-
'type' => 'integer',
13+
#[ApiResource(
14+
operations: [
15+
new Get(
16+
openapiContext: [
17+
'parameters' => [
18+
[
19+
'name' => 'id',
20+
'in' => 'path',
21+
'required' => true,
22+
'schema' => [
23+
'type' => 'integer',
24+
],
2325
],
2426
],
25-
],
26-
'responses' => [
27-
'200' => [
28-
'description' => 'Single event',
27+
'responses' => [
28+
'200' => [
29+
'description' => 'Single event',
30+
],
2931
],
3032
],
31-
],
32-
output: EventRepresentationProvider::class,
33-
provider: EventRepresentationProvider::class,
34-
),
35-
new GetCollection(
36-
output: EventRepresentationProvider::class,
37-
provider: EventRepresentationProvider::class,
38-
),
39-
])]
33+
output: EventRepresentationProvider::class,
34+
provider: EventRepresentationProvider::class,
35+
),
36+
new GetCollection(
37+
output: EventRepresentationProvider::class,
38+
provider: EventRepresentationProvider::class,
39+
),
40+
],
41+
paginationClientItemsPerPage: true,
42+
paginationItemsPerPage: 10,
43+
paginationMaximumItemsPerPage: 50
44+
)]
4045
#[ApiFilter(
4146
EventTagFilter::class,
4247
properties: ['tags']

src/Api/Dto/Organization.php

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,51 @@
22

33
namespace App\Api\Dto;
44

5+
use ApiPlatform\Metadata\ApiFilter;
56
use ApiPlatform\Metadata\ApiProperty;
67
use ApiPlatform\Metadata\ApiResource;
78
use ApiPlatform\Metadata\Get;
89
use ApiPlatform\Metadata\GetCollection;
10+
use App\Api\Filter\MatchFilter;
911
use App\Api\State\OrganizationRepresentationProvider;
1012

11-
#[ApiResource(operations: [
12-
new Get(
13-
openapiContext: [
14-
'summary' => 'Get single organization base on identifier',
15-
'parameters' => [
16-
[
17-
'name' => 'id',
18-
'in' => 'path',
19-
'required' => true,
20-
'schema' => [
21-
'type' => 'integer',
13+
#[ApiResource(
14+
operations: [
15+
new Get(
16+
openapiContext: [
17+
'summary' => 'Get single organization based on identifier',
18+
'parameters' => [
19+
[
20+
'name' => 'id',
21+
'in' => 'path',
22+
'required' => true,
23+
'schema' => [
24+
'type' => 'integer',
25+
],
2226
],
2327
],
24-
],
25-
'responses' => [
26-
'200' => [
27-
'description' => 'Single organization',
28+
'responses' => [
29+
'200' => [
30+
'description' => 'Single organization',
31+
],
2832
],
2933
],
30-
],
31-
output: OrganizationRepresentationProvider::class,
32-
provider: OrganizationRepresentationProvider::class,
33-
),
34-
new GetCollection(
35-
output: OrganizationRepresentationProvider::class,
36-
provider: OrganizationRepresentationProvider::class,
37-
),
38-
])]
34+
output: OrganizationRepresentationProvider::class,
35+
provider: OrganizationRepresentationProvider::class,
36+
),
37+
new GetCollection(
38+
output: OrganizationRepresentationProvider::class,
39+
provider: OrganizationRepresentationProvider::class,
40+
),
41+
],
42+
paginationClientItemsPerPage: true,
43+
paginationItemsPerPage: 20,
44+
paginationMaximumItemsPerPage: 100
45+
)]
46+
#[ApiFilter(
47+
MatchFilter::class,
48+
properties: ['name']
49+
)]
3950
class Organization
4051
{
4152
#[ApiProperty(

src/Api/Filter/MatchFilter.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Api\Filter;
4+
5+
use ApiPlatform\Elasticsearch\Filter\AbstractFilter;
6+
use ApiPlatform\Metadata\Operation;
7+
use Symfony\Component\PropertyInfo\Type;
8+
9+
/**
10+
* This class represents a filter that performs a search based on matching properties in a given resource.
11+
*/
12+
final class MatchFilter extends AbstractFilter
13+
{
14+
public function apply(array $clauseBody, string $resourceClass, Operation $operation = null, array $context = []): array
15+
{
16+
$properties = $this->getProperties($resourceClass);
17+
$matches = [];
18+
19+
/** @var string $property */
20+
foreach ($properties as $property) {
21+
$matches[] = ['match' => [$property => $context['filters'][$property]]];
22+
}
23+
24+
return isset($matches[1]) ? ['bool' => ['should' => $matches]] : $matches[0];
25+
}
26+
27+
public function getDescription(string $resourceClass): array
28+
{
29+
if (!$this->properties) {
30+
return [];
31+
}
32+
33+
$description = [];
34+
foreach ($this->properties as $filterParameterName => $value) {
35+
$description[$filterParameterName] = [
36+
'property' => $filterParameterName,
37+
'type' => Type::BUILTIN_TYPE_STRING,
38+
'required' => false,
39+
'description' => 'Search field based on value given',
40+
'openapi' => [
41+
'allowReserved' => false,
42+
'allowEmptyValue' => true,
43+
'explode' => false,
44+
],
45+
];
46+
}
47+
48+
return $description;
49+
}
50+
}

src/Api/State/AbstractProvider.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
abstract class AbstractProvider
1717
{
18-
protected const PAGE_SIZE = 10;
18+
protected const PAGE_SIZE_FALLBACK = 10;
1919

2020
public function __construct(
2121
protected readonly IndexInterface $index,
@@ -70,14 +70,28 @@ protected function getFilters(Operation $operation, array $context = []): array
7070
* Calculates the offset for a paginated result based on the provided context.
7171
*
7272
* @param array $context
73-
* The context containing the pagination filters
73+
* The context containing the pagination information
7474
*
7575
* @return int
7676
* The calculated offset value
7777
*/
7878
protected function calculatePageOffset(array $context): int
7979
{
80-
return (($context['filters']['page'] ?? 1) - 1) * self::PAGE_SIZE;
80+
return (($context['filters']['page'] ?? 1) - 1) * $this->getImagesPerPage($context);
81+
}
82+
83+
/**
84+
* Retrieves the number of items per page.
85+
*
86+
* @param array $context
87+
* The context containing the pagination information
88+
*
89+
* @return int
90+
* The number of items per page as determined by the context
91+
*/
92+
protected function getImagesPerPage(array $context): int
93+
{
94+
return $context['filters']['itemsPerPage'] ?? self::PAGE_SIZE_FALLBACK;
8195
}
8296

8397
/**

src/Api/State/EventRepresentationProvider.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ApiPlatform\Metadata\Operation;
77
use ApiPlatform\State\ProviderInterface;
88
use App\Model\IndexNames;
9+
use App\Service\ElasticSearch\ElasticSearchPaginator;
910
use Psr\Container\ContainerExceptionInterface;
1011
use Psr\Container\NotFoundExceptionInterface;
1112

@@ -16,18 +17,17 @@ final class EventRepresentationProvider extends AbstractProvider implements Prov
1617
* @throws NotFoundExceptionInterface
1718
* @throws \App\Exception\IndexException
1819
*/
19-
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
20+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ElasticSearchPaginator|array|null
2021
{
21-
// @TODO: should we create enum with 5,10,15,20
22-
// Get page size from context.
23-
2422
if ($operation instanceof CollectionOperationInterface) {
2523
$filters = $this->getFilters($operation, $context);
26-
$from = $this->calculatePageOffset($context);
24+
$offset = $this->calculatePageOffset($context);
25+
$limit = $this->getImagesPerPage($context);
26+
$results = $this->index->getAll(IndexNames::Events->value, $filters, $offset, $limit);
2727

28-
return $this->index->getAll(IndexNames::Events->value, $filters, $from, self::PAGE_SIZE);
28+
return new ElasticSearchPaginator($results, $limit, $offset);
2929
}
3030

31-
return (object) $this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source'];
31+
return [$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']];
3232
}
3333
}

src/Api/State/OrganizationRepresentationProvider.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ApiPlatform\Metadata\Operation;
77
use ApiPlatform\State\ProviderInterface;
88
use App\Model\IndexNames;
9+
use App\Service\ElasticSearch\ElasticSearchPaginator;
910
use Psr\Container\ContainerExceptionInterface;
1011
use Psr\Container\NotFoundExceptionInterface;
1112

@@ -20,9 +21,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
2021
{
2122
if ($operation instanceof CollectionOperationInterface) {
2223
$filters = $this->getFilters($operation, $context);
23-
$from = $this->calculatePageOffset($context);
24+
$offset = $this->calculatePageOffset($context);
25+
$limit = $this->getImagesPerPage($context);
26+
$results = $this->index->getAll(IndexNames::Organization->value, $filters, $offset, $limit);
2427

25-
return $this->index->getAll(IndexNames::Organization->value, $filters, $from, self::PAGE_SIZE);
28+
return new ElasticSearchPaginator($results, $limit, $offset);
2629
}
2730

2831
return [$this->index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']];

0 commit comments

Comments
 (0)