samson/autocomplete-bundle
Symfony bundle that adds autocomplete form fields backed by Doctrine entities, providing a bundled AJAX search endpoint and configurable matching so users pick records from a live-filtered dropdown instead of long, unwieldy select lists.
This is a Symfony bundle, not a Laravel package — despite being evaluated in a Laravel context. It wires Doctrine ORM entities/query builders to Select2-style AJAX autocomplete endpoints. If you're on Laravel, you're in the wrong ecosystem; on Symfony it's a natural fit.
Minimal steps:
composer require samson/autocomplete-bundle. Register the bundle in config/bundles.php (or app/AppKernel.php on older Symfony).src/ and look for:
AutocompleteController with an action taking a query parameter).Resources/config/routing.xml|yaml).samson_autocomplete config block mapping an entity to a repository method or DQL string.config/routes.yaml (or routes/samson_autocomplete.yaml on Symfony 5+).ajax.url at the bundle's route with ?query={term}, and confirm you get <option>-shaped JSON back. Test with curl before touching the UI.Start with a single simple entity (e.g., User selecting by name), verify the round trip end-to-end, then expand.
Provider registration (most likely shape). Autocomplete bundles of this vintage typically expect you to declare which entity to search and which fields:
# config/packages/samson_autocomplete.yaml
samson_autocomplete:
entities:
user:
class: App\Entity\User
search_fields: [fullName, email]
label_field: fullName
Verify against source — the exact keys matter more than the pattern.
Controller action pattern. Wrap the bundle's route in your own thin controller if the bundle's built-in one is too rigid. You can reuse its search service directly:
#[Route('/autocomplete/users', name: 'autocomplete_users')]
public function users(Request $request, UserSearchProvider $provider): JsonResponse
{
return $this->json($provider->search($request->query->get('query', '')));
}
Select2 integration. In your template, initialise Select2 with a processResults mapper if the bundle returns raw arrays rather than {id, text} objects:
$('.js-user-select').select2({
ajax: {
url: '/autocomplete/users',
dataType: 'json',
delay: 250,
data: params => ({ query: params.term }),
processResults: data => ({ results: data.map(u => ({ id: u.id, text: u.fullName })) })
},
minimumInputLength: 2
});
Query builder customisation. Most implementations of this pattern let you supply a closure or service that returns a QueryBuilder. Use it to:
->andWhere('u.tenant = :t')).->andWhere('u.enabled = true')).->setMaxResults(10) — do this at the query layer, not the DTO layer, for performance.Reusable JS component. Wrap the Select2 init in a small Stimulus controller or a shared JS function. You'll reuse it across forms; avoid copy-pasting the AJAX config.
Form type integration. If the bundle ships a AutocompleteType or EntityAutocompleteType, use it in your FormType so the Select2 assets and data-* attributes are generated consistently. Otherwise, a plain EntityType + manual JS is the fallback.
Caching the option list. For rarely-changing, small datasets (countries, categories), skip the AJAX round trip and pre-render options; reserve the bundle for genuinely large or dynamic sets.
Symfony-only. Do not attempt to force this into Laravel. The Doctrine coupling and bundle registration assume Symfony's kernel and container. On Laravel you'd be better off with a simple Route::get('/autocomplete', ...) + ->pluck('name', 'id').
Low maintenance signal. 9 stars, 0 dependents means: expect stale code (likely targets Symfony 3/4-era APIs), no security advisories, and no community answers when it breaks. Read every line of the controller before trusting it in production — check for missing result limits, missing input escaping, and whether it enforces a max query length.
Unbounded queries. Autocomplete endpoints are easy to hammer. Confirm the bundle applies setMaxResults and consider adding:
#[RateLimiter] or a kernel listener).minimumInputLength on the client (Select2) — but never rely on it alone; validate server-side.Doctrine hydration. If the bundle hydrates full entities you'll pay for relation loading. Prefer repositories that return scalar arrays (getArrayResult() / select() with partials) — autocomplete only needs id and a label. Watch for N+1s if label_field is a relation's __toString.
LIKE vs. full-text. Typical implementations use LIKE %term%, which is unindexed and slow on large tables. For >100k rows switch to a DB full-text index, or pg_trgm/MySQL MATCH ... AGAINST, or Elasticsearch/Meilisearch behind the same endpoint.
CSRF and methods. Autocomplete is a GET endpoint; make sure any bundle route doesn't accidentally require a CSRF token or POST, which will break Select2's default GET. Check methods: in the routing config.
JSON shape mismatch. Select2 needs {results: [{id, text}]} (Select2 4.x) — older versions expect [{id, text}]. If the bundle targets an older Select2, you'll get a silently empty dropdown. Inspect the raw response in DevTools Network tab first; that's the #1 debugging step.
Service container overrides. If the search logic is a service, override it via services.yaml with the same class ID + a higher priority compiler pass rather than forking the bundle. Safer for upgrades.
Entity mapping assumptions. Bundles like this often assume getId() and a __toString() exist. If your entity uses UUIDs or a non-id primary key, test early — the bundle may hardcode the id property.
Debugging checklist when the dropdown is empty:
curl 'http://localhost/autocomplete?query=foo' — is there any JSON?bin/console debug:router | grep -i autocomplete.bin/console doctrine:query:dql.processResults mapping correctly (check Console → Network → Response headers for a Content-Type: application/json)?Extension point. If there's no clean hook for custom logic, keep your own controller and call the bundle's search service. That decouples your app from the bundle's routing/controller so you can rip the bundle out later without touching templates.
Plan an exit. Given the maintenance profile, treat the bundle as a thin convenience layer, not a foundation. Isolate its usage behind one service or controller so a future swap (FOSElastica, a custom endpoint, or Laravel Scout) is a one-file change.
How can I help you explore Laravel packages today?