jbafford/elastica-extra-bundle
jbafford/elastica-extra-bundle is a Symfony bundle that layers extra services, helpers and integration glue on top of the Elastica Elasticsearch client, making it easier to register clients and wire search features into a Symfony application's service container.
Confirm the mismatch before writing code. This is a Symfony bundle: it registers through config/bundles.php, loads services via a DependencyInjection/Extension class, and may run compiler passes. Laravel has none of those lifecycle hooks. composer require jbafford/elastica-extra-bundle will not register anything automatically — there is no extra.laravel.providers. Treat the install as reference source only until you've read it, and decide between (a) vendoring specific helpers into app/Services/Search/ or (b) rebuilding the wiring directly. Do not try to boot the bundle inside Laravel.
Where to look first, in this order:
composer show jbafford/elastica-extra-bundle to see the pinned ruflin/elastica constraint and Symfony component versions. This tells you immediately whether the Elastica generation supports your target Elasticsearch cluster.DependencyInjection/ — the extension's load() method and any compiler passes. This is the bundle's actual value: a list of service definitions and how they're assembled. Everything else is plumbing you can ignore.Resources/config/*.xml|yml — the raw service definitions. These are your migration checklist.First working use case (about 30 minutes). Bind Elastica\Client as a singleton in a Laravel service provider, create an index, insert one document, and query it back. This confirms the client connects to your cluster before you invest in porting helper logic:
// app/Providers/ElasticServiceProvider.php
public function register(): void
{
$this->app->singleton(Client::class, function ($app) {
return new Client([
'connections' => [
[
'host' => config('elastic.host'),
'port' => config('elastic.port'),
],
],
]);
});
}
// config/elastic.php — translate the bundle's semantic config tree here
return [
'host' => env('ELASTIC_HOST', 'localhost'),
'port' => (int) env('ELASTIC_PORT', 9200),
];
$index = app(Client::class)->getIndex('blog');
$index->create(['settings' => [...], 'mappings' => [...]], true);
$index->addDocument(new \Elastica\Document('1', ['title' => 'Hello']));
$index->refresh();
$results = $index->search('hello')->getResults();
If that spike fails on a version mismatch, you've learned the most important thing in half an hour.
Bind the client once, inject it everywhere. A singleton binding plus constructor injection into repositories is the whole pattern. Never let Elastica\Client or Elastica\Index types cross into controllers — wrap them:
class PostRepository
{
public function __construct(private Client $client) {}
public function index(Post $post): void
{
$this->client->getIndex('posts')->addDocument(
new \Elastica\Document($post->id, $post->toSearchableArray())
);
}
public function search(string $q, int $page = 1, int $perPage = 20): array
{
$query = new \Elastica\Query(new \Elastica\Query\MultiMatch($q, [
'fields' => ['title^2', 'body'],
]));
$query->setFrom(($page - 1) * $perPage)->setSize($perPage);
return $this->client->getIndex('posts')->search($query)->getHits();
}
}
Model integration on Eloquent events, not on the bundle's persister. The bundle almost certainly ships a persister/hydrator pattern tied to Symfony's event dispatcher. In Laravel, the idiomatic equivalent is observer-driven: Post::saved → call PostRepository::index(), Post::deleted → deleteDocument(). Queue the write (ShouldQueue listener) so an Elasticsearch outage does not roll back a database transaction.
Translate the semantic config, key by key. Walk the bundle's Configuration::getConfigTreeBuilder() and write a flat config/elastic.php that mirrors it. Anything that reads $container->getParameter(...) becomes config('elastic....'). Anything that resolves environment-specific index names (the bundle's "index per environment" convenience is typically its most useful contribution) becomes:
'index_prefix' => env('APP_ENV') === 'production' ? '' : env('APP_ENV') . '_',
That one line is often the actual reason to read this package.
Reproduce compiler passes with tagged bindings. If the bundle tags services (e.g. elastica.index tags collected into a registry), Laravel's equivalent is $this->app->tag([PostIndexer::class], 'search.indexers') and resolving with $this->app->tagged('search.indexers'). Don't port the compiler pass mechanism — port the outcome.
Reindexing as a first-class command. Give yourself an artisan command early: create a new index with a versioned name, bulk-index from the database with Elastica\Bulk, then swap an alias. Elastica\Client supports bulk operations natively; wrap that in a command and schedule it.
Test against a real cluster. Elastica has no in-memory fake worth relying on. Use a containerized Elasticsearch at your exact target version (Testcontainers or a docker-compose service) and run a small contract suite: index a doc, query it, assert the hit shape. This is also your version-compatibility canary.
types are a trap on ES 7+. Much of the Elastica-era API is organized around index types, which Elasticsearch removed. If the bundle's helpers take (index, type) tuples, they will not translate. Check this in the source before anything else.artisan vendor:publish. Everything is manual. Register the provider in config/app.php (or bootstrap/providers.php on Laravel 11) and expect to hand-write all config.composer require explodes with an unrelated-looking constraint error, that's the cause — don't chase phantom bugs.config:cache will freeze the client config. Any dynamically computed host (e.g. from a service discovery call) must be resolved in the binding closure, not at config-load time, or cache clearing becomes part of your deploy.$index->refresh() in non-production before asserting in tests, and dump ->getQuery()->toArray() — the Elastica query builder silently drops some nested clauses if the DSL shape doesn't match the client version.SearchRepository) in front of everything. That single decision means you can swap Elastica for the official elasticsearch/elasticsearch client — a realistic near-term move given zero upstream dependents and a bus factor of one — without touching application code."jbafford/elastica-extra-bundle": "x.y.*") and record the decision in an ADR. There is no upstream to notify you of a breaking change; your pin is the safety net.How can I help you explore Laravel packages today?