enhavo/search-bundle
A subtree split of the Enhavo search bundle, providing search functionality for the open source Enhavo CMS built on Symfony. It helps handle complex data structures with a clean, usable interface. See the main Enhavo repository for full documentation and details.
Reality check first. enhavo/search-bundle has no usable README of its own — the file you see is just the subtree-split banner. There is no API doc, no config reference, and essentially zero public adoption (2 stars, 0 dependents). Treat this as "read the source and the monorepo docs" territory rather than "follow the tutorial" territory. Budget an hour of orientation before you write code.
1. Confirm the package is actually intended for standalone use. It is a [READ-ONLY] subtree split of enhavo/enhavo. In practice most projects get it transitively through the Enhavo metapackage rather than requiring it directly. If you are building a plain Symfony app and only want search, weigh whether pulling a CMS-coupled search abstraction is worth it versus wiring your search engine directly.
2. Install and register.
composer require enhavo/search-bundle
// config/bundles.php
return [
// ...
Enhavo\Bundle\SearchBundle\EnhavoSearchBundle::class => ['all' => true],
];
3. Discover the configuration instead of guessing. This is the single most valuable first step given the missing docs:
php bin/console config:dump-reference enhavo_search
php bin/console debug:container --show-private | grep -i search
php bin/console debug:container --tag= kernel.event_subscriber | grep -i search
The Configuration.php in DependencyInjection/ is the ground truth for every config key. Read it before you read anything else — it is usually 50–150 lines and tells you what adapters, indexes, and clients are supported.
4. Read the tests. Subtree splits normally retain tests/. Those tests are effectively your documentation: they show how the index is populated, how queries are built, and how the engine is mocked. Start with whatever *Test.php files touch indexing or the search engine interface.
5. Check the monorepo docs. The banner's link to github.com/enhavo/enhavo is the real documentation home; look under docs/ for a search section. Also check which sibling bundles you likely need (an adapter bundle, a resource bundle for indexing entities, an Elasticsearch/OpenSearch client bundle if applicable).
First use case — index one entity and search it. Pick a single low-risk entity (a Page, an Article), get it into the index via whatever CLI command the bundle exposes (php bin/console list enhavo or list search), then run a query and render results. Do not attempt to index your whole model on day one; the index definition layer is where the undocumented sharp edges live.
Wrap the bundle behind your own service. Because the public API surface is not documented or versioned for external consumers, do not let controllers and templates call bundle classes directly. Introduce a thin App\Search\SearchGateway that depends on the bundle's engine/search interface and exposes search(string $term, array $options): ResultPage. When the bundle's API shifts (and on a 0.x/low-adoption package, it will), you change one file.
final class SearchGateway
{
public function __construct(
private object $engine, // bundle's engine interface; narrow it once you've read the source
) {}
public function search(string $term, int $page = 1): SearchResult
{
// translate your DTO <-> bundle query objects here
}
}
Define one index per resource, keyed by a stable identifier. The idiomatic Enhavo pattern is resource-oriented: each searchable model gets a document/index definition declaring its fields and how they are fetched. Keep the index name in config, not in code, so you can run blue/green reindexes.
Separate "indexing" from "searching" in your head and in your code.
postPersist/postUpdate/postRemove on Doctrine) or, more safely, an explicit reindex command. For anything you care about, prefer command-driven/batch reindexing plus a queued job on writes — synchronous indexing inside postPersist will happily make your save requests slow and your index inconsistent when a transaction rolls back.// Illustrative shape — verify method names against the bundle source
$query = $this->queryFactory->create($term);
$query->setLimit($limit)->setOffset(($page - 1) * $limit);
$result = $this->engine->search($query);
Drive the index from your deploy pipeline, not from a console session.
php bin/console <enhavo-search reindex command> --env=prod
Run it as a post-deploy step whenever mapping or field definitions change. Make the command idempotent and safe to re-run; that is the whole point of having it.
Integration with the front end. Keep the controller dumb: read q from the request, hand it to your gateway, paginate, and pass a results array to Twig. Validate and clamp page/limit server-side — search endpoints are trivially abusable for deep pagination.
Adapter swapping is your escape hatch. If the bundle does expose an engine interface with multiple implementations (a real search backend vs. a simpler/DB-backed fallback), code against the interface and select the implementation in config per environment. That lets tests run without a live search cluster and lets you swap backends if the bundle stops being maintained.
Testing. Stub your SearchGateway in functional tests; for integration tests, index a small fixture corpus in a setup step and assert on results. Avoid asserting on exact scores/ordering from a real backend — they drift between versions.
EnhavoSearchBundle will be closed. Submit changes to the main enhavo/enhavo monorepo instead, and expect the subtree to be regenerated. For local fixes, use a composer path repository or a fork.enhavo/* packages to compatible versions; mixing a v0.x search bundle with a v1.x resource bundle is a fast route to container compilation errors.config:dump-reference. If a key you expect is missing, check Configuration.php — the option may have moved or been renamed between releases.--no-events, imports) leaves the index behind. Schedule periodic reconciliation, or always reindex after fixture loads in CI.debug:container to confirm the engine service is wired and which class it is; (2) the bundle's Monolog channel or the app profiler to see the generated query; (3) hit the search backend directly with curl to separate "bundle built a bad query" from "backend has no data"; (4) inspect the raw index contents to confirm documents landed.DependencyInjection/. Registering your own tagged service is usually the supported way to add a field type or a new adapter — much safer than subclassing internals.How can I help you explore Laravel packages today?