From 14225838e44eae218a9e083803d65b29185db976 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 17 Jul 2026 11:04:00 +0200 Subject: [PATCH 1/4] chore: Compatibility with prime 3.0 (#FRAM-233) --- .github/workflows/php.yml | 92 ++--- composer.json | 12 +- ...RegisterIndexConfigurationCompilerPass.php | 2 +- .../RegisterIndexFactoryCompilerPass.php | 2 +- .../PrimeIndexerExtension.php | 12 +- .../PrimeIndexerTestExtension.php | 2 +- .../Query/Result/ElasticsearchPaginator.php | 318 ++++++++++++++++-- src/Sync/RepositorySubscriber.php | 21 +- .../_files/UserAttributesMapper.php | 2 +- .../Query/ElasticsearchCreateQueryTest.php | 4 +- 10 files changed, 377 insertions(+), 90 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ee3a6ed..6e60428 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -122,52 +122,52 @@ jobs: - name: Run test suite run: composer run-script tests -# -# prime_compatiblity: -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# matrix: -# prime-versions: ['2.0'] -# name: Prime version ${{ matrix.prime-versions }} -# -# steps: -# - uses: actions/checkout@v2 -# -# - name: Set Timezone -# uses: szenius/set-timezone@v1.0 -# with: -# timezoneLinux: "Europe/Paris" -# -# - name: Install PHP -# uses: shivammathur/setup-php@v2 -# with: -# php-version: 7.2 -# extensions: json -# ini-values: date.timezone=Europe/Paris -# -# - name: Configure sysctl limits -# run: | -# sudo swapoff -a -# sudo sysctl -w vm.swappiness=1 -# sudo sysctl -w fs.file-max=262144 -# sudo sysctl -w vm.max_map_count=262144 -# -# - uses: nyaruka/elasticsearch-action@v1 -# with: -# elastic version: 5.6 -# -# - name: Install dependencies -# run: composer install --prefer-dist --no-progress -# -# - name: Install Prime ${{ matrix.prime-versions }} -# run: composer require --dev 'b2pweb/bdf-prime:~${{ matrix.prime-versions }}.0' --with-all-dependencies -# -# - name: Install ES Lib 5.0 -# run: composer require --dev 'elasticsearch/elasticsearch:~5.0' --with-all-dependencies -# -# - name: Run test suite -# run: composer run-script tests + + prime_compatiblity: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + prime-versions: ['2.0', '3.0'] + name: Prime version ${{ matrix.prime-versions }} + + steps: + - uses: actions/checkout@v2 + + - name: Set Timezone + uses: szenius/set-timezone@v1.0 + with: + timezoneLinux: "Europe/Paris" + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: json + ini-values: date.timezone=Europe/Paris + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - uses: nyaruka/elasticsearch-action@v1 + with: + elastic version: 8.19.12 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Install Prime ${{ matrix.prime-versions }} + run: composer require --dev 'b2pweb/bdf-prime:~${{ matrix.prime-versions }}.0' --with-all-dependencies + + - name: Install ES Lib 8.0 + run: composer require --dev 'elasticsearch/elasticsearch:~8.0' --with-all-dependencies + + - name: Run test suite + run: composer run-script tests analysis: name: Analysis diff --git a/composer.json b/composer.json index 5655c2c..f5db6ef 100755 --- a/composer.json +++ b/composer.json @@ -28,17 +28,17 @@ "prefer-stable": true, "require": { "php": "~7.4 | ~8.0.0 | ~8.1.0 | ~8.2.0 | ~8.3.0 | ~8.4.0 | ~8.5.0", - "b2pweb/bdf-prime": "~2.0", + "b2pweb/bdf-prime": "~2.2|~3.0", "elasticsearch/elasticsearch": "~7.0|~8.0", "b2pweb/bdf-collections": "^1.1.4" }, "require-dev": { "phpunit/phpunit": "~9.0", - "b2pweb/bdf-prime-bundle": "~1.0", - "symfony/framework-bundle": "~4.0|~5.0|~6.0|~7.0", - "symfony/messenger": "~4.0|~5.0|~6.0|~7.0", - "symfony/console": "~4.0|~5.0|~6.0|~7.0", - "symfony/yaml": "~4.0|~5.0|~6.0|~7.0", + "b2pweb/bdf-prime-bundle": "dev-feature-FRAM-233-prime-v3 as 1.11.0", + "symfony/framework-bundle": "~4.0|~5.0|~6.0|~7.0|~8.0", + "symfony/messenger": "~4.0|~5.0|~6.0|~7.0|~8.0", + "symfony/console": "~4.0|~5.0|~6.0|~7.0|~8.0", + "symfony/yaml": "~4.0|~5.0|~6.0|~7.0|~8.0", "vimeo/psalm": "~5.22 | ~6.0", "squizlabs/php_codesniffer": "~3.0" }, diff --git a/src/Bundle/DependencyInjection/Compiler/RegisterIndexConfigurationCompilerPass.php b/src/Bundle/DependencyInjection/Compiler/RegisterIndexConfigurationCompilerPass.php index 2699595..b7f4221 100644 --- a/src/Bundle/DependencyInjection/Compiler/RegisterIndexConfigurationCompilerPass.php +++ b/src/Bundle/DependencyInjection/Compiler/RegisterIndexConfigurationCompilerPass.php @@ -16,7 +16,7 @@ final class RegisterIndexConfigurationCompilerPass implements CompilerPassInterf /** * {@inheritdoc} */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $resolverDefinition = $container->findDefinition(MappingResolver::class); diff --git a/src/Bundle/DependencyInjection/Compiler/RegisterIndexFactoryCompilerPass.php b/src/Bundle/DependencyInjection/Compiler/RegisterIndexFactoryCompilerPass.php index 88a4270..04c4302 100644 --- a/src/Bundle/DependencyInjection/Compiler/RegisterIndexFactoryCompilerPass.php +++ b/src/Bundle/DependencyInjection/Compiler/RegisterIndexFactoryCompilerPass.php @@ -18,7 +18,7 @@ final class RegisterIndexFactoryCompilerPass implements CompilerPassInterface /** * {@inheritdoc} */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $factory = $container->findDefinition(IndexFactory::class); $factories = []; diff --git a/src/Bundle/DependencyInjection/PrimeIndexerExtension.php b/src/Bundle/DependencyInjection/PrimeIndexerExtension.php index 2b4f846..1da9a7c 100644 --- a/src/Bundle/DependencyInjection/PrimeIndexerExtension.php +++ b/src/Bundle/DependencyInjection/PrimeIndexerExtension.php @@ -3,6 +3,7 @@ namespace Bdf\Prime\Indexer\Bundle\DependencyInjection; use Bdf\Prime\Indexer\Bundle\Factory\IndexFactoryInterface; +use Bdf\Prime\Indexer\Elasticsearch\Adapter\ClientInterface; use Bdf\Prime\Indexer\IndexConfigurationInterface; use Bdf\Prime\Indexer\IndexFactory; use Symfony\Component\Config\FileLocator; @@ -11,6 +12,8 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use function class_exists; + /** * Class PrimeIndexerExtension */ @@ -21,7 +24,7 @@ class PrimeIndexerExtension extends Extension * * @psalm-suppress PossiblyNullArgument */ - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -43,6 +46,13 @@ public function load(array $configs, ContainerBuilder $container) ; $this->configureIndexes($config, $container); + + $container->register('Elasticsearch\Client') + ->setClass(class_exists('Elasticsearch\Client') ? 'Elasticsearch\Client' : 'Elastic\Elasticsearch\Client') + ->setPublic(true) + ->setFactory([new Reference(ClientInterface::class), 'getInternalClient']) + ->setDeprecated('b2pweb/bdf-prime-indexer', '2.0', 'Direct usage of elasticsearch driver client using service "%service_id%" is deprecated since 2.0') + ; } private function configureIndexes(array $config, ContainerBuilder $container): void diff --git a/src/Bundle/DependencyInjection/PrimeIndexerTestExtension.php b/src/Bundle/DependencyInjection/PrimeIndexerTestExtension.php index 214f225..2867a4f 100644 --- a/src/Bundle/DependencyInjection/PrimeIndexerTestExtension.php +++ b/src/Bundle/DependencyInjection/PrimeIndexerTestExtension.php @@ -12,7 +12,7 @@ final class PrimeIndexerTestExtension extends Extension /** * {@inheritdoc} */ - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $container->register(TestingIndexer::class) ->setArguments([new Reference('service_container'), false]) diff --git a/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php b/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php index cc31366..b19b6b0 100644 --- a/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php +++ b/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php @@ -6,24 +6,27 @@ use Bdf\Prime\Collection\CollectionInterface; use Bdf\Prime\Indexer\Elasticsearch\Adapter\Response\SearchResults; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchQuery; -use Bdf\Prime\Query\Pagination\AbstractPaginator; -use Bdf\Prime\Query\Pagination\Paginator; +use Bdf\Prime\PrimeSerializable; use Bdf\Prime\Query\Pagination\PaginatorInterface; use IteratorAggregate; +use ReturnTypeWillChange; use Traversable; +use function count; + /** * Implements paginator for elasticsearch * * @template R as array|object - * @extends AbstractPaginator + * * @implements IteratorAggregate * @implements PaginatorInterface - * - * @psalm-suppress PropertyNotSetInConstructor */ -class ElasticsearchPaginator extends AbstractPaginator implements IteratorAggregate, PaginatorInterface +class ElasticsearchPaginator extends PrimeSerializable implements IteratorAggregate, PaginatorInterface { + public const DEFAULT_PAGE = 1; + public const DEFAULT_LIMIT = 20; + /** * The raw Elasticsearch result */ @@ -39,16 +42,21 @@ class ElasticsearchPaginator extends AbstractPaginator implements IteratorAggreg private $transformer; /** - * @var ElasticsearchQuery - * @psalm-suppress NonInvariantDocblockPropertyType + * Current page */ - protected $query; + private ?int $page = null; + + /** + * Number of entities loaded in the collection + */ + private ?int $maxRows = null; + + private ElasticsearchQuery $query; /** * @var CollectionInterface - * @psalm-suppress NonInvariantDocblockPropertyType */ - protected $collection; + private CollectionInterface $collection; /** * ElasticsearchPaginator constructor. @@ -61,17 +69,17 @@ class ElasticsearchPaginator extends AbstractPaginator implements IteratorAggreg public function __construct(ElasticsearchQuery $query, ?int $maxRows = null, ?int $page = null, ?callable $transformer = null) { $this->query = $query; - $this->maxRows = $maxRows ?: Paginator::DEFAULT_LIMIT; - $this->page = $page ?: Paginator::DEFAULT_PAGE; + $this->maxRows = $maxRows ?: self::DEFAULT_LIMIT; + $this->page = $page ?: self::DEFAULT_PAGE; $this->transformer = $transformer; - $this->loadCollection(); + $this->collection = $this->loadCollection(); } /** * {@inheritdoc} */ - public function size() + public function size(): int { return $this->result->total(); } @@ -84,18 +92,287 @@ public function getIterator(): Traversable return $this->collection; } + /** + * {@inheritdoc} + * + */ + final public function collection(): CollectionInterface + { + return $this->collection; + } + + /** + * Get the query + */ + final public function query(): ElasticsearchQuery + { + return $this->query; + } + + /** + * {@inheritdoc} + */ + public function order($attribute = null) + { + $orders = $this->query->getOrders(); + + if ($attribute === null) { + return $orders; + } + + return isset($orders[$attribute]) ? $orders[$attribute] : null; + } + + /** + * {@inheritdoc} + */ + public function limit(): ?int + { + return $this->query->getLimit(); + } + + /** + * {@inheritdoc} + */ + public function offset(): ?int + { + return $this->query->getOffset(); + } + + /** + * {@inheritdoc} + */ + public function page() + { + return $this->query->getPage(); + } + + /** + * {@inheritdoc} + */ + public function pageMaxRows() + { + return (int) $this->query->getLimit(); + } + + /** + * SPL - Countable + * + * {@inheritdoc} + */ + public function count(): int + { + return count($this->collection); + } + + //--------- collection interface + + /** + * {@inheritdoc} + */ + public function pushAll(array $items) + { + $this->collection->pushAll($items); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function push($item) + { + $this->collection->push($item); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function put($key, $item) + { + $this->collection->put($key, $item); + + return $this; + } + + /** + * SPL - ArrayAccess + * + * {@inheritdoc} + */ + public function offsetSet($offset, $value): void + { + $this->collection[$offset] = $value; + } + + /** + * {@inheritdoc} + */ + public function all(): array + { + return $this->collection->all(); + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + return $this->collection->get($key, $default); + } + + /** + * {@inheritdoc} + */ + #[ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->collection[$offset]; + } + + /** + * {@inheritdoc} + */ + public function has($key): bool + { + return $this->collection->has($key); + } + + /** + * SPL - ArrayAccess + * + * {@inheritdoc} + */ + public function offsetExists($offset): bool + { + return isset($this->collection[$offset]); + } + + /** + * {@inheritdoc} + */ + public function remove($key) + { + $this->collection->remove($key); + + return $this; + } + + /** + * SPL - ArrayAccess + * + * {@inheritdoc} + */ + public function offsetUnset($offset): void + { + unset($this->collection[$offset]); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->collection->clear(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function keys(): array + { + return $this->collection->keys(); + } + /** * {@inheritdoc} */ - protected function buildSize(): void + public function isEmpty(): bool { - // No-op + return $this->collection->isEmpty(); } /** * {@inheritdoc} + * + * @param callable(R):M $callback The function to run + * @return static The new collection + * + * @template M as array|object + */ + public function map($callback) + { + /** @var static $this */ + $this->collection = $this->collection->map($callback); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function filter($callback = null) + { + $this->collection = $this->collection->filter($callback); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function groupBy($groupBy, $mode = PaginatorInterface::GROUPBY) + { + $this->collection = $this->collection->groupBy($groupBy, $mode); + + return $this; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function contains($element): bool + { + return $this->collection->contains($element); + } + + /** + * {@inheritdoc} + */ + public function indexOf($value, $strict = false) + { + return $this->collection->indexOf($value, $strict); + } + + /** + * {@inheritdoc} + */ + public function merge($items) + { + $this->collection = $this->collection->merge($items); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function sort(?callable $callback = null) + { + $this->collection = $this->collection->sort($callback); + + return $this; + } + /** + * @return CollectionInterface */ - protected function loadCollection(): void + private function loadCollection(): CollectionInterface { if ($this->maxRows > -1) { $this->query->limitPage($this->page, $this->maxRows); @@ -105,10 +382,9 @@ protected function loadCollection(): void $collection = new ArrayCollection($this->result->hits()); if ($this->transformer) { - $this->collection = $collection->map($this->transformer); + return $collection->map($this->transformer); } else { - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $this->collection = $collection; + return $collection; } } } diff --git a/src/Sync/RepositorySubscriber.php b/src/Sync/RepositorySubscriber.php index 89582fb..24bf267 100644 --- a/src/Sync/RepositorySubscriber.php +++ b/src/Sync/RepositorySubscriber.php @@ -5,6 +5,9 @@ use Bdf\Bus\MessageDispatcherInterface; use Bdf\Prime\Indexer\ShouldBeIndexedConfigurationInterface; use Bdf\Prime\Repository\EntityRepository; +use Bdf\Prime\Repository\Event\AfterDelete; +use Bdf\Prime\Repository\Event\AfterInsert; +use Bdf\Prime\Repository\Event\AfterUpdate; use Symfony\Component\Messenger\MessageBusInterface; /** @@ -47,11 +50,11 @@ public function __construct($dispatcher, string $index, object $config) /** * An entity is inserted * Index only if it should be indexed - * - * @param object $entity */ - public function inserted(object $entity): void + public function inserted(AfterInsert $event): void { + $entity = $event->entity; + if ($this->shouldBeIndexed($entity)) { $this->dispatcher->dispatch(new AddToIndex($this->index, $entity)); } @@ -62,11 +65,11 @@ public function inserted(object $entity): void * * If the entity should still be indexed, it will be updated * Otherwise, the entity is removed - * - * @param object $entity */ - public function updated(object $entity): void + public function updated(AfterUpdate $event): void { + $entity = $event->entity; + if ($this->shouldBeIndexed($entity)) { $this->dispatcher->dispatch(new UpdateIndexedEntity($this->index, $entity)); } else { @@ -77,12 +80,10 @@ public function updated(object $entity): void /** * An entity is deleted from the database * The entity will also be removed from index - * - * @param object $entity */ - public function deleted(object $entity): void + public function deleted(AfterDelete $event): void { - $this->dispatcher->dispatch(new RemoveFromIndex($this->index, $entity)); + $this->dispatcher->dispatch(new RemoveFromIndex($this->index, $event->entity)); } /** diff --git a/tests/Denormalize/_files/UserAttributesMapper.php b/tests/Denormalize/_files/UserAttributesMapper.php index 3301368..2d348f9 100644 --- a/tests/Denormalize/_files/UserAttributesMapper.php +++ b/tests/Denormalize/_files/UserAttributesMapper.php @@ -9,7 +9,7 @@ class UserAttributesMapper extends Mapper public function schema(): array { return [ - 'connexion' => 'test', + 'connection' => 'test', 'table' => 'user_attr', ]; } diff --git a/tests/Elasticsearch/Query/ElasticsearchCreateQueryTest.php b/tests/Elasticsearch/Query/ElasticsearchCreateQueryTest.php index fa6c729..d7fd7e3 100644 --- a/tests/Elasticsearch/Query/ElasticsearchCreateQueryTest.php +++ b/tests/Elasticsearch/Query/ElasticsearchCreateQueryTest.php @@ -289,7 +289,7 @@ public function test_insert() $this->assertEmpty(iterator_to_array($response)); $this->assertEmpty($response->all()); - $this->assertSame($response, $response->asAssociative()->asList()->asObject()->asClass(\stdClass::class)->asColumn(1)->fetchMode(ResultSetInterface::FETCH_ASSOC)); + $this->assertSame($response, $response->asAssociative()->asList()->asObject()->asClass(\stdClass::class)->asColumn(1)); $this->assertArrayHasKey('result', $response); $this->assertArrayNotHasKey('foo', $response); @@ -370,7 +370,7 @@ public function test_insert_replace_same_id_twice() $this->assertEmpty(iterator_to_array($response)); $this->assertEmpty($response->all()); - $this->assertSame($response, $response->asAssociative()->asList()->asObject()->asClass(\stdClass::class)->asColumn(1)->fetchMode(ResultSetInterface::FETCH_ASSOC)); + $this->assertSame($response, $response->asAssociative()->asList()->asObject()->asClass(\stdClass::class)->asColumn(1)); $this->assertEquals(1, $this->search()->execute()->total()); $this->assertEquals([ From 523d3d29eb4259e67b9aebe95fbf3c1511daef17 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 17 Jul 2026 11:20:21 +0200 Subject: [PATCH 2/4] ci: fix build --- .github/workflows/php.yml | 12 ++++++------ .../Query/Result/ElasticsearchPaginator.php | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6e60428..8002d63 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -128,7 +128,7 @@ jobs: strategy: fail-fast: false matrix: - prime-versions: ['2.0', '3.0'] + prime-versions: ['2.2', '2.3', '3.0'] name: Prime version ${{ matrix.prime-versions }} steps: @@ -157,14 +157,14 @@ jobs: with: elastic version: 8.19.12 - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - name: Install Prime ${{ matrix.prime-versions }} - run: composer require --dev 'b2pweb/bdf-prime:~${{ matrix.prime-versions }}.0' --with-all-dependencies + run: composer require --no-update --dev 'b2pweb/bdf-prime:~${{ matrix.prime-versions }}.0' - name: Install ES Lib 8.0 - run: composer require --dev 'elasticsearch/elasticsearch:~8.0' --with-all-dependencies + run: composer require --no-update --dev 'elasticsearch/elasticsearch:~8.0' + + - name: Install dependencies + run: composer update --prefer-dist --no-progress --with-all-dependencies - name: Run test suite run: composer run-script tests diff --git a/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php b/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php index b19b6b0..01567e8 100644 --- a/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php +++ b/src/Elasticsearch/Query/Result/ElasticsearchPaginator.php @@ -44,7 +44,7 @@ class ElasticsearchPaginator extends PrimeSerializable implements IteratorAggreg /** * Current page */ - private ?int $page = null; + private int $page; /** * Number of entities loaded in the collection @@ -201,6 +201,9 @@ public function put($key, $item) * SPL - ArrayAccess * * {@inheritdoc} + * + * @psalm-suppress PossiblyNullArrayOffset + * @psalm-suppress PossiblyNullArgument */ public function offsetSet($offset, $value): void { @@ -369,6 +372,7 @@ public function sort(?callable $callback = null) return $this; } + /** * @return CollectionInterface */ @@ -384,6 +388,7 @@ private function loadCollection(): CollectionInterface if ($this->transformer) { return $collection->map($this->transformer); } else { + /** @var CollectionInterface */ return $collection; } } From c61846da2169eb023a742484d19decd537020bab Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 17 Jul 2026 11:26:03 +0200 Subject: [PATCH 3/4] ci: fix build --- .github/workflows/php.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 8002d63..75411e1 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -153,9 +153,11 @@ jobs: sudo sysctl -w fs.file-max=262144 sudo sysctl -w vm.max_map_count=262144 - - uses: nyaruka/elasticsearch-action@v1 + - name: Runs Elasticsearch ${{ matrix.versions.es }} + uses: elastic/elastic-github-actions/elasticsearch@dc110609b1cb3024477ead739ca23ab547b8b9ff with: - elastic version: 8.19.12 + stack-version: 8.19.12 + security-enabled: false - name: Install Prime ${{ matrix.prime-versions }} run: composer require --no-update --dev 'b2pweb/bdf-prime:~${{ matrix.prime-versions }}.0' From d7ec9651e532f4eec9ae672922572fa0e33338dc Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 17 Jul 2026 11:39:13 +0200 Subject: [PATCH 4/4] test: Improve coverage of ElastisearchPaginator --- .../Result/ElasticsearchPaginatorTest.php | 348 +++++++++++++++++- 1 file changed, 345 insertions(+), 3 deletions(-) diff --git a/tests/Elasticsearch/Query/Result/ElasticsearchPaginatorTest.php b/tests/Elasticsearch/Query/Result/ElasticsearchPaginatorTest.php index 83b8a3d..9b6022c 100644 --- a/tests/Elasticsearch/Query/Result/ElasticsearchPaginatorTest.php +++ b/tests/Elasticsearch/Query/Result/ElasticsearchPaginatorTest.php @@ -2,12 +2,11 @@ namespace Bdf\Prime\Indexer\Elasticsearch\Query\Result; +use Bdf\Prime\Collection\CollectionInterface; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchCreateQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchQuery; use Bdf\Prime\Indexer\IndexTestCase; -use Elastic\Elasticsearch\Client; -use Elasticsearch\ClientBuilder; -use PHPUnit\Framework\TestCase; +use Bdf\Prime\Query\Pagination\PaginatorInterface; /** * Class ElasticsearchPaginatorTest @@ -163,4 +162,347 @@ public function test_page_too_high() $this->assertEquals(10, $paginator->page()); $this->assertEquals([], iterator_to_array($paginator)); } + + /** + * + */ + public function test_without_transformer() + { + $paginator = new ElasticsearchPaginator($this->query); + + $this->assertCount(4, $paginator); + $this->assertEquals([ + ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + ['name' => 'Paris', 'population' => 27022, 'country' => 'US'], + ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + ], array_column($paginator->all(), '_source')); + } + + /** + * + */ + public function test_getIterator_should_return_the_collection() + { + $paginator = $this->paginator(); + + $this->assertInstanceOf(CollectionInterface::class, $paginator->getIterator()); + $this->assertSame($paginator->collection(), $paginator->getIterator()); + } + + /** + * + */ + public function test_query() + { + $paginator = $this->paginator(); + + $this->assertSame($this->query, $paginator->query()); + } + + /** + * + */ + public function test_collection() + { + $paginator = $this->paginator(); + + $this->assertInstanceOf(CollectionInterface::class, $paginator->collection()); + $this->assertEquals([ + ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + ['name' => 'Paris', 'population' => 27022, 'country' => 'US'], + ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + ], $paginator->collection()->all()); + } + + /** + * + */ + public function test_order() + { + $paginator = $this->paginator(); + + $this->assertEquals(['population' => 'desc'], $paginator->order()); + $this->assertEquals('desc', $paginator->order('population')); + $this->assertNull($paginator->order('name')); + } + + /** + * + */ + public function test_order_when_empty() + { + $query = new ElasticsearchQuery(self::getClient()); + $query->from('test_cities', 'city'); + + $paginator = new ElasticsearchPaginator($query); + + $this->assertEquals([], $paginator->order()); + $this->assertNull($paginator->order('population')); + } + + /** + * + */ + public function test_push() + { + $paginator = $this->paginator(); + $city = ['name' => 'Marseille', 'population' => 861635, 'country' => 'FR']; + + $this->assertSame($paginator, $paginator->push($city)); + $this->assertCount(5, $paginator); + $this->assertEquals($city, $paginator->get(4)); + } + + /** + * + */ + public function test_pushAll_should_replace_items() + { + $paginator = $this->paginator(); + $cities = [ + ['name' => 'Marseille', 'population' => 861635, 'country' => 'FR'], + ['name' => 'Lyon', 'population' => 513275, 'country' => 'FR'], + ]; + + $this->assertSame($paginator, $paginator->pushAll($cities)); + $this->assertCount(2, $paginator); + $this->assertEquals($cities, $paginator->all()); + } + + /** + * + */ + public function test_put() + { + $paginator = $this->paginator(); + $city = ['name' => 'Marseille', 'population' => 861635, 'country' => 'FR']; + + $this->assertSame($paginator, $paginator->put('marseille', $city)); + $this->assertEquals($city, $paginator->get('marseille')); + } + + /** + * + */ + public function test_get() + { + $paginator = $this->paginator(); + + $this->assertEquals(['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], $paginator->get(0)); + $this->assertNull($paginator->get('not_found')); + $this->assertEquals('default', $paginator->get('not_found', 'default')); + } + + /** + * + */ + public function test_has() + { + $paginator = $this->paginator(); + + $this->assertTrue($paginator->has(0)); + $this->assertFalse($paginator->has('not_found')); + } + + /** + * + */ + public function test_remove() + { + $paginator = $this->paginator(); + + $this->assertSame($paginator, $paginator->remove(0)); + $this->assertCount(3, $paginator); + $this->assertFalse($paginator->has(0)); + } + + /** + * + */ + public function test_array_access() + { + $paginator = $this->paginator(); + + $this->assertTrue(isset($paginator[0])); + $this->assertFalse(isset($paginator['not_found'])); + $this->assertEquals(['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], $paginator[0]); + + $city = ['name' => 'Marseille', 'population' => 861635, 'country' => 'FR']; + $paginator['marseille'] = $city; + $this->assertEquals($city, $paginator['marseille']); + + $paginator[] = $city; + $this->assertEquals($city, $paginator[4]); + + unset($paginator[0]); + $this->assertFalse(isset($paginator[0])); + $this->assertCount(5, $paginator); + } + + /** + * + */ + public function test_all() + { + $paginator = $this->paginator(); + + $this->assertEquals([ + ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + ['name' => 'Paris', 'population' => 27022, 'country' => 'US'], + ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + ], $paginator->all()); + } + + /** + * + */ + public function test_clear_and_isEmpty() + { + $paginator = $this->paginator(); + + $this->assertFalse($paginator->isEmpty()); + $this->assertSame($paginator, $paginator->clear()); + $this->assertTrue($paginator->isEmpty()); + $this->assertCount(0, $paginator); + } + + /** + * + */ + public function test_keys() + { + $paginator = $this->paginator(); + + $this->assertEquals([0, 1, 2, 3], $paginator->keys()); + } + + /** + * + */ + public function test_map() + { + $paginator = $this->paginator(); + + $this->assertSame($paginator, $paginator->map(function ($city) { return $city['name']; })); + $this->assertEquals(['Paris', 'Paris', 'Cavaillon', 'Parthenay'], $paginator->all()); + } + + /** + * + */ + public function test_filter() + { + $paginator = $this->paginator(); + + $this->assertSame($paginator, $paginator->filter(function ($city) { return $city['country'] === 'FR'; })); + $this->assertEquals([ + 0 => ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + 2 => ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + 3 => ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + ], $paginator->all()); + } + + /** + * + */ + public function test_groupBy() + { + $paginator = $this->paginator(); + + $this->assertSame($paginator, $paginator->groupBy(function ($city) { return $city['country']; }, PaginatorInterface::GROUPBY_COMBINE)); + $this->assertEquals([ + 'FR' => [ + ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + ], + 'US' => [ + ['name' => 'Paris', 'population' => 27022, 'country' => 'US'], + ], + ], $paginator->all()); + } + + /** + * + */ + public function test_contains() + { + $paginator = $this->paginator(); + + $this->assertTrue($paginator->contains(['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'])); + $this->assertFalse($paginator->contains(['name' => 'Marseille', 'population' => 861635, 'country' => 'FR'])); + $this->assertTrue($paginator->contains(function ($city) { return $city['name'] === 'Cavaillon'; })); + $this->assertFalse($paginator->contains(function ($city) { return $city['name'] === 'Marseille'; })); + } + + /** + * + */ + public function test_indexOf() + { + $paginator = $this->paginator(); + + $this->assertSame(1, $paginator->indexOf(['name' => 'Paris', 'population' => 27022, 'country' => 'US'])); + $this->assertFalse($paginator->indexOf(['name' => 'Marseille', 'population' => 861635, 'country' => 'FR'])); + $this->assertSame(2, $paginator->indexOf(function ($city) { return $city['name'] === 'Cavaillon'; })); + } + + /** + * + */ + public function test_merge() + { + $paginator = $this->paginator(); + $cities = [ + ['name' => 'Marseille', 'population' => 861635, 'country' => 'FR'], + ['name' => 'Lyon', 'population' => 513275, 'country' => 'FR'], + ]; + + $this->assertSame($paginator, $paginator->merge($cities)); + $this->assertCount(6, $paginator); + $this->assertEquals($cities[0], $paginator->get(4)); + $this->assertEquals($cities[1], $paginator->get(5)); + } + + /** + * + */ + public function test_sort() + { + $paginator = $this->paginator(); + + $this->assertSame($paginator, $paginator->sort(function ($a, $b) { return $a['population'] <=> $b['population']; })); + $this->assertEquals([ + 3 => ['name' => 'Parthenay', 'population' => 11599, 'country' => 'FR'], + 2 => ['name' => 'Cavaillon', 'population' => 26689, 'country' => 'FR'], + 1 => ['name' => 'Paris', 'population' => 27022, 'country' => 'US'], + 0 => ['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], + ], $paginator->all()); + } + + /** + * + */ + public function test_collection_method_do_not_change_the_paginator_collection() + { + $paginator = $this->paginator(); + + $collection = $paginator->collection()->map(function ($city) { return $city['name']; }); + + $this->assertEquals(['name' => 'Paris', 'population' => 2201578, 'country' => 'FR'], $paginator->get(0)); + $this->assertEquals('Paris', $collection->get(0)); + } + + /** + * Create a paginator on the base query, with a transformer extracting the document source + * + * @return ElasticsearchPaginator + */ + private function paginator(): ElasticsearchPaginator + { + return new ElasticsearchPaginator($this->query, null, null, function ($doc) { return $doc['_source']; }); + } }