Skip to content

Commit

Permalink
Merge 04449cf into 4359926
Browse files Browse the repository at this point in the history
  • Loading branch information
hhamon committed Dec 6, 2020
2 parents 4359926 + 04449cf commit 0d7dd37
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,10 +8,12 @@
* DTO: Add `ApiPlatform\Core\DataTransformer\DataTransformerInitializerInterface` to pre-hydrate inputs (#3701)
* DTO: Improve Input/Output support (#3231)
* Data Persisters: Add `previous_data` to the context passed to persisters when available (#3752)
* Data Providers: Add `PaginatorFactoryInterface` interface.
* Debug: Display API Platform's version in the debug bar (#3235)
* Docs: Make `asset_package` configurable (#3764)
* Doctrine: Allow searching on multiple values on every strategies (#3786)
* Elasticsearch: The `Paginator` class constructor now receives the denormalization context to support denormalizing documents using serialization groups. This change may cause potential **BC** breaks for existing applications as denormalization was previously done without serialization groups.
* Elasticsearch: The `CollectionDataProvider` class constructor now receives a concrete `PaginatorFactoryInterface` instance that is responsible for creating the concrete `PaginatorInterface` instance. This change enables more flexibility allowing applications to inject their custom implementation of `PaginatorFactoryInterface` dependency to produce a custom implementation of the `PaginatorInterface` interface.
* GraphQL: **BC** New syntax for the filters' arguments to preserve the order: `order: [{foo: 'asc'}, {bar: 'desc'}]` (#3468)
* GraphQL: **BC** `operation` is now `operationName` to follow the standard (#3568)
* GraphQL: **BC** `paginationType` is now `pagination_type` (#3614)
Expand Down
20 changes: 9 additions & 11 deletions src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\Pagination;
use ApiPlatform\Core\DataProvider\PaginatorFactoryInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
Expand All @@ -38,20 +39,24 @@ final class CollectionDataProvider implements ContextAwareCollectionDataProvider
private $client;
private $documentMetadataFactory;
private $identifierExtractor;
private $denormalizer;
private $paginatorFactory;
private $pagination;
private $resourceMetadataFactory;
private $collectionExtensions;

/**
* @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions
*/
public function __construct(Client $client, DocumentMetadataFactoryInterface $documentMetadataFactory, IdentifierExtractorInterface $identifierExtractor, DenormalizerInterface $denormalizer, Pagination $pagination, ResourceMetadataFactoryInterface $resourceMetadataFactory, iterable $collectionExtensions = [])
public function __construct(Client $client, DocumentMetadataFactoryInterface $documentMetadataFactory, IdentifierExtractorInterface $identifierExtractor, DenormalizerInterface $denormalizer, Pagination $pagination, ResourceMetadataFactoryInterface $resourceMetadataFactory, iterable $collectionExtensions = [], ?PaginatorFactoryInterface $paginatorFactory = null)
{
if (null === $paginatorFactory) {
$paginatorFactory = new PaginatorFactory($denormalizer);
}

$this->client = $client;
$this->documentMetadataFactory = $documentMetadataFactory;
$this->identifierExtractor = $identifierExtractor;
$this->denormalizer = $denormalizer;
$this->paginatorFactory = $paginatorFactory;
$this->pagination = $pagination;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->collectionExtensions = $collectionExtensions;
Expand Down Expand Up @@ -111,13 +116,6 @@ public function getCollection(string $resourceClass, ?string $operationName = nu
'body' => $body,
]);

return new Paginator(
$this->denormalizer,
$documents,
$resourceClass,
$limit,
$offset,
$context
);
return $this->paginatorFactory->createPaginator($documents, $limit, $offset, array_merge(['resourceClass' => $resourceClass], $context));
}
}
50 changes: 50 additions & 0 deletions src/Bridge/Elasticsearch/DataProvider/PaginatorFactory.php
@@ -0,0 +1,50 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider;

use ApiPlatform\Core\DataProvider\PaginatorFactoryInterface;
use ApiPlatform\Core\DataProvider\PaginatorInterface;
use ApiPlatform\Core\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

final class PaginatorFactory implements PaginatorFactoryInterface
{
private $denormalizer;

public function __construct(DenormalizerInterface $denormalizer)
{
$this->denormalizer = $denormalizer;
}

/**
* {@inheritdoc}
*/
public function createPaginator($subject, int $limit, int $offset, array $context = []): PaginatorInterface
{
$resourceClass = $context['resourceClass'] ?? null;

if (null === $resourceClass) {
throw new RuntimeException('The given context array is missing the "resourceClass" key.');
}

return new Paginator(
$this->denormalizer,
$subject,
$resourceClass,
$limit,
$offset,
$context
);
}
}
5 changes: 5 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
Expand Up @@ -78,10 +78,15 @@
<argument type="service" id="api_platform.pagination" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="tagged" tag="api_platform.elasticsearch.request_body_search_extension.collection" />
<argument type="service" id="api_platform.elasticsearch.paginator_factory" />

<tag name="api_platform.collection_data_provider" priority="5" />
</service>

<service id="api_platform.elasticsearch.paginator_factory" class="ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\PaginatorFactory" public="false">
<argument type="service" id="serializer" />
</service>

<service id="api_platform.elasticsearch.request_body_search_extension.filter" public="false" abstract="true">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.filter_locator" />
Expand Down
31 changes: 31 additions & 0 deletions src/DataProvider/PaginatorFactoryInterface.php
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\DataProvider;

use ApiPlatform\Core\Exception\ExceptionInterface;

interface PaginatorFactoryInterface
{
/**
* Creates a new {@see PaginatorInterface} concrete instance.
*
* @param mixed $subject The subject to paginate (array, ORM query, etc.)
* @param int $limit The maximum number of records to fetch
* @param int $offset The starting index from which to fetch the records
* @param array<string, mixed> $context The associative array context for the paginator
*
* @throws ExceptionInterface Whenever something wrong occurs
*/
public function createPaginator($subject, int $limit, int $offset, array $context = []): PaginatorInterface;
}
111 changes: 107 additions & 4 deletions tests/Bridge/Elasticsearch/DataProvider/CollectionDataProviderTest.php
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\CollectionDataProvider;
use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\RequestBodySearchCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator;
use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\PaginatorFactory;
use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException;
use ApiPlatform\Core\Bridge\Elasticsearch\Exception\NonUniqueIdentifierException;
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
Expand All @@ -43,15 +44,19 @@ class CollectionDataProviderTest extends TestCase

public function testConstruct()
{
$denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal();

self::assertInstanceOf(
CollectionDataProviderInterface::class,
new CollectionDataProvider(
$this->prophesize(Client::class)->reveal(),
$this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(),
$this->prophesize(IdentifierExtractorInterface::class)->reveal(),
$this->prophesize(DenormalizerInterface::class)->reveal(),
$denormalizer,
new Pagination($this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()),
$this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
$this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
[],
new PaginatorFactory($denormalizer)
)
);
}
Expand All @@ -73,14 +78,17 @@ public function testSupports()
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadata());
$resourceMetadataFactoryProphecy->create(CompositeRelation::class)->shouldBeCalled()->willReturn(new ResourceMetadata());
$resourceMetadataFactoryProphecy->create(DummyCarColor::class)->shouldBeCalled()->willThrow(new ResourceClassNotFoundException());
$denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal();

$collectionDataProvider = new CollectionDataProvider(
$this->prophesize(Client::class)->reveal(),
$documentMetadataFactoryProphecy->reveal(),
$identifierExtractorProphecy->reveal(),
$this->prophesize(DenormalizerInterface::class)->reveal(),
$denormalizer,
new Pagination($this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()),
$resourceMetadataFactoryProphecy->reveal()
$resourceMetadataFactoryProphecy->reveal(),
[],
new PaginatorFactory($denormalizer)
);

self::assertTrue($collectionDataProvider->supports(Foo::class));
Expand All @@ -94,6 +102,101 @@ public function testGetCollection()
{
$context = [
'groups' => ['custom'],
'resourceClass' => Foo::class,
];

$documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
$documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();

$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata());

$documents = [
'took' => 15,
'time_out' => false,
'_shards' => [
'total' => 5,
'successful' => 5,
'skipped' => 0,
'failed' => 0,
],
'hits' => [
'total' => 4,
'max_score' => 1,
'hits' => [
[
'_index' => 'foo',
'_type' => '_doc',
'_id' => '1',
'_score' => 1,
'_source' => [
'id' => 1,
'name' => 'Kilian',
'bar' => 'Jornet',
],
],
[
'_index' => 'foo',
'_type' => '_doc',
'_id' => '2',
'_score' => 1,
'_source' => [
'id' => 2,
'name' => 'François',
'bar' => 'D\'Haene',
],
],
],
],
];

$clientProphecy = $this->prophesize(Client::class);
$clientProphecy
->search(
Argument::allOf(
Argument::withEntry('index', 'foo'),
Argument::withEntry('type', DocumentMetadata::DEFAULT_TYPE),
Argument::withEntry('body', Argument::allOf(
Argument::withEntry('size', 2),
Argument::withEntry('from', 0),
Argument::withEntry('query', Argument::allOf(
Argument::withEntry('match_all', Argument::type(\stdClass::class)),
Argument::size(1)
)),
Argument::size(3)
)),
Argument::size(3)
)
)
->willReturn($documents)
->shouldBeCalled();

$requestBodySearchCollectionExtensionProphecy = $this->prophesize(RequestBodySearchCollectionExtensionInterface::class);
$requestBodySearchCollectionExtensionProphecy->applyToCollection([], Foo::class, 'get', $context)->willReturn([])->shouldBeCalled();
$denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal();

$collectionDataProvider = new CollectionDataProvider(
$clientProphecy->reveal(),
$documentMetadataFactoryProphecy->reveal(),
$this->prophesize(IdentifierExtractorInterface::class)->reveal(),
$denormalizer,
new Pagination($resourceMetadataFactoryProphecy->reveal(), ['items_per_page' => 2]),
$resourceMetadataFactoryProphecy->reveal(),
[$requestBodySearchCollectionExtensionProphecy->reveal()],
new PaginatorFactory($denormalizer)
);

self::assertEquals(
new Paginator($denormalizer, $documents, Foo::class, 2, 0, $context),
$collectionDataProvider->getCollection(Foo::class, 'get', $context)
);
}

public function testGetCollectionWithoutPaginatorFactoryDependency()
{
$context = [
'groups' => ['custom'],
'resourceClass' => Foo::class,
];

$documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
Expand Down
58 changes: 58 additions & 0 deletions tests/Bridge/Elasticsearch/DataProvider/PaginatorFactoryTest.php
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Bridge\Elasticsearch\DataProvider;

use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator;
use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\PaginatorFactory;
use ApiPlatform\Core\DataProvider\PaginatorFactoryInterface;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
use ApiPlatform\Core\Tests\ProphecyTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

final class PaginatorFactoryTest extends TestCase
{
use ProphecyTrait;

/**
* @var PaginatorFactory
*/
private $paginatorFactory;

public function testConstruct()
{
$this->assertInstanceOf(PaginatorFactoryInterface::class, $this->paginatorFactory);
}

public function testCreatePaginator()
{
$paginator = $this->paginatorFactory->createPaginator([], 10, 0, ['resourceClass' => Foo::class]);

$this->assertInstanceOf(Paginator::class, $paginator);
}

public function testCreatePaginatorFailsWhenResourceClassAttributeIsMissing()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('The given context array is missing the "resourceClass" key.');

$this->paginatorFactory->createPaginator([], 10, 0, ['resourceClass' => null]);
}

protected function setUp(): void
{
$this->paginatorFactory = new PaginatorFactory($this->prophesize(DenormalizerInterface::class)->reveal());
}
}
Expand Up @@ -666,6 +666,7 @@ public function testEnableElasticsearch()
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.normalizer.item', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.item_data_provider', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.collection_data_provider', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.paginator_factory', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.request_body_search_extension.filter', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.request_body_search_extension.constant_score_filter', Argument::type(Definition::class))->shouldBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.elasticsearch.request_body_search_extension.sort_filter', Argument::type(Definition::class))->shouldBeCalled();
Expand Down

0 comments on commit 0d7dd37

Please sign in to comment.