elastification/php-client-bundle
Symfony bundle that integrates the Elastification PHP Elasticsearch client, with configurable HTTP or Thrift transports, native/JMS serializers, and ready-made index, search, document, and cat repositories.
Reality check first: this is a Symfony2 bundle, not a Laravel package. There is no service provider, no facade, no config/elastification.php auto-publish. What you are actually getting is a thin DI/config wrapper around elastification/php-client (the framework-agnostic client with SearchRepository, IndexRepository, DocumentRepository) plus a Symfony Extension and services.xml. In a Laravel app you will be using perhaps 10% of the bundle and writing the glue yourself. Score/star reality (0 stars, 0.03) plus the ES 1.4-era API means you should treat this as a reference implementation to borrow patterns from, not a drop-in dependency.
Minimal path:
composer require elastification/php-client-bundle "<1.0" — expect fights, see Gotchas.guzzlehttp/guzzle "<7.0" for HTTP, or munkie/elasticsearch-thrift-php ~1.4 for Thrift.ElastificationPhpClientBundle\DependencyInjection\ElastificationPhpClientExtension and its services.xml/Configuration class — those tell you exactly which services exist, which constructor args they take, and how the serializer DIC ids are wired.ElastificationServiceProvider that reads config/elastification.php and binds the same services by hand.Where to look first, in order:
DependencyInjection/Configuration.php — the authoritative config schema (host, port, protocol, elasticsearch_version, the three *_repository_serializer_dic_id keys).Resources/config/services.xml — the service ids the bundle registers; these are the strings you'll be mapping into Laravel's container.elastification/php-client repo — this is where the real API lives (ClientInterface, repositories, serializers, transports). Everything day-to-day happens there.First use case — index-and-search a model:
// config/elastification.php
return [
'host' => env('ES_HOST', '127.0.0.1'),
'port' => env('ES_PORT', 9200),
'protocol' => 'http',
'elasticsearch_version' => env('ES_VERSION', '1.4.1'),
'search_repository_serializer_dic_id' => 'elastification.serializer.native',
'index_repository_serializer_dic_id' => 'elastification.serializer.native',
'document_repository_serializer_dic_id' => 'elastification.serializer.native',
];
Then in a repository class: build the transport → client → DocumentRepository->create(...), and SearchRepository->search(...) with a raw ES query array. That is the whole loop.
Container bridging. The bundle resolves serializer/transport services by string id against the Symfony container. In Laravel, bind those same ids in your provider so the ids in config stay meaningful:
$this->app->bind('elastification.serializer.native', NativeSerializer::class);
$this->app->bind('elastification.transport.http', fn () => new GuzzleTransport(...));
Then in your provider, resolve the ids from config('elastification.*_serializer_dic_id') at boot — don't hardcode the class. That preserves the bundle's one genuinely useful idea: serializers are swappable per repository (search vs index vs document).
Repository-per-concern. The bundle splits into three repositories; keep that split rather than wrapping everything in one god-service:
IndexRepository — index create/delete/mapping/aliases. Use it in migrations or an artisan command.DocumentRepository — single-document index/update/delete/get. Use it from Eloquent observers or queued jobs.SearchRepository — queries only. Use it from read paths/controllers.Eloquent serializer. The default native serializer deals in arrays. For day-to-day Laravel ergonomics, implement the upstream serializer interface to map documents ↔ Eloquent models and register it as App\Search\EloquentSerializer, then point document_repository_serializer_dic_id at it. This is the highest-value customization and the reason the serializer indirection exists.
Decouple writes from the request. Wire DocumentRepository into a SearchIndexJob dispatched from model events (saved, deleted). This keeps ES latency/timeouts off the request path and gives you retries — something the bundle itself has no opinion on.
Config-driven indices. Keep index/type names in per-model config (config/search.php → ['posts' => ['index' => 'posts', 'type' => 'post']]) rather than constants, so test environments can point at posts_test and you can reindex via alias swap.
Testing. Because transports are bound by id, swap in a fake transport (or a mock ClientInterface) in your test provider. Keep one real integration test against a live cluster gated behind an env flag — the serializers are exactly the kind of thing that silently mangles document shapes.
Guzzle <7.0 is a hard blocker on modern Laravel. Laravel 8+ ships Guzzle 7. Composer will either fail resolution or downgrade half your dependency tree. Verify resolution before writing any application code; if it can't be satisfied, don't force it — take the upstream elastification/php-client (or nothing) and implement transport yourself with Guzzle 7's handler stack.
Elasticsearch 1.4 semantics. elasticsearch_version: 1.4.1 in the config is not cosmetic — it shapes request paths and query compatibility. _type and the old query DSL do not exist in ES 6+/7+. If your cluster is modern, this bundle is the wrong tool; use elasticsearch/elasticsearch v7/8 or Laravel Scout.
Serializer DIC ids are Symfony service ids. Defaults like elastification_php_client.serializer.native only resolve if something registers them. If you copy config without registering those ids in Laravel's container, you get "target not instantiable"/class-not-found at first use, not at boot. Bind them eagerly in register().
Don't boot it through the bundle class. ElastificationPhpClientBundle::boot() assumes a Symfony kernel and ContainerInterface. Instantiate the underlying client/services directly; treat the bundle as documentation.
Thrift is a different animal. munkie/elasticsearch-thrift-php needs the PHP Thrift extension compiled in. If protocol: thrift fails in CI with a class-not-found, that's the missing extension — fall back to http and don't chase the bundle.
Debugging: since there's no Laravel logging integration, wrap the transport in a decorator that logs the method + path + payload at debug level. Most failures here are host/port/protocol typos or a cluster version mismatch, and the client's exceptions are thin — the logged request is what actually tells you what happened.
Freshly indexed data isn't searchable immediately. That's ES refresh behavior, not a bug in the bundle. Use refresh=true on writes in tests, or refresh=wait_for where supported, rather than sleeping.
Abandonware risk. Zero dependents, zero stars, README-only maturity, PHP 5-era constraints. Treat any code you write against it as owning the client: pin exact versions, isolate it behind your own SearchClient interface so you can swap the implementation later, and don't let the bundle's types leak into controllers or models.
Extension points worth using: custom serializer by id (per repository), custom transport by id, and the elasticsearch_version config knob. Everything else you'll build in Laravel-land, and that's fine — the query arrays and repository split are the transferable ideas.
How can I help you explore Laravel packages today?