liip/search-bundle
LiipSearchBundle integrates Zend Lucene search into Symfony applications, offering services to build indexes, run queries, and manage results. It provides a simple, configurable way to add full-text search to your project.
Reality check first: this bundle is archived, its last release was July 2016, and the Google XML/AJAX Search API it wraps was discontinued years ago. There is no "day-to-day" usage to adopt — the first step is deciding whether you're reading it as a reference implementation for building a replacement, or whether you mistakenly found it while searching Packagist. If it's the latter, stop and pick a live provider (Google Custom Search JSON API, Bing Web Search, Algolia, Meilisearch, or Elasticsearch).
If you still want to read it as a reference, the minimal path is:
composer require liip/search-bundle — expect this to fail on any modern Symfony; the constraints target Symfony 2.x and PHP 5.x. If you just want to inspect it, clone the repo instead.app/AppKernel.php (new Liip\SearchBundle\LiipSearchBundle()).liip_search in config.yml — typically an API key and any default query parameters.Where to look first, in order:
README.md — short, but it's the only usage documentation that exists.Resources/config/services.xml (or .yml) — this is the real API surface. The service ID and the class it points to tell you more than the README does.DependencyInjection/Configuration.php — the canonical list of config keys, their defaults, and whether they're required.First use case (historically): a search box on a Symfony site that submits a query, calls the Google-backed service, and iterates the results into a list of title/link/snippet entries. That's the entire scope of the bundle. It is not a site-search/indexing tool, not a crawler, and not a faceted search engine — it's a remote API client with Symfony wiring around it.
Service injection, not static calls. The bundle's value is that it registers a service you can inject. The pattern to copy:
class SearchController extends Controller
{
public function searchAction(Request $request)
{
$query = $request->query->get('q', '');
$results = $this->get('liip_search.google')->search($query);
return $this->render('search/results.html.twig', ['results' => $results]);
}
}
Prefer constructor injection and a type-hinted interface over $this->get() — the bundle era encouraged service-locator style, but if you're using it as a template for new code, don't inherit that.
Configuration as a first-class concern. Everything provider-specific (API key, endpoint host, result count, language, safe-search) belongs in the config tree, not in the service. The Configuration.php + Extension.php pair is the cleanest part of this bundle and is worth mimicking verbatim when you write your replacement: define the tree, mark required nodes, validate early so misconfiguration fails at container compile time rather than at the first search request.
Wrap results in a value object. The bundle maps the raw provider response into a collection the template can iterate without knowing the wire format. Keep that separation. When you swap providers, only the mapper changes; the controller and Twig stay identical. Define a SearchResult with getTitle(), getUrl(), getSnippet() and a SearchResultCollection implementing IteratorAggregate and Countable.
Defensive parsing. Provider responses are external input. Treat missing fields, empty result sets, and unexpected root elements as normal cases, not exceptions. A search that returns zero results should render an empty state, not a 500.
Cache aggressively. Search APIs are quota-metered and slow. Wrap the service in a cache decorator keyed on md5($query . serialize($options)) with a short TTL (5–15 minutes for web search). This is the single highest-leverage pattern in the whole bundle and it's usually what people forget when they reimplement it.
Decorator over inheritance for swapping providers. If you're porting this bundle's role into a live app, define SearchInterface { public function search(string $query, array $options = []): SearchResultCollection; }, implement GoogleCustomSearch, AlgoliaSearch, etc., and alias one of them to the interface in services.yaml. The controller never changes again.
Integration tip — handle the failure path explicitly. Network timeouts, quota-exceeded responses, and malformed payloads should degrade to an empty result set plus a logged warning, not propagate. Wire in a PSR-3 logger and log the raw response body at debug level when parsing fails; that's the difference between a five-minute fix and an afternoon.
The vendor API is gone — this is not a configuration problem. If searches return nothing, it isn't your API key. Google's AJAX/XML Search APIs were deprecated and shut down; the successor (Custom Search JSON API) requires both an API key and a Programmable Search Engine ID (cx), has a free tier of ~100 queries/day, and returns a completely different JSON schema. No amount of bundle configuration bridges that gap.
Abandoned means no security patches. The package has zero dependents and no maintenance. Even if you could install it, you'd be pulling in an unmaintained HTTP client and potentially old Guzzle/Symfony components with known CVEs. Pin and audit, or don't use it.
Composer will fight you. Symfony 2.x bundle conventions (AppKernel, services.xml referencing symfony/symfony) don't resolve against Symfony 5/6/7. Expect constraint conflicts before you ever reach runtime. If you need to run the code, use a throwaway PHP 5.6/7.0 container.
API keys in config.yml get committed. The bundle's default pattern puts the key in version-controlled config. Use %env(GOOGLE_API_KEY)% or parameters.yml-style overrides; never commit the literal key.
Quota exhaustion looks like a bug. If you do wire it to a live provider, the free tier runs out fast during development — and providers often return HTTP 200 with an error body rather than a 429. Check the response payload, not just the status code, and surface quota errors distinctly from "no results found."
Result parsing is the fragile seam. The bundle's mapping layer assumes a specific XML shape. Any provider change breaks it silently — you get empty fields rather than an exception. When porting, write a fixture-based unit test against a captured real response so schema drift shows up as a red test.
Extension points are thin but present. Because the search backend is a normal service, you can override it with a compiler pass ($container->setAlias('liip_search.google', MySearch::class)) or a bundle inheritance trick. That's the intended escape hatch — but at that point you've replaced the only thing the bundle does, so just drop the dependency.
Debugging checklist for the archived version: confirm the service is actually registered (debug:container liip_search), confirm the config node is populated (debug:config liip_search), then log the constructed request URL — nine times out of ten the API key is empty or the endpoint constant points at a host that no longer resolves.
Bottom line: treat this repository as a small, readable case study in how to structure a Symfony service around an external HTTP API — config tree, DI wiring, result value objects, thin controller. Do not treat it as a dependency. The replacement is a 100-line service plus an interface and a cache decorator, and you'll own it.
How can I help you explore Laravel packages today?