Skip to content

Commit

Permalink
fix(elasticsearch): elasticsearch 8 compatibility (#5795)
Browse files Browse the repository at this point in the history
Co-authored-by: Oleg Zhulnev <[email protected]>
  • Loading branch information
soyuka and sidz authored Sep 4, 2023
1 parent 4cf6b27 commit 6c9e121
Show file tree
Hide file tree
Showing 28 changed files with 289 additions and 217 deletions.
53 changes: 52 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,8 @@ jobs:
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: '7.6.0'
stack-version: '8.4.0'
security-enabled: false
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
Expand All @@ -547,6 +548,56 @@ jobs:
- name: Run Behat tests
run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction

elasticsearch-lowest:
name: Behat (PHP ${{ matrix.php }}) (Elasticsearch 7) (Symfony lowest)
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
php:
- '8.2'
fail-fast: false
env:
APP_ENV: elasticsearch
steps:
- name: Checkout
uses: actions/checkout@v3
- 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
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: '7.6.0'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: pecl, composer
extensions: intl, bcmath, curl, openssl, mbstring, mongodb
coverage: none
ini-values: memory_limit=-1
- name: Get composer cache directory
id: composercache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Update project dependencies
run: composer update --prefer-lowest --no-interaction --no-progress --ansi
- name: Install PHPUnit
run: vendor/bin/simple-phpunit --version
- name: Clear test app cache
run: tests/Fixtures/app/console cache:clear --ansi
- name: Run Behat tests
run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction

phpunit-no-deprecations:
name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations)
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"doctrine/mongodb-odm": "^2.2",
"doctrine/mongodb-odm-bundle": "^4.0",
"doctrine/orm": "^2.14",
"elasticsearch/elasticsearch": "^7.11.0",
"elasticsearch/elasticsearch": "^7.11 || ^8.4",
"friends-of-behat/mink-browserkit-driver": "^1.3.1",
"friends-of-behat/mink-extension": "^2.2",
"friends-of-behat/symfony-extension": "^2.1",
Expand Down Expand Up @@ -97,7 +97,7 @@
"symfony/var-exporter" : "<6.1.1",
"phpunit/phpunit": "<9.5",
"phpspec/prophecy": "<1.15",
"elasticsearch/elasticsearch": ">=8.0"
"elasticsearch/elasticsearch": ">=8.0,<8.4"
},
"suggest": {
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
Expand Down Expand Up @@ -135,7 +135,8 @@
"sort-packages": true,
"allow-plugins": {
"composer/package-versions-deprecated": true,
"phpstan/extension-installer": true
"phpstan/extension-installer": true,
"php-http/discovery": true
}
},
"extra": {
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,6 @@ parameters:
-
message: '#^Property .+ is unused.$#'
path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php
-
message: '#^Class .+ not found.$#'
path: src/Elasticsearch/Tests
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Util\Inflector;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elasticsearch\Client;
use Elasticsearch\Common\Exceptions\Missing404Exception;

Expand All @@ -30,6 +31,7 @@
*/
final class CatDocumentMetadataFactory implements DocumentMetadataFactoryInterface
{
// @phpstan-ignore-next-line
public function __construct(private readonly Client $client, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null)
{
}
Expand Down Expand Up @@ -62,8 +64,10 @@ public function create(string $resourceClass): DocumentMetadata
$index = Inflector::tableize($resourceShortName);

try {
// @phpstan-ignore-next-line
$this->client->cat()->indices(['index' => $index]);
} catch (Missing404Exception) {
// @phpstan-ignore-next-line
} catch (Missing404Exception|ClientResponseException) {
return $this->handleNotFound($documentMetadata, $resourceClass);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@

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

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

if ($operations) {
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
if ($operation->getProvider()) {
continue;
}

if (null !== ($elasticsearch = $operation->getElasticsearch())) {
$solution = $elasticsearch
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
: 'You will have to remove it when upgrading to v4';
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in Operation is deprecated. %s', $solution));
trigger_deprecation('api-platform/core', '3.1', sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class));
}
if ($this->hasIndices($operation)) {
$operation = $operation->withElasticsearch(true);

$hasElasticsearch = true === $elasticsearch || $operation->getStateOptions() instanceof Options;

// Old behavior in ES < 8
if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line
$hasElasticsearch = true;
}

if (null !== $operation->getProvider() || false === ($operation->getElasticsearch() ?? false)) {
if (!$hasElasticsearch) {
continue;
}

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

if ($graphQlOperations) {
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
if ($graphQlOperation->getProvider()) {
continue;
}

if (null !== ($elasticsearch = $graphQlOperation->getElasticsearch())) {
$solution = $elasticsearch
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
: 'You will have to remove it when upgrading to v4';
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in GraphQlOperation is deprecated. %s', $solution));
trigger_deprecation('api-platform/core', '3.1', sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class));
}
if (null !== $elasticsearch && $this->hasIndices($graphQlOperation)) {
$graphQlOperation = $graphQlOperation->withElasticsearch(true);

$hasElasticsearch = true === $elasticsearch || $graphQlOperation->getStateOptions() instanceof Options;

// Old behavior in ES < 8
if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line
$hasElasticsearch = true;
}

if (null !== $graphQlOperation->getProvider() || false === ($graphQlOperation->getElasticsearch() ?? false)) {
if (!$hasElasticsearch) {
continue;
}

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

private function hasIndices(Operation $operation): bool
{
if (false === $operation->getElasticsearch()) {
return false;
}

$shortName = $operation->getShortName();
$index = Inflector::tableize($shortName);

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

return true;
} catch (Missing404Exception|NoNodesAvailableException) {
} catch (Missing404Exception|NoNodesAvailableException) { // @phpstan-ignore-line
return false;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Elasticsearch/Paginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function getIterator(): \Traversable
$denormalizationContext = array_merge([AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true], $this->denormalizationContext);

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

if ($cacheKey && \array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) {
$object = $this->cachedDenormalizedDocuments[$cacheKey];
Expand Down
17 changes: 9 additions & 8 deletions src/Elasticsearch/State/CollectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
use ApiPlatform\Elasticsearch\Paginator;
use ApiPlatform\Elasticsearch\Util\ElasticsearchVersion;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Util\Inflector;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\ProviderInterface;
use Elasticsearch\Client;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elasticsearch\Client as LegacyClient;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
Expand All @@ -36,7 +37,7 @@ final class CollectionProvider implements ProviderInterface
/**
* @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions
*/
public function __construct(private readonly Client $client, private readonly DocumentMetadataFactoryInterface $documentMetadataFactory, private readonly DenormalizerInterface $denormalizer, private readonly Pagination $pagination, private readonly iterable $collectionExtensions = [])
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
{
}

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

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

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

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

$documents = $this->client->search($params);
if ($documents instanceof Elasticsearch) {
$documents = $documents->asArray();
}

return new Paginator(
$this->denormalizer,
Expand Down
28 changes: 18 additions & 10 deletions src/Elasticsearch/State/ItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer;
use ApiPlatform\Elasticsearch\Util\ElasticsearchVersion;
use ApiPlatform\Metadata\Exception\RuntimeException;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Util\Inflector;
use ApiPlatform\State\ProviderInterface;
use Elasticsearch\Client;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elasticsearch\Client as LegacyClient;
use Elasticsearch\Common\Exceptions\Missing404Exception;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

Expand All @@ -32,7 +36,7 @@
*/
final class ItemProvider implements ProviderInterface
{
public function __construct(private readonly Client $client, private readonly DocumentMetadataFactoryInterface $documentMetadataFactory, private readonly DenormalizerInterface $denormalizer)
public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null) // @phpstan-ignore-line
{
}

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

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

if (!$options instanceof Options) {
throw new RuntimeException(sprintf('The "%s" provider was called without "%s".', self::class, Options::class));
}

$params = [
'client' => ['ignore' => 404],
'index' => $options->getIndex() ?? $this->getIndex($operation),
'id' => (string) reset($uriVariables),
];

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

$document = $this->client->get($params);
if (!$document['found']) {
return null;
if ($document instanceof Elasticsearch) {
$document = $document->asArray();
}

$item = $this->denormalizer->denormalize($document, $resourceClass, DocumentNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true]);
Expand Down
3 changes: 3 additions & 0 deletions src/Elasticsearch/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Options implements OptionsInterface
{
public function __construct(
protected ?string $index = null,
/**
* @deprecated this parameter is not used anymore
*/
protected ?string $type = null,
) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ class CatDocumentMetadataFactoryTest extends TestCase
{
use ProphecyTrait;

protected function setUp(): void
{
if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) {
$this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.');
}
}

public function testConstruct(): void
{
self::assertInstanceOf(
Expand Down Expand Up @@ -127,6 +134,7 @@ public function testCreateWithIndexNotFound(): void
$resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled();

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

$clientProphecy = $this->prophesize(Client::class);
Expand Down
Loading

0 comments on commit 6c9e121

Please sign in to comment.