Skip to content

Commit 6c9e121

Browse files
soyukasidz
andauthored
fix(elasticsearch): elasticsearch 8 compatibility (#5795)
Co-authored-by: Oleg Zhulnev <[email protected]>
1 parent 4cf6b27 commit 6c9e121

28 files changed

+289
-217
lines changed

Diff for: .github/workflows/ci.yml

+52-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,8 @@ jobs:
520520
- name: Runs Elasticsearch
521521
uses: elastic/elastic-github-actions/elasticsearch@master
522522
with:
523-
stack-version: '7.6.0'
523+
stack-version: '8.4.0'
524+
security-enabled: false
524525
- name: Setup PHP
525526
uses: shivammathur/setup-php@v2
526527
with:
@@ -547,6 +548,56 @@ jobs:
547548
- name: Run Behat tests
548549
run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction
549550

551+
elasticsearch-lowest:
552+
name: Behat (PHP ${{ matrix.php }}) (Elasticsearch 7) (Symfony lowest)
553+
runs-on: ubuntu-latest
554+
timeout-minutes: 20
555+
strategy:
556+
matrix:
557+
php:
558+
- '8.2'
559+
fail-fast: false
560+
env:
561+
APP_ENV: elasticsearch
562+
steps:
563+
- name: Checkout
564+
uses: actions/checkout@v3
565+
- name: Configure sysctl limits
566+
run: |
567+
sudo swapoff -a
568+
sudo sysctl -w vm.swappiness=1
569+
sudo sysctl -w fs.file-max=262144
570+
sudo sysctl -w vm.max_map_count=262144
571+
- name: Runs Elasticsearch
572+
uses: elastic/elastic-github-actions/elasticsearch@master
573+
with:
574+
stack-version: '7.6.0'
575+
- name: Setup PHP
576+
uses: shivammathur/setup-php@v2
577+
with:
578+
php-version: ${{ matrix.php }}
579+
tools: pecl, composer
580+
extensions: intl, bcmath, curl, openssl, mbstring, mongodb
581+
coverage: none
582+
ini-values: memory_limit=-1
583+
- name: Get composer cache directory
584+
id: composercache
585+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
586+
- name: Cache dependencies
587+
uses: actions/cache@v3
588+
with:
589+
path: ${{ steps.composercache.outputs.dir }}
590+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
591+
restore-keys: ${{ runner.os }}-composer-
592+
- name: Update project dependencies
593+
run: composer update --prefer-lowest --no-interaction --no-progress --ansi
594+
- name: Install PHPUnit
595+
run: vendor/bin/simple-phpunit --version
596+
- name: Clear test app cache
597+
run: tests/Fixtures/app/console cache:clear --ansi
598+
- name: Run Behat tests
599+
run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction
600+
550601
phpunit-no-deprecations:
551602
name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations)
552603
runs-on: ubuntu-latest

Diff for: composer.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"doctrine/mongodb-odm": "^2.2",
3737
"doctrine/mongodb-odm-bundle": "^4.0",
3838
"doctrine/orm": "^2.14",
39-
"elasticsearch/elasticsearch": "^7.11.0",
39+
"elasticsearch/elasticsearch": "^7.11 || ^8.4",
4040
"friends-of-behat/mink-browserkit-driver": "^1.3.1",
4141
"friends-of-behat/mink-extension": "^2.2",
4242
"friends-of-behat/symfony-extension": "^2.1",
@@ -97,7 +97,7 @@
9797
"symfony/var-exporter" : "<6.1.1",
9898
"phpunit/phpunit": "<9.5",
9999
"phpspec/prophecy": "<1.15",
100-
"elasticsearch/elasticsearch": ">=8.0"
100+
"elasticsearch/elasticsearch": ">=8.0,<8.4"
101101
},
102102
"suggest": {
103103
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
@@ -135,7 +135,8 @@
135135
"sort-packages": true,
136136
"allow-plugins": {
137137
"composer/package-versions-deprecated": true,
138-
"phpstan/extension-installer": true
138+
"phpstan/extension-installer": true,
139+
"php-http/discovery": true
139140
}
140141
},
141142
"extra": {

Diff for: phpstan.neon.dist

+3
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,6 @@ parameters:
8686
-
8787
message: '#^Property .+ is unused.$#'
8888
path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php
89+
-
90+
message: '#^Class .+ not found.$#'
91+
path: src/Elasticsearch/Tests

Diff for: src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
1818
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
1919
use ApiPlatform\Metadata\Util\Inflector;
20+
use Elastic\Elasticsearch\Exception\ClientResponseException;
2021
use Elasticsearch\Client;
2122
use Elasticsearch\Common\Exceptions\Missing404Exception;
2223

@@ -30,6 +31,7 @@
3031
*/
3132
final class CatDocumentMetadataFactory implements DocumentMetadataFactoryInterface
3233
{
34+
// @phpstan-ignore-next-line
3335
public function __construct(private readonly Client $client, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null)
3436
{
3537
}
@@ -62,8 +64,10 @@ public function create(string $resourceClass): DocumentMetadata
6264
$index = Inflector::tableize($resourceShortName);
6365

6466
try {
67+
// @phpstan-ignore-next-line
6568
$this->client->cat()->indices(['index' => $index]);
66-
} catch (Missing404Exception) {
69+
// @phpstan-ignore-next-line
70+
} catch (Missing404Exception|ClientResponseException) {
6771
return $this->handleNotFound($documentMetadata, $resourceClass);
6872
}
6973

Diff for: src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php

+27-24
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@
2727

2828
final class ElasticsearchProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
2929
{
30-
public function __construct(private readonly Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true)
30+
public function __construct(private readonly Client|null $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true) // @phpstan-ignore-line
3131
{
32-
if ($this->triggerDeprecation) {
33-
trigger_deprecation('api-platform/core', '3.1', '%s is deprecated and will be removed in v4', self::class);
34-
}
3532
}
3633

3734
/**
@@ -46,17 +43,22 @@ public function create(string $resourceClass): ResourceMetadataCollection
4643

4744
if ($operations) {
4845
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
46+
if ($operation->getProvider()) {
47+
continue;
48+
}
49+
4950
if (null !== ($elasticsearch = $operation->getElasticsearch())) {
50-
$solution = $elasticsearch
51-
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
52-
: 'You will have to remove it when upgrading to v4';
53-
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in Operation is deprecated. %s', $solution));
51+
trigger_deprecation('api-platform/core', '3.1', sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class));
5452
}
55-
if ($this->hasIndices($operation)) {
56-
$operation = $operation->withElasticsearch(true);
53+
54+
$hasElasticsearch = true === $elasticsearch || $operation->getStateOptions() instanceof Options;
55+
56+
// Old behavior in ES < 8
57+
if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line
58+
$hasElasticsearch = true;
5759
}
5860

59-
if (null !== $operation->getProvider() || false === ($operation->getElasticsearch() ?? false)) {
61+
if (!$hasElasticsearch) {
6062
continue;
6163
}
6264

@@ -70,17 +72,22 @@ public function create(string $resourceClass): ResourceMetadataCollection
7072

7173
if ($graphQlOperations) {
7274
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
75+
if ($graphQlOperation->getProvider()) {
76+
continue;
77+
}
78+
7379
if (null !== ($elasticsearch = $graphQlOperation->getElasticsearch())) {
74-
$solution = $elasticsearch
75-
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
76-
: 'You will have to remove it when upgrading to v4';
77-
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in GraphQlOperation is deprecated. %s', $solution));
80+
trigger_deprecation('api-platform/core', '3.1', sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class));
7881
}
79-
if (null !== $elasticsearch && $this->hasIndices($graphQlOperation)) {
80-
$graphQlOperation = $graphQlOperation->withElasticsearch(true);
82+
83+
$hasElasticsearch = true === $elasticsearch || $graphQlOperation->getStateOptions() instanceof Options;
84+
85+
// Old behavior in ES < 8
86+
if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line
87+
$hasElasticsearch = true;
8188
}
8289

83-
if (null !== $graphQlOperation->getProvider() || false === ($graphQlOperation->getElasticsearch() ?? false)) {
90+
if (!$hasElasticsearch) {
8491
continue;
8592
}
8693

@@ -98,18 +105,14 @@ public function create(string $resourceClass): ResourceMetadataCollection
98105

99106
private function hasIndices(Operation $operation): bool
100107
{
101-
if (false === $operation->getElasticsearch()) {
102-
return false;
103-
}
104-
105108
$shortName = $operation->getShortName();
106109
$index = Inflector::tableize($shortName);
107110

108111
try {
109-
$this->client->cat()->indices(['index' => $index]);
112+
$this->client->cat()->indices(['index' => $index]); // @phpstan-ignore-line
110113

111114
return true;
112-
} catch (Missing404Exception|NoNodesAvailableException) {
115+
} catch (Missing404Exception|NoNodesAvailableException) { // @phpstan-ignore-line
113116
return false;
114117
}
115118
}

Diff for: src/Elasticsearch/Paginator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function getIterator(): \Traversable
9595
$denormalizationContext = array_merge([AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true], $this->denormalizationContext);
9696

9797
foreach ($this->documents['hits']['hits'] ?? [] as $document) {
98-
$cacheKey = isset($document['_index'], $document['_type'], $document['_id']) ? md5("{$document['_index']}_{$document['_type']}_{$document['_id']}") : null;
98+
$cacheKey = isset($document['_index'], $document['_id']) ? md5("{$document['_index']}_{$document['_id']}") : null;
9999

100100
if ($cacheKey && \array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) {
101101
$object = $this->cachedDenormalizedDocuments[$cacheKey];

Diff for: src/Elasticsearch/State/CollectionProvider.php

+9-8
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
1818
use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
1919
use ApiPlatform\Elasticsearch\Paginator;
20-
use ApiPlatform\Elasticsearch\Util\ElasticsearchVersion;
2120
use ApiPlatform\Metadata\Operation;
2221
use ApiPlatform\Metadata\Util\Inflector;
2322
use ApiPlatform\State\Pagination\Pagination;
2423
use ApiPlatform\State\ProviderInterface;
25-
use Elasticsearch\Client;
24+
use Elastic\Elasticsearch\Client;
25+
use Elastic\Elasticsearch\Response\Elasticsearch;
26+
use Elasticsearch\Client as LegacyClient;
2627
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2728

2829
/**
@@ -36,7 +37,7 @@ final class CollectionProvider implements ProviderInterface
3637
/**
3738
* @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions
3839
*/
39-
public function __construct(private readonly Client $client, private readonly DocumentMetadataFactoryInterface $documentMetadataFactory, private readonly DenormalizerInterface $denormalizer, private readonly Pagination $pagination, private readonly iterable $collectionExtensions = [])
40+
public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?Pagination $pagination = null, private readonly iterable $collectionExtensions = []) // @phpstan-ignore-line
4041
{
4142
}
4243

@@ -62,7 +63,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6263
$options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation));
6364

6465
// TODO: remove in 4.x
65-
if ($operation->getElasticsearch() && !$operation->getStateOptions()) {
66+
if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) {
6667
$options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass));
6768
}
6869

@@ -71,11 +72,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7172
'body' => $body,
7273
];
7374

74-
if (null !== $options->getType() && ElasticsearchVersion::supportsMappingType()) {
75-
$params['type'] = $options->getType();
76-
}
75+
$documents = $this->client->search($params); // @phpstan-ignore-line
7776

78-
$documents = $this->client->search($params);
77+
if ($documents instanceof Elasticsearch) {
78+
$documents = $documents->asArray();
79+
}
7980

8081
return new Paginator(
8182
$this->denormalizer,

Diff for: src/Elasticsearch/State/ItemProvider.php

+18-10
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
1717
use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
1818
use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer;
19-
use ApiPlatform\Elasticsearch\Util\ElasticsearchVersion;
19+
use ApiPlatform\Metadata\Exception\RuntimeException;
2020
use ApiPlatform\Metadata\Operation;
2121
use ApiPlatform\Metadata\Util\Inflector;
2222
use ApiPlatform\State\ProviderInterface;
23-
use Elasticsearch\Client;
23+
use Elastic\Elasticsearch\Client;
24+
use Elastic\Elasticsearch\Exception\ClientResponseException;
25+
use Elastic\Elasticsearch\Response\Elasticsearch;
26+
use Elasticsearch\Client as LegacyClient;
27+
use Elasticsearch\Common\Exceptions\Missing404Exception;
2428
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
2529
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2630

@@ -32,7 +36,7 @@
3236
*/
3337
final class ItemProvider implements ProviderInterface
3438
{
35-
public function __construct(private readonly Client $client, private readonly DocumentMetadataFactoryInterface $documentMetadataFactory, private readonly DenormalizerInterface $denormalizer)
39+
public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null) // @phpstan-ignore-line
3640
{
3741
}
3842

@@ -46,23 +50,27 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
4650
$options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation));
4751

4852
// TODO: remove in 4.x
49-
if ($operation->getElasticsearch() && !$operation->getStateOptions()) {
53+
if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) {
5054
$options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass));
5155
}
5256

57+
if (!$options instanceof Options) {
58+
throw new RuntimeException(sprintf('The "%s" provider was called without "%s".', self::class, Options::class));
59+
}
60+
5361
$params = [
54-
'client' => ['ignore' => 404],
5562
'index' => $options->getIndex() ?? $this->getIndex($operation),
5663
'id' => (string) reset($uriVariables),
5764
];
5865

59-
if (null !== $options->getType() && ElasticsearchVersion::supportsMappingType()) {
60-
$params['type'] = $options->getType();
66+
try {
67+
$document = $this->client->get($params); // @phpstan-ignore-line
68+
} catch (Missing404Exception|ClientResponseException) { // @phpstan-ignore-line
69+
return null;
6170
}
6271

63-
$document = $this->client->get($params);
64-
if (!$document['found']) {
65-
return null;
72+
if ($document instanceof Elasticsearch) {
73+
$document = $document->asArray();
6674
}
6775

6876
$item = $this->denormalizer->denormalize($document, $resourceClass, DocumentNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true]);

Diff for: src/Elasticsearch/State/Options.php

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class Options implements OptionsInterface
1919
{
2020
public function __construct(
2121
protected ?string $index = null,
22+
/**
23+
* @deprecated this parameter is not used anymore
24+
*/
2225
protected ?string $type = null,
2326
) {
2427
}

Diff for: src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ class CatDocumentMetadataFactoryTest extends TestCase
3333
{
3434
use ProphecyTrait;
3535

36+
protected function setUp(): void
37+
{
38+
if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) {
39+
$this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.');
40+
}
41+
}
42+
3643
public function testConstruct(): void
3744
{
3845
self::assertInstanceOf(
@@ -127,6 +134,7 @@ public function testCreateWithIndexNotFound(): void
127134
$resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled();
128135

129136
$catNamespaceProphecy = $this->prophesize(CatNamespace::class);
137+
// @phpstan-ignore-next-line
130138
$catNamespaceProphecy->indices(['index' => 'foo'])->willThrow(new Missing404Exception())->shouldBeCalled();
131139

132140
$clientProphecy = $this->prophesize(Client::class);

0 commit comments

Comments
 (0)