diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4b5de..1b6ba13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## 2.0.0 + +### Added + +- Updated `ruflin/elastica` to v8 (Breaking Change) +- Dropped `HttpClientTransport` (Breaking Change) ## 1.9.1 diff --git a/Makefile b/Makefile index 089850e..be61188 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ test: ## Run test suite ./vendor/bin/simple-phpunit start: ## Start testing tools (Elasticsearch) - docker run --rm -d --name "elastically_es" -p 9999:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch-oss:7.8.0 + docker run --rm -d --name "elastically_es" -p 9999:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" -e "action.destructive_requires_name=false" -it -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.14.2 start_opensearch: ## Start testing tools (OpenSearch) docker run --rm -d --name "elastically_es" -p 9999:9200 -e "discovery.type=single-node" -e "plugins.security.disabled=true" opensearchproject/opensearch:2.3.0 diff --git a/README.md b/README.md index ea19df6..fa928e6 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,6 @@ elastically: client: host: '%env(ELASTICSEARCH_HOST)%' # If you want to use the Symfony HttpClient (you MUST create this service) - #transport: 'JoliCode\Elastically\Transport\HttpClientTransport' # Path to the mapping directory (in YAML) mapping_directory: '%kernel.project_dir%/config/elasticsearch' diff --git a/UPGRADE.md b/UPGRADE.md index 65a85a2..b2a4921 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,19 @@ # Upgrade guide +## From v1.9.0 to v2.0.0 + +HttpClientTransport has been removed: +```diff + JoliCode\Elastically\Client: + arguments: + $config: + host: '%env(ELASTICSEARCH_HOST)%' + port: '%env(ELASTICSEARCH_PORT)%' +- transport: '@JoliCode\Elastically\Transport\HttpClientTransport' ++ transport_client: ++ client: '@my_custom_psr18_client' # An instance of Symfony\Component\HttpClient\Psr18Client (Or any PSR 18 compliant one) +``` + ## From v1.3.0 to v1.4.0 If you're using Symfony, here are the changes to apply: diff --git a/composer.json b/composer.json index c9cbd64..2a90a4f 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,13 @@ "require": { "php": ">=8.0", "ext-json": "*", + "nyholm/psr7": "^1.8", "phpdocumentor/reflection-docblock": "^4.4|^5.0", - "ruflin/elastica": "^7.0", + "ruflin/elastica": "^8.0", "symfony/deprecation-contracts": "^2.4 || ^3.0", "symfony/property-access": "^5.4 || ^6.0 || ^7.0", "symfony/property-info": "^5.4 || ^6.0 || ^7.0", + "symfony/psr-http-message-bridge": "^7.1", "symfony/serializer": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, diff --git a/src/Bridge/Symfony/DependencyInjection/Configuration.php b/src/Bridge/Symfony/DependencyInjection/Configuration.php index 743e3a5..63b5ee2 100644 --- a/src/Bridge/Symfony/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/DependencyInjection/Configuration.php @@ -32,7 +32,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('All options for the Elastica client constructor') ->example([ 'host' => '%env(ELASTICSEARCH_HOST)%', - 'transport' => 'JoliCode\Elastically\Transport\HttpClientTransport', ]) ->normalizeKeys(false) ->defaultValue([]) diff --git a/src/Bridge/Symfony/DependencyInjection/ElasticallyExtension.php b/src/Bridge/Symfony/DependencyInjection/ElasticallyExtension.php index 27a2f11..c7af325 100644 --- a/src/Bridge/Symfony/DependencyInjection/ElasticallyExtension.php +++ b/src/Bridge/Symfony/DependencyInjection/ElasticallyExtension.php @@ -67,8 +67,8 @@ private function buildConnection(string $name, array $config, bool $isDefaultCon $container->setDefinition("elastically.{$name}.result_set_builder", $resultSetBuilder); $client = new ChildDefinition('elastically.abstract.client'); - if (\array_key_exists('client', $config) && \array_key_exists('transport', $config['client'])) { - $config['client']['transport'] = new Reference($config['client']['transport']); + if ($transportClient = $config['client']['transport_config']['http_client'] ?? null) { + $config['client']['transport_config']['http_client'] = new Reference($transportClient); } $client->replaceArgument('$config', $config['client'] ?? []); $client->replaceArgument('$resultSetBuilder', new Reference("elastically.{$name}.result_set_builder")); diff --git a/src/Client.php b/src/Client.php index 088549c..a1dd4f5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -31,9 +31,9 @@ class Client extends ElasticaClient private ResultSetBuilder $resultSetBuilder; private IndexNameMapper $indexNameMapper; - public function __construct($config = [], ?callable $callback = null, ?LoggerInterface $logger = null, ?ResultSetBuilder $resultSetBuilder = null, ?IndexNameMapper $indexNameMapper = null) + public function __construct($config = [], ?LoggerInterface $logger = null, ?ResultSetBuilder $resultSetBuilder = null, ?IndexNameMapper $indexNameMapper = null) { - parent::__construct($config, $callback, $logger); + parent::__construct($config, $logger); // BC Layer, to remove in 2.0 $this->factory = new Factory($config); diff --git a/src/Factory.php b/src/Factory.php index 5468db4..c77dad6 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -65,7 +65,6 @@ public function buildClient(): Client return $this->client ??= new Client( $this->config, null, - null, $this->buildBuilder(), $this->buildIndexNameMapper() ); diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php index 344c33a..54aa2b2 100644 --- a/src/IndexBuilder.php +++ b/src/IndexBuilder.php @@ -14,10 +14,8 @@ use Elastica\Exception\ExceptionInterface; use Elastica\Exception\RuntimeException; use Elastica\Reindex; -use Elastica\Request; use Elastica\Response; use Elastica\Task; -use Elasticsearch\Endpoints\Cluster\State; use JoliCode\Elastically\Mapping\MappingProviderInterface; class IndexBuilder @@ -64,7 +62,9 @@ public function markAsLive(Index $index, string $indexName): Response $data['actions'][] = ['remove' => ['index' => $indexPrefixedName . '*', 'alias' => $indexPrefixedName]]; $data['actions'][] = ['add' => ['index' => $index->getName(), 'alias' => $indexPrefixedName]]; - return $this->client->request('_aliases', Request::POST, $data); + return $this->client->toElasticaResponse( + $this->client->indices()->updateAliases(['index' => $indexName, 'body' => $data]) + ); } /** @@ -119,13 +119,11 @@ public function purgeOldIndices(string $indexName, bool $dryRun = false): array { $indexName = $this->indexNameMapper->getPrefixedIndex($indexName); - $stateRequest = new State(); - $stateRequest->setParams([ + $state = $this->client->cluster()->state([ 'filter_path' => 'metadata.indices.*.state,metadata.indices.*.aliases', ]); - $indexes = $this->client->requestEndpoint($stateRequest); - $indexes = $indexes->getData(); + $indexes = $this->client->toElasticaResponse($state)->getData(); $indexes = $indexes['metadata']['indices']; foreach ($indexes as $realIndexName => &$data) { diff --git a/src/Indexer.php b/src/Indexer.php index 506779b..504c644 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -156,6 +156,11 @@ public function setBulkRequestParams(array $bulkRequestParams): void $this->refreshBulkRequestParams(); } + public function getClient(): Client + { + return $this->client; + } + protected function getCurrentBulk(): Bulk { if (!$this->currentBulk) { diff --git a/src/Messenger/IndexationRequestHandler.php b/src/Messenger/IndexationRequestHandler.php index ccde51c..6656bb2 100644 --- a/src/Messenger/IndexationRequestHandler.php +++ b/src/Messenger/IndexationRequestHandler.php @@ -17,7 +17,6 @@ use JoliCode\Elastically\Client; use JoliCode\Elastically\Indexer; use JoliCode\Elastically\IndexNameMapper; -use Psr\Log\NullLogger; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\MessageBusInterface; @@ -50,9 +49,6 @@ public function __construct(Client $client, MessageBusInterface $bus, DocumentEx $this->exchanger = $exchanger; $this->indexer = $indexer; $this->indexNameMapper = $indexNameMapper; - - // Disable the logs for memory concerns - $this->client->setLogger(new NullLogger()); } /** diff --git a/src/Transport/HttpClientTransport.php b/src/Transport/HttpClientTransport.php deleted file mode 100644 index 1288894..0000000 --- a/src/Transport/HttpClientTransport.php +++ /dev/null @@ -1,151 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace JoliCode\Elastically\Transport; - -use Elastica\Connection; -use Elastica\Exception\Connection\HttpException; -use Elastica\Exception\ConnectionException; -use Elastica\Exception\ExceptionInterface; -use Elastica\Exception\PartialShardFailureException; -use Elastica\Exception\ResponseException; -use Elastica\JSON; -use Elastica\Request; -use Elastica\Request as ElasticaRequest; -use Elastica\Response; -use Elastica\Transport\AbstractTransport; -use Elastica\Util; -use Symfony\Component\HttpClient\Exception\ClientException; -use Symfony\Component\HttpClient\Exception\ServerException; -use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -/** - * Implement Symfony HttpClient as an Elastica Transport. - */ -class HttpClientTransport extends AbstractTransport -{ - private HttpClientInterface $client; - - /** - * Elastica Connection does not have this option. - */ - private string $scheme; - - public function __construct(HttpClientInterface $client, string $scheme = 'http', ?Connection $connection = null) - { - parent::__construct($connection); - - $this->client = $client; - $this->scheme = $scheme; - } - - /** - * @throws ExceptionInterface - * @throws TransportExceptionInterface - * @throws ClientExceptionInterface - * @throws RedirectionExceptionInterface - * @throws ServerExceptionInterface - */ - public function exec(Request $request, array $params): Response - { - $connection = $this->getConnection(); - - $headers = $connection->hasConfig('headers') && \is_array($connection->getConfig('headers')) - ? $connection->getConfig('headers') - : []; - $headers['Content-Type'] = $request->getContentType(); - - $options = [ - 'headers' => $headers, - ]; - - $data = $request->getData(); - $method = $request->getMethod(); - if (!empty($data) || '0' === $data) { - if (ElasticaRequest::GET === $method) { - $method = ElasticaRequest::POST; - } - - if (\is_array($data)) { - $options['body'] = JSON::stringify($data, \JSON_UNESCAPED_UNICODE); - } else { - $options['body'] = $data; - } - } - - if ($connection->getTimeout()) { - $options['timeout'] = $connection->getTimeout(); - } - - $proxy = $connection->getProxy(); - if (null !== $proxy) { - $options['proxy'] = $proxy; - } - - try { - $response = $this->client->request($method, $this->_getUri($request, $connection), $options); - $elasticaResponse = new Response($response->getContent(), $response->getStatusCode()); - } catch (ClientException|ServerException $e) { // Error 4xx and 5xx - $elasticaResponse = new Response($response->getContent(false), $response->getStatusCode()); - } catch (HttpExceptionInterface $e) { - throw new HttpException($e->getCode(), $request); - } catch (TransportExceptionInterface $e) { - throw new ConnectionException($e->getMessage(), $request); - } - - if ($connection->hasConfig('bigintConversion')) { - $elasticaResponse->setJsonBigintConversion($connection->getConfig('bigintConversion')); - } - - $elasticaResponse->setTransferInfo($response->getInfo()); - - if ($elasticaResponse->hasError()) { - throw new ResponseException($request, $elasticaResponse); - } - - if ($elasticaResponse->hasFailedShards()) { - throw new PartialShardFailureException($request, $elasticaResponse); - } - - return $elasticaResponse; - } - - protected function _getUri(Request $request, Connection $connection): string - { - $url = $connection->hasConfig('url') ? $connection->getConfig('url') : ''; - - if (!empty($url)) { - $baseUri = $url; - } else { - $baseUri = $this->scheme . '://' . $connection->getHost() . ':' . $connection->getPort() . '/' . $connection->getPath(); - } - - $requestPath = $request->getPath(); - if (!Util::isDateMathEscaped($requestPath)) { - $requestPath = Util::escapeDateMath($requestPath); - } - - $baseUri .= $requestPath; - - $query = $request->getQuery(); - - if (!empty($query)) { - $baseUri .= '?' . http_build_query($this->sanityzeQueryStringBool($query)); - } - - return $baseUri; - } -} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 5e46f29..b6d12c5 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -13,6 +13,8 @@ namespace JoliCode\Elastically\Tests; +use Elastica\Request; +use Http\Discovery\Psr17Factory; use JoliCode\Elastically\Client; use JoliCode\Elastically\Factory; use PHPUnit\Framework\TestCase; @@ -21,7 +23,7 @@ abstract class BaseTestCase extends TestCase { protected function setUp(): void { - $this->getFactory()->buildClient()->request('*', 'DELETE'); + $this->getClient()->sendRequest((new Psr17Factory())->createRequest(Request::DELETE, '*')); } protected function getFactory(?string $path = null, array $config = []): Factory @@ -29,7 +31,7 @@ protected function getFactory(?string $path = null, array $config = []): Factory return new Factory($config + [ Factory::CONFIG_MAPPINGS_DIRECTORY => $path ?? __DIR__ . '/configs', 'log' => false, - 'port' => '9999', + 'hosts' => ['http://127.0.0.1:9999'], ]); } diff --git a/tests/Bridge/Symfony/DependencyInjection/ElasticallyExtensionTest.php b/tests/Bridge/Symfony/DependencyInjection/ElasticallyExtensionTest.php index bb3e027..cde737c 100644 --- a/tests/Bridge/Symfony/DependencyInjection/ElasticallyExtensionTest.php +++ b/tests/Bridge/Symfony/DependencyInjection/ElasticallyExtensionTest.php @@ -211,7 +211,9 @@ public function testWithTransport(): void 'connections' => [ 'default' => [ 'client' => [ - 'transport' => HttpClientTransport::class, + 'transport_config' => [ + 'http_client' => '@psr_es_client' + ], ], 'mapping_directory' => __DIR__, 'index_class_mapping' => ['foobar' => self::class], @@ -222,7 +224,7 @@ public function testWithTransport(): void $container->compile(); $configArgument = $container->getDefinition('elastically.default.client')->getArgument('$config'); - $this->assertInstanceOf(Reference::class, $configArgument['transport']); + $this->assertInstanceOf(Reference::class, $configArgument['transport_config']['http_client'] ?? null); } private function buildContainer(): ContainerBuilder diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php index f17f612..76b7a82 100644 --- a/tests/IndexBuilderTest.php +++ b/tests/IndexBuilderTest.php @@ -13,9 +13,9 @@ namespace JoliCode\Elastically\Tests; +use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastica\Document as ElasticaDocument; use Elastica\Exception\InvalidException; -use Elastica\Exception\ResponseException; use Elastica\Index; use Elastica\Index\Settings; use JoliCode\Elastically\Factory; @@ -175,7 +175,7 @@ public function testPurgeAndCloseOldIndices(IndexBuilder $indexBuilder): void try { $index2->search(); $this->assertFalse(true, 'Search should throw a "closed index" exception.'); - } catch (ResponseException $e) { + } catch (ClientResponseException $e) { $this->assertStringContainsStringIgnoringCase('closed', $e->getMessage()); } } diff --git a/tests/IndexerTest.php b/tests/IndexerTest.php index 8147fbb..ee96917 100644 --- a/tests/IndexerTest.php +++ b/tests/IndexerTest.php @@ -160,8 +160,8 @@ public function testRequestParameters(): void $response = $indexer->flush(); $this->assertInstanceOf(ResponseSet::class, $response); - $transferInfo = $response->getTransferInfo(); - $this->assertStringContainsString('_bulk?refresh=wait_for', $transferInfo['url']); + $query = $indexer->getClient()->getLastRequest()->getUri()->getQuery(); + $this->assertStringContainsString('refresh=wait_for', $query); // Test the same with an invalid pipeline $indexer->setBulkRequestParams([ diff --git a/tests/Jane/JaneTest.php b/tests/Jane/JaneTest.php index 35e4373..39b5343 100644 --- a/tests/Jane/JaneTest.php +++ b/tests/Jane/JaneTest.php @@ -66,8 +66,7 @@ public function testCreateIndexAndSearchWithJaneObject() // Build Elastically Client $elastically = new Client( - ['port' => '9999'], - null, + ['hosts' => ['http://127.0.0.1:9999']], null, $resultSetBuilder, $indexNameMapper diff --git a/tests/Symfony/config.yaml b/tests/Symfony/config.yaml index 4c0d834..1923408 100644 --- a/tests/Symfony/config.yaml +++ b/tests/Symfony/config.yaml @@ -16,6 +16,10 @@ framework: routing: 'JoliCode\Elastically\Messenger\MultipleIndexationRequest': async 'JoliCode\Elastically\Messenger\IndexationRequest': queuing + http_client: + scoped_clients: + elasticsearch.client: + base_uri: 'http://localhost:9999' services: _defaults: @@ -36,6 +40,11 @@ services: arguments: - '@Symfony\Component\Messenger\MessageBusInterface' + psr_es_client: + class: Symfony\Component\HttpClient\Psr18Client + arguments: + $client: '@elasticsearch.client' + JoliCode\Elastically\Client: arguments: $config: diff --git a/tests/Transport/HttpClientTransportTest.php b/tests/Transport/HttpClientTransportTest.php deleted file mode 100644 index 3cb396d..0000000 --- a/tests/Transport/HttpClientTransportTest.php +++ /dev/null @@ -1,172 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace JoliCode\Elastically\Tests\Transport; - -use Elastica\Exception\ExceptionInterface; -use Elastica\ResultSet; -use JoliCode\Elastically\Client; -use JoliCode\Elastically\Factory; -use JoliCode\Elastically\Model\Document; -use JoliCode\Elastically\Tests\BaseTestCase; -use JoliCode\Elastically\Transport\HttpClientTransport; -use Symfony\Component\HttpClient\HttpClient; -use Symfony\Component\HttpClient\MockHttpClient; -use Symfony\Component\HttpClient\Response\MockResponse; - -final class HttpClientTransportTest extends BaseTestCase -{ - public function testBulkPayload(): void - { - $indexName = mb_strtolower(__FUNCTION__); - - $dto = new TestDTO(); - $dto->bar = 'Roses are red'; - $dto->foo = 'Violets are blue'; - - $indexer = $this->getFactory(null, [ - 'transport' => new HttpClientTransport(HttpClient::create()), - ])->buildIndexer(); - - $indexer->scheduleIndex($indexName, new Document('1', $dto)); - $indexer->scheduleIndex($indexName, new Document('2', $dto)); - $indexer->scheduleIndex($indexName, new Document('3', $dto)); - - $responseSet = $indexer->flush(); - - $this->assertTrue($responseSet->isOk()); - } - - public function testCreateIndex(): void - { - $indexBuilder = $this->getFactory(null, [ - Factory::CONFIG_MAPPINGS_DIRECTORY => __DIR__ . '/../configs', - 'log' => false, - 'transport' => new HttpClientTransport(HttpClient::create()), - ])->buildIndexBuilder(); - - $index = $indexBuilder->createIndex('beers'); - $response = $indexBuilder->markAsLive($index, 'beers'); - - $this->assertTrue($response->isOk()); - } - - public function testHttpClientIsCalledOnSearch() - { - $responses = [ - new MockResponse( - <<<'JSON' - { - "took" : 1, - "timed_out" : false, - "hits" : { - "total" : { - "value" : 0, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ ] - } - } - JSON - ), - ]; - - $client = $this->getClient(null, [ - 'log' => false, - 'transport' => new HttpClientTransport(new MockHttpClient($responses)), - ]); - - $results = $client->getIndex(__FUNCTION__)->search(); - - $this->assertInstanceOf(ResultSet::class, $results); - $this->assertSame(0, $results->getTotalHits()); - } - - public function testHttpClientHandleErrorIdentically() - { - $clientHttpClient = $this->getClient(null, [ - 'log' => false, - 'transport' => new HttpClientTransport(HttpClient::create()), - ]); - - $clientNativeTransport = $this->getClient(null, [ - 'log' => false, - 'transport' => new HttpClientTransport(HttpClient::create()), - ]); - - $this->runOnBothAndCompare($clientHttpClient, $clientNativeTransport); - - $clientHttpClient = $this->getClient(null, [ - 'log' => false, - 'host' => 'MALFORMED:828282', - 'transport' => new HttpClientTransport(HttpClient::create()), - ]); - - $clientNativeTransport = $this->getClient(null, [ - 'host' => 'MALFORMED:828282', - 'log' => false, - ]); - - $this->runOnBothAndCompare($clientHttpClient, $clientNativeTransport); - - $clientHttpClient = $this->getClient(null, [ - 'log' => false, - 'proxy' => '127.0.0.1:9292', - 'transport' => new HttpClientTransport(HttpClient::create()), - ]); - - $clientNativeTransport = $this->getClient(null, [ - 'proxy' => '127.0.0.1:9292', - 'log' => false, - ]); - - $this->runOnBothAndCompare($clientHttpClient, $clientNativeTransport); - } - - private function runOnBothAndCompare(Client $clientHttpClient, Client $clientNative) - { - try { - $clientHttpClient->getIndex(__FUNCTION__)->search(); - $this->assertFalse(true, 'No exception thrown by HttpClient!'); - - return; - } catch (\PHPUnit\Exception $e) { - throw $e; - } catch (\Exception $e) { - $httpClientException = $e; - } - - try { - $clientNative->getIndex(__FUNCTION__)->search(); - - $this->assertFalse(true, 'No exception thrown by Native Client!'); - - return; - } catch (\PHPUnit\Exception $e) { - throw $e; - } catch (\Exception $e) { - $nativeException = $e; - } - - $this->assertInstanceOf(ExceptionInterface::class, $nativeException); - $this->assertInstanceOf(ExceptionInterface::class, $httpClientException); - $this->assertInstanceOf($httpClientException::class, $nativeException); - } -} - -class TestDTO -{ - public $foo; - public $bar; -}