Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ jobs:
continue-on-error: true

phpunit-components:
name: PHPUnit ${{ matrix.component }} (PHP ${{ matrix.php.version }} ${{ matrix.php.coverage && 'coverage' || '' }}${{ matrix.php.lowest && 'lowest' || '' }}${{ matrix.php.minimal-changes && 'minimal-changes' || '' }})
name: PHPUnit ${{ matrix.component }} (PHP ${{ matrix.php.version }} ${{ matrix.php.coverage && 'coverage' || '' }}${{ matrix.php.lowest && 'lowest' || '' }}${{ matrix.php.minimal-changes && 'minimal-changes' || '' }}${{ matrix.opensearch && 'opensearch' || '' }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
Expand Down Expand Up @@ -338,6 +338,11 @@ jobs:
- api-platform/state
- api-platform/symfony
- api-platform/validator
include:
- php:
version: '8.5'
component: api-platform/elasticsearch
opensearch: true
fail-fast: false
steps:
- name: Checkout
Expand Down Expand Up @@ -368,6 +373,11 @@ jobs:
run: |
cd $(composer ${{matrix.component}} --cwd)
composer update${{ matrix.php.lowest && ' --prefer-lowest --prefer-source' || '' }}${{ matrix.php.minimal-changes && ' --minimal-changes' || '' }}
- name: Install OpenSearch PHP client
if: ${{ matrix.opensearch }}
run: |
cd $(composer ${{matrix.component}} --cwd)
composer require --dev opensearch-project/opensearch-php "^2.5" -W
- name: Run ${{ matrix.component }} tests
run: |
mkdir -p /tmp/build/logs/phpunit
Expand Down Expand Up @@ -888,6 +898,64 @@ jobs:
- name: Run Behat tests
run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction

opensearch:
name: Behat (PHP ${{ matrix.php }}) (OpenSearch ${{ matrix.opensearch-version }})
runs-on: ubuntu-22.04
timeout-minutes: 20
strategy:
matrix:
include:
- php: '8.5'
opensearch-version: '2.19.4'
extensions: 'intl, bcmath, curl, openssl, mbstring'
fail-fast: false
env:
APP_ENV: opensearch
services:
opensearch:
image: opensearchproject/opensearch:${{ matrix.opensearch-version }}
ports:
- 9200:9200
env:
discovery.type: single-node
DISABLE_SECURITY_PLUGIN: 'true'
OPENSEARCH_INITIAL_ADMIN_PASSWORD: 'Admin_1234!'
options: >-
--health-cmd "curl -f http://localhost:9200/_cluster/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: pecl, composer
extensions: ${{ matrix.extensions }}
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@v5
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Update project dependencies
run: |
composer global require soyuka/pmu
composer global config allow-plugins.soyuka/pmu true --no-interaction
composer global link .
composer require --dev opensearch-project/opensearch-php "^2.5" -W
- 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=opensearch --no-interaction

phpunit-no-deprecations:
name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations)
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

* [f74d7ba1a](https://github.com/api-platform/core/commit/f74d7ba1a) feat(elasticsearch): add OpenSearch support (#7519)

## v4.3.0-alpha.2

### Bug fixes
Expand Down
15 changes: 15 additions & 0 deletions behat.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ elasticsearch:
filters:
tags: '@elasticsearch&&~@mercure&&~@query_parameter_validator'

opensearch:
suites:
default: false
opensearch:
paths:
- '%paths.base%/features/elasticsearch'
contexts:
- 'ApiPlatform\Tests\Behat\CommandContext'
- 'ApiPlatform\Tests\Behat\ElasticsearchContext'
- 'ApiPlatform\Tests\Behat\JsonContext'
- 'Behat\MinkExtension\Context\MinkContext'
- 'behatch:context:rest'
filters:
tags: '@elasticsearch&&~@mercure&&~@query_parameter_validator'

default-coverage:
suites:
default: &default-coverage-suite
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
"suggest": {
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
"elasticsearch/elasticsearch": "To support Elasticsearch.",
"opensearch-project/opensearch-php": "To support OpenSearch (^2.5).",
"phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
"psr/cache-implementation": "To use metadata caching.",
"ramsey/uuid": "To support Ramsey's UUID identifiers.",
Expand Down
6 changes: 4 additions & 2 deletions src/Elasticsearch/State/CollectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elasticsearch\Client as V7Client;
use Elasticsearch\Common\Exceptions\Missing404Exception as V7Missing404Exception;
use OpenSearch\Client as OpenSearchClient;
use OpenSearch\Common\Exceptions\Missing404Exception as OpenSearchMissing404Exception;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
Expand All @@ -40,7 +42,7 @@ final class CollectionProvider implements ProviderInterface
* @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions
*/
public function __construct(
private readonly V7Client|Client $client, // @phpstan-ignore-line
private readonly V7Client|Client|OpenSearchClient $client, // @phpstan-ignore-line
private readonly ?DenormalizerInterface $denormalizer = null,
private readonly ?Pagination $pagination = null,
private readonly iterable $collectionExtensions = [],
Expand Down Expand Up @@ -76,7 +78,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c

try {
$documents = $this->client->search($params); // @phpstan-ignore-line
} catch (V7Missing404Exception $e) { // @phpstan-ignore-line
} catch (V7Missing404Exception|OpenSearchMissing404Exception $e) { // @phpstan-ignore-line
throw new Error(status: $e->getCode(), detail: $e->getMessage(), title: $e->getMessage(), originalTrace: $e->getTrace()); // @phpstan-ignore-line
} catch (ClientResponseException $e) {
$response = $e->getResponse();
Expand Down
6 changes: 4 additions & 2 deletions src/Elasticsearch/State/ItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elasticsearch\Client as V7Client;
use Elasticsearch\Common\Exceptions\Missing404Exception as V7Missing404Exception;
use OpenSearch\Client as OpenSearchClient;
use OpenSearch\Common\Exceptions\Missing404Exception as OpenSearchMissing404Exception;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

Expand All @@ -36,7 +38,7 @@
final class ItemProvider implements ProviderInterface
{
public function __construct(
private readonly V7Client|Client $client, // @phpstan-ignore-line
private readonly V7Client|Client|OpenSearchClient $client, // @phpstan-ignore-line
private readonly ?DenormalizerInterface $denormalizer = null,
private readonly ?InflectorInterface $inflector = new Inflector(),
) {
Expand All @@ -60,7 +62,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c

try {
$document = $this->client->get($params); // @phpstan-ignore-line
} catch (V7Missing404Exception) { // @phpstan-ignore-line
} catch (V7Missing404Exception|OpenSearchMissing404Exception) { // @phpstan-ignore-line
return null;
} catch (ClientResponseException $e) {
$response = $e->getResponse();
Expand Down
3 changes: 3 additions & 0 deletions src/Elasticsearch/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"symfony/type-info": "^7.3 || ^8.0",
"symfony/uid": "^6.4 || ^7.0 || ^8.0"
},
"suggest": {
"opensearch-project/opensearch-php": "Required to use OpenSearch instead of Elasticsearch (^2.5)"
},
"require-dev": {
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^12.2"
Expand Down
13 changes: 9 additions & 4 deletions src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -991,16 +991,21 @@ private function registerElasticsearchConfiguration(ContainerBuilder $container,
throw new \LogicException('Elasticsearch support cannot be enabled as the Elasticsearch component is not installed. Try running "composer require api-platform/elasticsearch".');
}

$clientClass = !class_exists(\Elasticsearch\Client::class)
// ES v7
? \Elastic\Elasticsearch\Client::class
if ('opensearch' === $config['elasticsearch']['client']) {
$clientClass = \OpenSearch\Client::class; // @phpstan-ignore class.notFound
} elseif (!class_exists(\Elasticsearch\Client::class)) {
// ES v8 and up
: \Elasticsearch\Client::class;
$clientClass = \Elastic\Elasticsearch\Client::class;
} else {
// ES v7
$clientClass = \Elasticsearch\Client::class;
}

$clientDefinition = new Definition($clientClass);
$container->setDefinition('api_platform.elasticsearch.client', $clientDefinition);
$container->registerForAutoconfiguration(RequestBodySearchCollectionExtensionInterface::class)
->addTag('api_platform.elasticsearch.request_body_search_extension.collection');
$container->setParameter('api_platform.elasticsearch.client', $config['elasticsearch']['client']);
$container->setParameter('api_platform.elasticsearch.hosts', $config['elasticsearch']['hosts']);
$container->setParameter('api_platform.elasticsearch.ssl_ca_bundle', $config['elasticsearch']['ssl_ca_bundle']);
$container->setParameter('api_platform.elasticsearch.ssl_verification', $config['elasticsearch']['ssl_verification']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public function process(ContainerBuilder $container): void
$clientConfiguration['hosts'] = $hosts;
}

if (class_exists(\Elasticsearch\ClientBuilder::class)) {
if ('opensearch' === $container->getParameter('api_platform.elasticsearch.client')) {
$builderName = \OpenSearch\ClientBuilder::class; // @phpstan-ignore class.notFound
} elseif (class_exists(\Elasticsearch\ClientBuilder::class)) {
// ES v7
$builderName = \Elasticsearch\ClientBuilder::class;
} else {
Expand Down
19 changes: 18 additions & 1 deletion src/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,10 @@ private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void
!class_exists(\Elasticsearch\Client::class)
// ES v8 and up
&& !class_exists(\Elastic\Elasticsearch\Client::class)
// OpenSearch
&& !class_exists(\OpenSearch\Client::class)
) {
throw new InvalidConfigurationException('The elasticsearch/elasticsearch package is required for Elasticsearch support.');
throw new InvalidConfigurationException('The elasticsearch/elasticsearch or opensearch-project/opensearch-php package is required for Elasticsearch support.');
}

return $v;
Expand All @@ -526,6 +528,21 @@ private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void
->defaultTrue()
->info('Enable or disable SSL verification for Elasticsearch connections.')
->end()
->enumNode('client')
->values(['elasticsearch', 'opensearch'])
->defaultValue('elasticsearch')
->info('The search engine client to use: "elasticsearch" or "opensearch".')
->validate()
->ifString()
->then(static function (string $v): string {
if ('opensearch' === $v && !class_exists(\OpenSearch\Client::class)) {
throw new InvalidConfigurationException('Setting api_platform.elasticsearch.client to "opensearch" requires the opensearch-project/opensearch-php package. Try running "composer require opensearch-project/opensearch-php".');
}

return $v;
})
->end()
->end()
->end()
->end()
->end();
Expand Down
3 changes: 2 additions & 1 deletion tests/Behat/ElasticsearchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Behat\Behat\Context\Context;
use Elastic\Elasticsearch\Client;
use Elasticsearch\Client as V7Client;
use OpenSearch\Client as OpenSearchClient;
use Symfony\Component\Finder\Finder;

/**
Expand All @@ -26,7 +27,7 @@
final class ElasticsearchContext implements Context
{
public function __construct(
private readonly V7Client|Client $client, // @phpstan-ignore-line
private readonly V7Client|Client|OpenSearchClient $client, // @phpstan-ignore-line
private readonly string $elasticsearchMappingsPath,
private readonly string $elasticsearchFixturesPath,
) {
Expand Down
27 changes: 27 additions & 0 deletions tests/Fixtures/app/config/config_opensearch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
imports:
- { resource: config_common.yml }
- { resource: config_behat_orm.yml }

parameters:
env(ELASTICSEARCH_URL): http://localhost:9200

api_platform:
doctrine: false
mapping:
paths:
- '%kernel.project_dir%/../Elasticsearch/Model'
elasticsearch:
hosts: '%env(resolve:ELASTICSEARCH_URL)%'
client: opensearch

services:
test.api_platform.elasticsearch.client:
parent: api_platform.elasticsearch.client
public: true

ApiPlatform\Tests\Behat\ElasticsearchContext:
public: true
arguments:
$client: '@test.api_platform.elasticsearch.client'
$elasticsearchMappingsPath: '%kernel.project_dir%/../Elasticsearch/Mappings/'
$elasticsearchFixturesPath: '%kernel.project_dir%/../Elasticsearch/Fixtures/'
2 changes: 2 additions & 0 deletions tests/Fixtures/app/config/routing_opensearch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
_main:
resource: routing.yml
Loading
Loading