ulff/elasticsearch-php-client-bundle
A Symfony bundle that integrates the official Elasticsearch PHP client, providing service configuration and convenient client access for building Elasticsearch-powered search features in Symfony applications.
Install and register. The bundle is a thin Symfony 3 wrapper around elasticsearch/elasticsearch 2.x, so the dependency itself is pulled in by Composer:
composer require ulff/elasticsearch-php-client-bundle
Then register it in app/AppKernel.php:
$bundles = [
// ...
new Ulff\ElasticsearchPhpClientBundle\UlffElasticsearchPhpClientBundle(),
];
Where to look first. Before writing any YAML, open three files in the bundle — they are the documentation:
DependencyInjection/Configuration.php — the authoritative config tree (hosts, logging, etc.). Do not trust blog posts over this file.DependencyInjection/UlffElasticsearchPhpClientBundleExtension.php — shows exactly which ClientBuilder calls are wired from config. Anything not called here (retries, SSL, connection pool tuning) is not configurable via the bundle.Resources/config/services.yml (or .xml) — the service id you will actually inject/alias. Expect something like ulff.elasticsearch_php_client.client aliased to Elasticsearch\Client.Minimal config in app/config/config.yml:
ulff_elasticsearch_php_client:
hosts:
- '%elasticsearch_host%'
First use case — inject the client and index a document. In Elasticsearch-PHP 2.x, type is mandatory (types were removed only in 7.x):
use Elasticsearch\Client;
class ProductIndexer
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function index(array $product): array
{
return $this->client->index([
'index' => 'products',
'type' => 'product',
'id' => $product['id'],
'body' => $product,
]);
}
}
If autowiring is not in play (it usually is not on Symfony 3 for third-party services), wire it explicitly:
services:
app.product_indexer:
class: AppBundle\Service\ProductIndexer
arguments: ['@ulff.elasticsearch_php_client.client']
Wrap, don't scatter. Treat the injected Elasticsearch\Client as infrastructure and hide it behind your own small services (ProductIndexer, ProductSearch, IndexManager). The client's index()/search() signatures are verbose; centralising them gives you one place to change when you upgrade off ES 2.x — which you eventually must.
Config from the environment. Symfony 3.2+ resolves %env()% inside bundle config, so per-environment hosts work without a custom parameters.yml dance:
ulff_elasticsearch_php_client:
hosts:
- '%env(ELASTICSEARCH_HOST)%'
Search with a body array. Do not use the deprecated query() / filter() top-level methods; ES 2.x expects a body. A reusable search method:
public function search(string $query, int $from = 0, int $size = 20): array
{
return $this->client->search([
'index' => 'products',
'type' => 'product',
'body' => [
'from' => $from,
'size' => $size,
'query' => [
'bool' => [
'must' => ['multi_match' => ['query' => $query, 'fields' => ['name^2', 'description']]],
],
],
],
]);
}
Bulk for anything beyond a handful of documents. One HTTP request per document destroys ingest throughput; build an actions array and call bulk() once per batch (500–1000 actions is a sane default). Remember the trailing newline on body is not required in PHP bulk arrays (unlike the cURL API), but the _index/_type per action is.
Idempotent index creation. Do it in a console command wrapped in a try/catch for Elasticsearch\Common\Exceptions\Missing404Exception / IndexAlreadyExistsException, so deploys can run it repeatedly:
try {
$client->indices()->create(['index' => 'products', 'body' => $mapping]);
} catch (Elasticsearch\Common\Exceptions\BadRequest400Exception $e) {
// already exists — inspect $e->getMessage() if you need to be certain
}
Health check command. A tiny app:es:ping command calling $client->ping() (and optionally $client->cluster()->health()) is the fastest way to catch bad host config before your first real request fails inside a controller.
Testing. Because the bundle is just DI glue, unit tests should mock Elasticsearch\Client (it is not final) or, better, mock the interface you wrap around it. For integration tests, run a real ES 2.x container — the PHP client's behaviour around 404s and mapping errors is not worth simulating badly.
Everything here is EOL. Elasticsearch-PHP 2.x, Symfony 3.x, and this bundle (last release March 2018) are all unsupported. The API surface of elasticsearch/elasticsearch 2.x (type parameters, filtered query, missing filter) will not work against ES 6+. Adopt this bundle only for a legacy system you are maintaining, and plan the exit.
No autowiring, no alias by default. The extension typically registers the client under a bundle-specific id but may not alias it to Elasticsearch\Client. Check services.yml; if there is no alias, add one in your own services.yml:
services:
Elasticsearch\Client:
alias: ulff.elasticsearch_php_client.client
public: false
Otherwise constructor injection by type-hint silently fails with "cannot autowire argument".
The config tree is small — read it, don't guess. Common surprises: host entries are required even in tests (no "do nothing" default), and settings like retries, connectionParams, sslVerification are usually not exposed. If you need them, decorate or define your own client service rather than forking the bundle — the bundle is a few dozen lines and a fork will drift.
Do not new ClientBuilder inside your services. You will bypass the configured hosts/logger and get a client that ignores config.yml, which is a maddening bug to diagnose (requests go to localhost:9200 while your parameters say otherwise).
Logging. If the bundle wires a logger, it uses the Symfony logger service. In dev you will see every request logged including bodies — useful for debugging malformed queries, but noisy and slow in test. Verify the extension's logger wiring before assuming debug output exists; some versions of this bundle never call setLogger() at all.
Exception handling is class-based, not status-based in the code. Catch the specific Elasticsearch\Common\Exceptions\* classes (Missing404Exception, BadRequest400Exception, NoNodesAvailableException). Missing404Exception is thrown for a missing document; a missing index surfaces as IndexNotFoundException (a 404 too), and conflating the two is the classic search-page 500.
indices()->refresh() after writes in tests. ES near-real-time means the document is not searchable until the refresh interval (1s) elapses. Integration tests that index then search fail intermittently without an explicit refresh — and only intermittently, which is worse.
Bulk responses lie by status code. A 200 from bulk() does not mean success; inspect $response['errors'] and the per-item status values. Half your batch can be rejected (mapping conflicts, version conflicts) while the request returns 200.
Extension points are thin. Since there is no compiler pass or event system in this bundle, your realistic extension points are (a) override the service id in your services.yml, (b) define your own Client-producing factory and inject that instead, or (c) decorate a bundle-provided client. Decoration is usually cleanest because it preserves the bundle's config while letting you add retry or metrics layers.
Debugging connection issues. Enable debug in the bundle config (if present) or call setTracer on the underlying ClientBuilder in a local script; the client's own exception messages include the resolved host list, which quickly distinguishes "config not picked up" from "node down". Also confirm you are not pointing a 2.x client at an ES 6+/7+ node — the handshake will fail with confusing parse errors rather than a version mismatch message.
How can I help you explore Laravel packages today?