atoolo/graphql-search-bundle
Atoolo GraphQL Search Bundle extends a Symfony GraphQL interface with powerful search capabilities, letting Atoolo-based applications query search results via GraphQL. Supports PHP 8.2–8.4 and is PHPStan level 9 compliant.
This bundle is not a standalone search engine — it is a GraphQL façade over the Atoolo search stack. Treat it as the last layer of a three-part dependency chain: atoolo/resource-bundle (resources, URL rewriting, images) → atoolo/search-bundle (index access, query building) → atoolo-graphql-search-bundle (schema types, resolvers, filters, facets). If your project does not already run the first two, integrating this bundle is a multi-week project, not a composer require.
Minimal install path:
composer require sitepark/atoolo-graphql-search-bundle
Then, in order:
config/bundles.php and confirm the underlying search/resource bundles are registered and configured first. This bundle assumes a populated search index and a working atoolo/search-bundle query pipeline; there is no fixture data or fallback backend.bin/console graphql:dump-schema schema.graphql
bin/console debug:container | grep -i atoolo
The dumped .graphql file is your actual reference for search query arguments, filter inputs, facet types, and teaser fragments. Keep it committed.
Where to look first, in priority order:
https://sitepark.github.io/atoolo-docs/develop/graphql/search/ — the documented query shapes.https://sitepark.github.io/atoolo-docs/develop/bundles/graphql-search/ — bundle wiring and configuration.tests/ directory — with a thin README, tests are the de facto specification for resolver behaviour, and they reveal edge cases the docs skip.CHANGELOG / release files — this project renames GraphQL fields in minor releases, so the changelog is required reading on every upgrade.First use case to implement: replace a hand-written search controller (form → repository → Twig) with a single GraphQL search query returning hits, total count, facets, and teaser fragments, then render the teasers in your template. That exercises the whole chain — filters, facets, teaser resolvers, image/URL resolution — in one query and surfaces configuration gaps immediately. Validate it in GraphiQL/Altair against a real index before touching frontend code.
One query per page, not per component. The natural temptation is to issue a separate query for the result list, the facet sidebar, and the "related content" rail. Issue one search query returning hits + facets + counts together; the backend query is built once and the facet computation is essentially free relative to a second round trip.
Request only the fields you render. Resolvers for images, teasers, kicker, dates, and copyright details do additional work per hit. A fragment that pulls a full teaser (image, symbolic image, features, link, accessibility label) for 25 hits is 25× that work. Define a slim list fragment and a rich detail fragment, and use the slim one in listing contexts.
Compose filters and facets from the schema, not from hand-built query strings. The bundle exposes discrete input types for ID filters, archive flags, template filters, and relative date ranges. Prefer these over injecting raw query syntax; they compose predictably and survive backend changes.
Relative date ranges need explicit boundary testing. The relative date range filter/facet supports from, to, and an optional base offset (added in 1.9). Offsets are evaluated server-side, so client timezone assumptions leak in. Write tests that hit the boundary (yesterday/today/tomorrow) with a fixed clock rather than trusting the default "now" behaviour.
Sorting has two layers. Standard sorts come from the schema; custom sorts are resolved through a SortCriteriaFactory, and spatial sort is its own path. Before adding a custom sort, check whether the factory already evaluates it — 1.9 added "evaluate custom sort", which means older sort definitions may now behave differently. Register custom sort criteria as services rather than patching the factory.
Use explain for relevance work only. The explain support added in 1.1 is genuinely useful for understanding why a document ranked where it did, but it inflates payloads and exposes scoring internals. Gate it behind an admin flag or a non-production environment.
More-like-this is the pattern for "related" rails. Rather than writing a custom resolver that re-queries by category, use the more-like-this query and pass the archive flag deliberately — archived content showing up in recommendations is the most common complaint with this feature.
Search context is the extension seam for URL generation. Fields that produce links depend on a rewriter context; the urlBasePath input and the context dispatcher (1.6/1.7) exist so the same query can resolve links correctly for different hostnames or path prefixes. When the same query returns different link bases on staging vs production, this is the mechanism — not a bug. Pass context explicitly rather than relying on ambient request state.
Server-side query execution (1.9) is the pattern for SSR, sitemaps, and cache warmup. If you are generating a page server-side or pre-warming a cache, execute the GraphQL query in-process instead of making an HTTP call to your own endpoint. This avoids the loopback request and the auth plumbing around it.
Frontend integration: for a JS frontend, persist the queries (hashed, allow-listed) rather than accepting arbitrary query documents. For a Twig/Symfony frontend, execute server-side and pass the result array to the template.
Testing strategy: unit-test custom resolvers, filters, and sort criteria with the search service mocked; rely on the separate atoolo-e2e-test repository pattern for integration-level assertions. Do not attempt to test GraphQL search behaviour against fixtures — the value is in the index, and a fixture harness will mislead you.
The README is not documentation. Ten lines plus two external links. Budget time to read the schema and the tests; do not assume a missing feature is absent because it is not mentioned in the README.
GraphQL field names are renamed in minor releases. The release history shows actions → teaserFeatures, ariaLabel → accessibilityLabel, symbolic image url moving under a content key, asset removed from MediaTeaser, staticImage added in 1.9. Your queries and frontend types are a contract with this schema. Commit the dumped schema and diff it in CI; a resolver rename will otherwise reach production as a null field rather than an error.
Partial failures return HTTP 200. A throwing resolver nulls its field and populates the errors array. Always inspect errors, not just the status code, and log it server-side — otherwise you ship pages with silently missing teasers.
Nullability is inconsistent by design in places. asset.variant was explicitly made nullable; several teaser fields are optional. Write fragments defensively rather than assuming a teaser has an image, kicker, or link.
Index freshness is not this bundle's job. Empty or stale results almost always trace to the indexing pipeline in atoolo/search-bundle, not to the GraphQL layer. Debug the pipeline first: check the resource was indexed, then check the query — in that order.
Filters fail closed and silently. A template filter, minHitCount, or archive flag that excludes everything returns zero hits with no error. When a query "returns nothing," strip filters one at a time before suspecting the resolver layer.
PHP 8.1 is no longer checked. The badge is explicit. Target 8.2+ to stay within tested territory; PHPStan level 9 means the maintainers care about types, but only on the versions CI covers.
Caching requires care with context-dependent fields. Results vary by language, archive flag, urlBasePath, and rewriter context. Key your cache on the full normalised query plus those context inputs, and invalidate on index updates. This is the easiest way to ship a cache-poisoning bug in a multi-site setup.
Disable introspection in production and apply query depth/complexity limits. A public search endpoint that accepts arbitrary nested teaser fragments is a cheap denial-of-service.
Adoption risk is real. Two stars, zero dependents, and a single-vendor maintainer mean you are an early adopter. Expect to read source to answer questions, plan for a local fork or patch path, and pin exact versions rather than floating to ^1.x if GraphQL field stability matters.
Extend by service, not by fork. Custom filters, sort criteria, resolvers, and type-map entries are registered as tagged services. Decorating an existing resolver service is far cheaper than vendoring the bundle — and this bundle's dependency on atoolo/search-bundle internals means a fork will rot quickly.
Debugging checklist when a field returns null: confirm the field exists in the dumped schema → check errors for a thrown exception → inspect the resolver service is registered (debug:container) → verify the underlying resource has that property populated in the index → check whether a rewriter context is required for that field. Following that order avoids hours lost to a missing service tag.
How can I help you explore Laravel packages today?