lmc/cqrs-solr
LMC CQRS Solr brings Apache Solr search into Laravel using command-query responsibility segregation, with query buses, indexing and repository abstractions for fast, filtered search across large datasets.
Context first: this package is a small adapter layer on top of
lmc/cqrsthat gives you Solr-flavouredQuery/Commandbase classes plus a handler that speaks to a Solr client. It has 1 star, 0 dependents, and the current line is 3.0.0 (2022-04-27) — a hard BC break over 2.x that requires PHP 8.1 and changes method signatures. Treat it as reference source code you install rather than an actively maintained dependency — readvendor/lmc/cqrs-solr/srcbefore writing anything, and be prepared to pin or fork it.
1. Install and inspect
composer require lmc/cqrs-solr:^3.0
You will pull in lmc/cqrs (the core bus/handler abstractions) and, depending on version constraints, a Symfony bundle. Two things to check before you write a line of code:
php -v # must be >= 8.1 for 3.0
composer show lmc/cqrs-solr lmc/cqrs
ls vendor/lmc/cqrs-solr/src
If you are pinned to PHP 8.0 you cannot use 3.0 at all — you'd be on 2.x, which is now an unmaintained branch with a different set of signatures. Decide that up front; mixing example code from 2.x docs/tutorials into a 3.0 install is a common way to waste an afternoon.
2. Where to look first (in this order)
| Path | Why |
|---|---|
src/Handler/ |
The single entry point where queries/commands become Solr calls. This is the class you will decorate, test, and debug. |
src/Query/, src/Command/ |
The base types you extend. Look at their abstract methods — 3.0 changed these signatures, so that's your per-request contract and your migration checklist. |
src/SolrClient/ |
The SolrClientInterface seam. This is your fake/mock in tests and your extension point for auth, retries, logging. Its method signatures are what your decorators must match post-3.0. |
src/Resources/config/ (or the bundle class) |
Service wiring and the DI tag names you need for autoconfiguration. |
vendor/lmc/cqrs/src |
The HandlerInterface, handler-collection, and callback/cache decorators your Solr handler plugs into. Check whether its constraint floats to a version compatible with 3.0. |
3. First use case — one read query end to end
The shape is: define a query object → let the handler turn it into a Solr request → get a typed response. Verify the exact method names against src/ (3.0's PHP 8.1 rewrite means older examples are unreliable), but the flow is:
// 1. A concrete query extending the package base type
final class FindProductsQuery extends SolrQuery // or implements SolrQueryInterface
{
public function __construct(private string $term, private int $rows = 20) {}
// returns the request path + params the handler will send
public function getRequest(): SolrRequestInterface { /* ... */ }
}
// 2. Dispatch through the core CQRS bus
final class ProductController
{
public function __construct(private QueryBusInterface $queryBus) {}
public function __invoke(string $term): array
{
/** @var SolrResponseInterface $response */
$response = $this->queryBus->query(new FindProductsQuery($term));
return $response->getResult()->getData(); // exact accessor: check src/
}
}
Get one q=*:* query returning numFound before you build anything else. That validates the client config, endpoint URL, and response parsing in one shot.
4. Configuration
You need a Solr endpoint (scheme/host/port/core) reachable from PHP. Confirm how the bundle expects it (env var vs. config/packages/lmc_cqrs_solr.yaml), and hit the endpoint with curl first so you're not debugging Solr connectivity through two abstraction layers. Because 3.0 is the only branch that ever shipped PHP 8.1 support, expect the Symfony bundle config/bundle class to be the least touched part of the codebase — verify DI wiring with debug:container rather than trusting the docs.
Don't build a generic "SolrService". The CQRS shape rewards granularity: FindProductsQuery, CountProductsQuery, IndexProductCommand. Each gets a stable identity, which makes caching, logging, and test fakes trivially scoped to the bus rather than to a god-service.
Keep the raw Solr response inside the handler layer and map to domain DTOs in your query object or a thin mapper. Solr's document shape (response.docs[], highlighting, facet_counts, spellcheck) is noisy — don't leak it into controllers.
final class ProductMapper
{
public function fromDocument(array $doc): Product
{
return new Product(
id: (string) $doc['id'],
name: (string) ($doc['name'][0] ?? ''),
);
}
}
Note Solr's habit of returning everything as arrays — even single-valued fields. Normalize once, in one place. PHP 8.1 support means you can confidently use constructor promotion, readonly, and enums in these DTOs — the package itself now targets that baseline, so your own classes are no longer held to 7.4 idioms.
Write commands (/update, /update/json/docs) go through the same handler with the same client. Two rules:
commit=false bulk index followed by an explicit commit command is dramatically faster than per-document commits and gives you a natural transaction boundary.responseHeader.status = 0 but per-doc errors) deserve their own command class and their own error handling.The SolrClientInterface seam is where you add cross-cutting concerns, because everything funnels through it:
final class LoggingSolrClient implements SolrClientInterface
{
public function __construct(
private SolrClientInterface $inner,
private LoggerInterface $logger,
) {}
public function query(SolrQueryInterface $query): SolrResponseInterface
{
$this->logger->debug('solr.request', ['request' => (string) $query->getRequest()]);
$start = microtime(true);
try {
return $this->inner->query($query);
} finally {
$this->logger->debug('solr.timing', ['ms' => (microtime(true) - $start) * 1000]);
}
}
}
Wire it as a decorator in the container. This survives package upgrades better than subclassing the handler. Post-3.0 caveat: copy the interface method signatures verbatim from vendor/lmc/cqrs-solr/src/SolrClient/ rather than from memory or a blog post — 3.0 changed them, and a mismatched decorator fails at container compile time with a misleading "must be compatible with" error.
lmc/cqrs's existing decoratorsThe core package ships callback/cache handler wrappers. Use them for:
Because these decorators key off the query object, give every query a deterministic identity (see Gotchas).
For reindex jobs (console command + batch loop):
AddDocumentCommands, dispatch with commit disabled.CommitCommand.numFound until it matches the source count.Keep the chunk size as config — Solr's maxBooleanClauses and your PHP memory limit will both have opinions.
Because everything goes through one handler + one client interface, your test suite is cheap:
$client = new class implements SolrClientInterface {
public array $sent = [];
public function query(SolrQueryInterface $q): SolrResponseInterface {
$this->sent[] = $q;
return new FakeSolrResponse(['numFound' => 1, 'docs' => [['id' => '1']]]);
}
};
Assert on $client->sent to verify request construction (params, filters, sort) without a live Solr. Reserve integration tests for query syntax — use a Solr container (solr:9 in CI) for those. Note that anonymous-class fakes written against 2.x signatures will not satisfy 3.0's interfaces; regenerate them from the current src/.
3.0.0 is a BC break with no upgrade guide. The release notes are two lines: PHP 8.1 required, method signatures changed, new language features used. That means any local subclass of the base Query/Command types, any custom handler, and any SolrClientInterface decorator you wrote against 2.x will need auditing — and you'll be doing the diffing yourself. If you are upgrading rather than greenfielding, diff src/Query/, src/Command/, src/Handler/, and src/SolrClient/ between the two tags before touching application code.
Maintenance risk is still the headline gotcha. 3.0.0 is the last release (2022-04-27) and it only bought you PHP 8.1 — not 8.2, 8.3, or 8.4, not Symfony 7, not any Solr 9 client changes. Zero dependents means nobody else is reporting these breakages upstream. Run the test suite against your target PHP/Symfony/Solr matrix before adopting; if it fails, fork it into your own namespace — it's a few hundred lines, and you'll want to own it anyway. The 3.0 release arguably makes forking easier: you're starting from modern PHP and don't have to modernise it yourself.
Solr's default rows=10 will silently truncate you. It is not an error; numFound says 1,000,000 and you get 10 docs. Always set rows explicitly, and treat "results count == 10" as a smell during development.
Deep paging. start beyond ~10,000 gets expensive and Solr may refuse depending on config. For exports, use cursorMark with sort=id asc (or a unique key). The package won't do this for you — your query object has to emit the cursor param and the caller has to loop on nextCursorMark.
Commit vs. soft commit. commit=true on every write will make your indexer look broken under load. Default to commit=false + softCommit batching, and only hard-commit when durability across a restart matters.
HTTP 200 lies. Solr returns 200 OK with responseHeader.status != 0 on many query errors, and update responses can carry per-document errors alongside a happy status. The handler may or may not check this depending on how you've wired the response parser — verify src/ and add an explicit status assertion in your response mapping if it doesn't. This is the single most likely production surprise, and 3.0 did not change the error-handling story, only the signatures around it.
Cache keys. If you wrap queries with the core package's cache handler, the key derives from the query object. Any query whose serialize()/__toString() omits a constructor parameter (a filter, a locale, a user scope) will return another user's data. Write a test that asserts two queries differing only in that parameter produce different keys. Re-check this after any 3.0 upgrade — if the serialization path was touched, cached entries written by 2.x may now be unreachable or, worse, collide.
Request building is where bugs live, not the transport. Nine times out of ten when a query returns zero results, the params are wrong (unterminated quote in q, fq on a non-indexed field, wrong df/q.op, a date without Z). Log the fully-rendered request string and paste it into the Solr Admin UI's query console — the fastest debugging loop available. Don't log the response body at debug level in production; it's enormous.
Timeout layering. There are three: PHP max_execution_time, the HTTP client's timeout (Guzzle/curl inside whatever client the package wraps), and Solr's own timeAllowed param. Set all three, descending. A query that blows past the HTTP timeout leaves Solr still churning on a dead connection.
Extension points, ranked by durability:
SolrClientInterface — safest, survives everything (copy the 3.0 signatures exactly).Autowiring the handler. If queries silently do nothing or you get "no handler for query", the handler isn't tagged. Register it explicitly with the core package's handler tag (check src/Resources/config for the exact name) and validate with bin/console debug:container --tag=<handler_tag>. This is more likely to bite after a 3.0 install because autoconfiguration attributes/interfaces may have moved.
Use the Solr Admin UI as your oracle. Before you write a single query class, build the query you want in the admin UI, confirm the result, then transcribe. Doing it the other way round wastes afternoons on Solr syntax you'll never remember.
Pin the version and record why. "lmc/cqrs-solr": "3.0.*" — no caret, and no floating above 3.0, on an unmaintained package whose next release will not exist. Note explicitly whether you are on 2.x or 3.0, since the two are signature-incompatible: an upgrade becomes a code change, not a composer bump. Add a comment in composer.json or the README naming the fork/upgrade plan and the PHP 8.1 baseline, so the next developer doesn't inherit an accidental archaeology project.
How can I help you explore Laravel packages today?