shieldo/solarium-async-plugin
A Solarium plugin that adds asynchronous query support to the PHP Solr client, letting you dispatch multiple requests concurrently and collect responses later. Useful for speeding up batch searches and reducing wait time when querying Solr from Laravel or plain PHP applications.
Before writing a line of code, verify the compatibility window. This plugin targets the HTTPlug era of Solarium (php-http/httplug, Http\Promise\Promise as the async contract). Solarium 6+ moved to PSR-18/PSR-7, so the plugin is a non-starter there. Read the package's composer.json first and pin solarium/solarium to whatever major it declares — do not let composer update drift you across that boundary.
Install the plugin plus an adapter that actually implements Http\Client\HttpAsyncClient:
composer require shieldo/solarium-async-plugin \
php-http/guzzle7-adapter \
solarium/solarium:^5.0
A sync-only PSR-18 client will install fine and then explode at runtime on sendAsyncRequest(). Confirm the async interface before you build on it.
Wire it up — the plugin registers like any Solarium plugin, either via config ('plugin' => ['async' => ['plugin' => ...]]) or programmatically:
use Solarium\Client;
use Solarium\Core\Client\Adapter\Http;
use Http\Adapter\Guzzle7\Client as GuzzleAdapter;
use Symfony\Component\EventDispatcher\EventDispatcher;
$adapter = new Http(new GuzzleAdapter(new \GuzzleHttp\Client()), new EventDispatcher());
$client = new Client($adapter, new EventDispatcher());
$client->registerPlugin('async', new \Shieldo\SolariumAsyncPlugin\AsyncPlugin());
Where to look first: the plugin class extends Solarium\Core\Plugin\AbstractPlugin; read its initPlugin() to see which Solarium\Core\Event\Events hooks it attaches to, then read its public methods to confirm the async entry point signature before you build abstractions around it.
First use case: fan-out on a search page. One page needs a select for documents, a second select with a different filter for facet counts, and a suggest query. Sequentially that's three round-trips; dispatched together and waited on at the end, it's one.
Fan-out / fan-in. Dispatch everything first, resolve afterwards. The requests are in flight the moment you call the async execute; wait() only blocks for completion, so resolve in a second loop:
$promises = [];
foreach ($queries as $key => $query) {
$promises[$key] = $plugin->executeAsync($query); // confirm exact method name in src/
}
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = $promise->wait(); // Solarium\QueryType\*\Result
}
Bound the concurrency. Do not fan out 200 queries. Solr queues them, your client holds sockets, and a slow query starves the rest. Chunk into batches of 5–10 and \Http\Promise\settle() or wait per batch.
Non-blocking callbacks. When you want to process as results land rather than after the last one, hang handlers off the promise:
$promise->then(
fn ($result) => $this->hydrate($result),
fn (\Throwable $e) => $this->logger->error('solr async failed', ['e' => $e])
);
Shared client, one registration. Register the plugin once in your DI container alongside the Solarium\Client singleton. The plugin is effectively stateless; rebuilding clients per request costs you warm connections.
Test with a mock async client. Http\Mock\Client implements the async interface and returns already-resolved promises, so you can assert on wait() results synchronously and keep the suite deterministic.
Bridge to your request pipeline. Because Solarium's response parsing runs inside the promise resolution, a then() chain is the natural place to push normalized hits into your own DTOs, keeping Solarium result objects out of your controllers.
AdapterInterface::createRequest() and sending them with your own async client. Roughly the same benefit, no unmaintained dependency.php-http/guzzle7-adapter implements both sync and async; some clients only implement HttpClient. The failure is a runtime exception at first dispatch, not a Composer conflict. Check instanceof HttpAsyncClient in a boot-time assertion.wait(). "Fire and forget, collect later in a cron" is not what this gives you.Result objects are incomplete until wait(). Calling getNumFound() on a half-resolved result throws or returns garbage. Only touch results inside the resolution path.\Throwable at the wait() boundary, inspect getPrevious(), and log the raw PSR-7 response body in the rejection handler — a Solr 400 is far easier to diagnose from the body than from a generic transport exception.path or VCS repository if you need a patch, and put a smoke test around the async execute path so a Solarium or adapter bump fails loudly in CI rather than in production.initPlugin() to attach to Events::PRE_EXECUTE_REQUEST yourself — that's the seam where request creation and dispatch are separated, and it's the least invasive place to add metrics, retries, or endpoint routing.How can I help you explore Laravel packages today?