Skip to content

Commit 74673b7

Browse files
handle Curl transport & PSR-18 client
1 parent 92edc66 commit 74673b7

8 files changed

Lines changed: 178 additions & 4 deletions

File tree

CHANGELOG-7.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ in 7.x versions.
66

77
### 7.1.1 (2026-xx-xx)
88
* Added Elasticsearch 9 and Elastica 9 support.
9+
* `headers` client config is now applied via `Transport::setHeader()` so it works uniformly across Guzzle, Symfony HTTP Client, and elastic-transport's bundled Curl client.
10+
* `timeout` client config is now translated to `CURLOPT_TIMEOUT` at runtime when the active transport client is elastic-transport's bundled Curl — Guzzle/Symfony HTTP Client keep consuming the original `'timeout'` key as before.
11+
* Default config no longer injects `'headers' => []` / `'timeout' => 30` into `http_client_options`, which would otherwise break elastic-transport's bundled Curl client (PHP 8+ `ValueError` on unknown `curl_setopt_array` keys).
12+
* **Deprecated** the top-level `headers` and `timeout` client config. Move them into `client_options` — for Guzzle/Symfony HTTP Client use `headers` / `timeout`, for the bundled Curl client use `CURLOPT_HTTPHEADER` / `CURLOPT_TIMEOUT`.
913
* Fix deprecated Symfony method call.
1014
* Instantiate custom repositories using the DI service locator.
1115
* Add compatibility with `doctrine/doctrine-bundle` 3.x, `doctrine/phpcr-odm` 3.x and Symfony 8.0 in highest-deps CI.

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"phpstan/phpstan-symfony": "^1.4",
6060
"phpunit/phpunit": "^9.5",
6161
"symfony/expression-language": "^6.4 || ^7.4 || ^8.0",
62+
"symfony/http-client": "^6.4 || ^7.4 || ^8.0",
6263
"symfony/messenger": "^6.4 || ^7.4 || ^8.0",
6364
"symfony/serializer": "^6.4 || ^7.4 || ^8.0",
6465
"symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0",
@@ -86,6 +87,7 @@
8687
"config": {
8788
"allow-plugins": {
8889
"ergebnis/composer-normalize": true,
90+
"php-http/discovery": true,
8991
"phpstan/extension-installer": true
9092
}
9193
},

doc/cookbook/elastica-http-client-configuration.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ Configuring Elastica HTTP client
44
Setting HTTP Headers
55
--------------------
66

7+
> **Deprecated since 7.1**: the top-level `headers` client option is deprecated.
8+
> Move headers into `client_options` for your HTTP client (`headers` key for
9+
> Guzzle/Symfony HTTP Client, `CURLOPT_HTTPHEADER` for elastic-transport's
10+
> bundled Curl client).
11+
712
It may be necessary to set HTTP headers on the Elastica client, for example an
813
Authorization header.
914

10-
They can be set using the `headers` configuration key:
15+
They can be set using the deprecated `headers` configuration key (still works,
16+
applied via `Transport::setHeader` so it covers every HTTP client):
1117

1218
```yaml
1319
# app/config/config.yml
@@ -59,7 +65,7 @@ fos_elastica:
5965
Setting other client options
6066
--------------------
6167

62-
Any other client option for Elastica client can be set using the `client_options` configuration key:
68+
`client_options` is a raw pass-through to the underlying HTTP client (Guzzle, Symfony HTTP Client, or elastic-transport's bundled Curl client). Configure it with whatever option names your client understands:
6369

6470
```yaml
6571
# app/config/config.yml
@@ -68,7 +74,16 @@ fos_elastica:
6874
default:
6975
hosts: ['http://example.com:80']
7076
client_options:
71-
!php/const \CURLOPT_RANDOM_FILE: /dev/urandom
77+
# Guzzle / Symfony HTTP Client
78+
timeout: 30
79+
connect_timeout: 10
7280
proxy: 'http://localhost:8125'
73-
connect_timeout: 10 # if using Guzzle
81+
82+
# elastic-transport's bundled Curl client (used when neither Guzzle nor
83+
# Symfony HTTP Client is installed). Use CURLOPT_* integer keys:
84+
!php/const \CURLOPT_TIMEOUT: 30
85+
!php/const \CURLOPT_CONNECTTIMEOUT: 10
86+
!php/const \CURLOPT_RANDOM_FILE: /dev/urandom
7487
```
88+
89+
> **Deprecated since 7.1:** the top-level `timeout` client option is no longer applied — move it into `client_options` for your specific HTTP client (`timeout` for Guzzle/Symfony, `CURLOPT_TIMEOUT` for bundled Curl).

src/DependencyInjection/Configuration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,12 +356,22 @@ private function addClientsSection(ArrayNodeDefinition $rootNode): void
356356
->prototype('scalar')->end()
357357
->end()
358358
->arrayNode('headers')
359+
->setDeprecated(
360+
'friendsofsymfony/elastica-bundle',
361+
'7.1',
362+
'The "%node%" option is deprecated. Configure headers via "client_options" instead — for Guzzle/Symfony HTTP Client use "headers", for elastic-transport\'s bundled Curl client use CURLOPT_HTTPHEADER.',
363+
)
359364
->normalizeKeys(false)
360365
->useAttributeAsKey('name')
361366
->prototype('scalar')->end()
362367
->end()
363368
->scalarNode('timeout')
364369
->defaultValue(30)
370+
->setDeprecated(
371+
'friendsofsymfony/elastica-bundle',
372+
'7.1',
373+
'The "%node%" option is deprecated. Configure your HTTP client directly via "client_options" instead — "timeout" for Guzzle/Symfony HTTP Client, CURLOPT_TIMEOUT for elastic-transport\'s bundled Curl client.',
374+
)
365375
->end()
366376
->scalarNode('retry_on_conflict')
367377
->defaultValue(0)

src/Elastica/Client.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Elastic\Elasticsearch\Exception\ClientResponseException;
1515
use Elastic\Elasticsearch\Exception\ElasticsearchException;
1616
use Elastic\Elasticsearch\Response\Elasticsearch;
17+
use Elastic\Transport\Client\Curl;
1718
use Elastica\Client as BaseClient;
1819
use Elastica\Exception\ClientException;
1920
use FOS\ElasticaBundle\Event\ElasticaRequestExceptionEvent;
@@ -36,7 +37,55 @@ class Client extends BaseClient implements ResetInterface
3637
{
3738
public function __construct(array|string $config = [], private readonly array $forbiddenCodes = [400, 403, 404], ?LoggerInterface $logger = null)
3839
{
40+
// Reroute deprecated http_client_options.headers via Transport::setHeader so it works for any
41+
// PSR-18 client — elastic-transport's bundled Curl client ValueErrors on string keys in curl_setopt_array.
42+
$headers = [];
43+
if (\is_array($config) && \is_array($config['transport_config']['http_client_options']['headers'] ?? null)) {
44+
$headers = $config['transport_config']['http_client_options']['headers'];
45+
unset($config['transport_config']['http_client_options']['headers']);
46+
}
47+
3948
parent::__construct($config, $logger);
49+
50+
foreach ($headers as $name => $value) {
51+
$this->getTransport()->setHeader($name, $value);
52+
}
53+
54+
$this->translateOptionsForBundledCurl();
55+
}
56+
57+
/**
58+
* When the active transport client is elastic-transport's bundled Curl, swap any 'timeout' string
59+
* key (deprecated bundle alias) to CURLOPT_TIMEOUT — otherwise curl_setopt_array ValueErrors on
60+
* the unknown option.
61+
*/
62+
private function translateOptionsForBundledCurl(): void
63+
{
64+
if (!\class_exists(Curl::class)) {
65+
return;
66+
}
67+
68+
$transportClient = $this->getTransport()->getClient();
69+
if (!$transportClient instanceof Curl) {
70+
return;
71+
}
72+
73+
$optionsProperty = new \ReflectionProperty($transportClient, 'options');
74+
$options = $optionsProperty->getValue($transportClient);
75+
76+
// `timeout` -> CURLOPT_TIMEOUT
77+
if (isset($options['timeout'])) {
78+
$options[\CURLOPT_TIMEOUT] = $options['timeout'];
79+
unset($options['timeout']);
80+
}
81+
82+
// `connect_timeout` -> CURLOPT_CONNECTTIMEOUT
83+
if (isset($options['connect_timeout'])) {
84+
$options[\CURLOPT_CONNECTTIMEOUT] = $options['connect_timeout'];
85+
unset($options['connect_timeout']);
86+
}
87+
88+
$transportClient->setOptions($options);
4089
}
4190

4291
/**

tests/Unit/DependencyInjection/FOSElasticaExtensionTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ public function testYamlConfiguration(): void
7979
], $defaultClientDefinition->getArgument('$config')['transport_config']);
8080
}
8181

82+
/**
83+
* @group legacy
84+
*/
85+
public function testDeprecatedHeadersAndTimeoutAreMergedIntoHttpClientOptionsForBC(): void
86+
{
87+
$containerBuilder = new ContainerBuilder();
88+
$containerBuilder->registerExtension($extension = new FOSElasticaExtension());
89+
$containerBuilder->setParameter('kernel.debug', true);
90+
91+
$loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__.'/fixtures'));
92+
$loader->load('deprecated_headers_timeout.yml');
93+
94+
$extensionConfig = $containerBuilder->getExtensionConfig($extension->getAlias());
95+
$extension->load($extensionConfig, $containerBuilder);
96+
97+
$defaultClientDefinition = $containerBuilder->findDefinition('fos_elastica.client.default');
98+
$this->assertSame([
99+
'headers' => ['Authorization' => 'Bearer xyz'],
100+
'timeout' => 10,
101+
], $defaultClientDefinition->getArgument('$config')['transport_config']['http_client_options']);
102+
}
103+
82104
public function testShouldRegisterDoctrineORMPagerProviderIfEnabled(): void
83105
{
84106
$container = new ContainerBuilder();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
fos_elastica:
2+
clients:
3+
default:
4+
hosts: [http://localhost:9200]
5+
headers:
6+
Authorization: 'Bearer xyz'
7+
timeout: 10
8+
indexes:
9+
test_index:
10+
client: default
11+
properties:
12+
text: ~
13+
persistence:
14+
driver: orm
15+
model: foo_model

tests/Unit/Elastica/ClientTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,63 @@ public function testGetIndexTemplate(): void
234234
$this->assertSame($template, $client->getIndexTemplate('some_index'));
235235
}
236236

237+
/**
238+
* Headers arriving via the deprecated http_client_options['headers'] passthrough are
239+
* extracted in the constructor and re-applied via Transport::setHeader. This makes
240+
* them work uniformly across Guzzle, Symfony HTTP Client, and elastic-transport's
241+
* bundled Curl client (which would otherwise ValueError on the string key in
242+
* curl_setopt_array).
243+
*/
244+
public function testHeadersFromHttpClientOptionsAreReroutedViaTransport(): void
245+
{
246+
$client = new Client([
247+
'transport_config' => [
248+
'http_client_options' => [
249+
'headers' => ['X-Foo' => 'bar', 'Authorization' => 'Bearer xyz'],
250+
'timeout' => 5,
251+
],
252+
],
253+
]);
254+
255+
$headers = $client->getTransport()->getHeaders();
256+
self::assertSame('bar', $headers['X-Foo'] ?? null);
257+
self::assertSame('Bearer xyz', $headers['Authorization'] ?? null);
258+
// timeout is left in place so the underlying HTTP client (Guzzle/Symfony) can consume it
259+
self::assertSame(['timeout' => 5], $client->getConfig()['transport_config']['http_client_options']);
260+
}
261+
262+
/**
263+
* When the active transport client is elastic-transport's bundled Curl, the deprecated
264+
* 'timeout' string key in http_client_options is translated to CURLOPT_TIMEOUT — otherwise
265+
* curl_setopt_array ValueErrors on the unknown option.
266+
*/
267+
public function testTimeoutIsTranslatedToCurlOptForBundledCurl(): void
268+
{
269+
if (!\class_exists(\Elastic\Transport\Client\Curl::class)) {
270+
self::markTestSkipped('Bundled Elastic\Transport\Client\Curl is only available on Elastica 9+.');
271+
}
272+
273+
$client = new Client([
274+
'transport_config' => [
275+
'http_client' => new \Elastic\Transport\Client\Curl(),
276+
'http_client_options' => [
277+
'timeout' => 7,
278+
\CURLOPT_RANDOM_FILE => '/dev/urandom',
279+
],
280+
],
281+
]);
282+
283+
$transportClient = $client->getTransport()->getClient();
284+
self::assertInstanceOf(\Elastic\Transport\Client\Curl::class, $transportClient);
285+
286+
$optionsProperty = new \ReflectionProperty($transportClient, 'options');
287+
$options = $optionsProperty->getValue($transportClient);
288+
289+
self::assertSame(7, $options[\CURLOPT_TIMEOUT] ?? null);
290+
self::assertArrayNotHasKey('timeout', $options);
291+
self::assertSame('/dev/urandom', $options[\CURLOPT_RANDOM_FILE] ?? null, 'other curl options must be preserved');
292+
}
293+
237294
private function getClient(LoggerInterface $logger, \Nyholm\Psr7\Response $response): Client
238295
{
239296
$httpClient = $this->createMock(ClientInterface::class);

0 commit comments

Comments
 (0)