fanforfun/select2-bundle
fanforfun/select2-bundle integrates the Select2 JavaScript library into PHP applications, giving forms searchable, AJAX-backed select boxes with tagging and multi-select support. A lightweight way to replace plain dropdowns with richer, user-friendly widgets.
First, calibrate expectations: fanforfun/select2-bundle is a Symfony2 bundle, not a Laravel package. There is no service provider, no facade, no config/select2.php. Installing it via Composer into a Laravel app will do nothing useful — Symfony bundles are only auto-registered by Symfony's kernel. Check the repo before investing time:
composer.json — look at the symfony/symfony constraint. If it pins ~2.x, the bundle predates Symfony 3 and is effectively abandoned (0 stars, 0 dependents confirms it).Resources/ — confirms it is an asset/form-helper bundle, not a data-layer library.DependencyInjection/ — the only real logic lives here, usually just registering a Twig extension or Assetic asset path.Where to look first as a Laravel developer: don't. Treat this package as a reference implementation for how to wrap Select2 in a server-rendered framework, then reimplement the ~40 lines of glue natively in Laravel.
First use case to port: an "entity picker" — a <select> on an edit form that shows a label, stores an ID, and swaps to an AJAX-backed search once the option count exceeds a few hundred rows. That's 90% of what Select2 is used for in any Symfony or Laravel app.
Minimal Laravel equivalent:
npm install select2 jquery
// resources/js/app.js
import $ from 'jquery';
import 'select2';
import 'select2/dist/css/select2.css';
$('.select2').select2({ theme: 'default' });
<select name="customer_id" class="select2" data-url="{{ route('customers.search') }}">
@if($customer) <option value="{{ $customer->id }}" selected>{{ $customer->name }}</option> @endif
</select>
1. AJAX data source (the pattern the bundle was built for). Select2 expects a specific JSON shape — { results: [{ id, text }], pagination: { more: boolean } }. Implement it as an invokable controller, not a closure:
// app/Http/Controllers/CustomerSearchController.php
public function __invoke(Request $request)
{
$term = $request->string('q');
$customers = Customer::query()
->when($term->isNotEmpty(), fn ($q) => $q->where('name', 'like', "%{$term}%"))
->orderBy('name')
->paginate(20, ['id', 'name']);
return response()->json([
'results' => $customers->getCollection()
->map(fn ($c) => ['id' => $c->id, 'text' => $c->name]),
'pagination' => ['more' => $customers->hasMorePages()],
]);
}
2. Init helper as a Blade component. Instead of copying $('.select2').select2() into every view, register a component that carries the defaults:
{{-- resources/views/components/select2.blade.php --}}
@props(['name', 'url' => null, 'multiple' => false, 'selected' => null])
<select
name="{{ $name }}{{ $multiple ? '[]' : '' }}"
{{ $attributes->merge(['class' => 'select2']) }}
@if($multiple) multiple @endif
@if($url) data-url="{{ $url }}" @endif
>
{{ $slot }}
</select>
@push('scripts')
<script>
$('[name="{{ $name }}{{ $multiple ? '[]' : '' }}"]').select2({
width: '100%',
ajax: @json($url ? ['url' => $url, 'dataType' => 'json', 'delay' => 250,
'data' => "params => ({ q: params.term, page: params.page || 1 })",
'processResults' => "data => data"] : null),
});
</script>
@endpush
3. Repopulation after validation failure. This is where naive Select2 integrations break. old('customer_id') returns an ID with no label, so the select renders blank. Two fixes:
withInput path and pass its label to the view.templateSelection + a data-selected attribute containing pre-rendered <option> markup.return back()->withInput()->with('selectedCustomer', Customer::find(old('customer_id')));
4. Multiple select + pivot sync. Name the field roles[] (Laravel's ->name attribute must end in []), validate with 'roles' => ['array'], 'roles.*' => ['exists:roles,id'], and sync(). Select2 will silently send a comma-joined string if you forget the brackets.
5. Tagging / create-on-the-fly. tags: true sends free text alongside IDs. Guard it with a custom validation rule that either resolves the string to an existing record or creates one inside a transaction — do not firstOrCreate in a loop inside the controller's happy path.
6. Livewire / Inertia. The bundle's Twig-helper approach has no analog. In Livewire use wire:ignore on the select and re-init select2 in a hook('select2:select') that calls $wire.set(...); in Inertia, mount on the form component's onMounted and destroy on unmount.
composer require fanforfun/select2-bundle will pull in a bundle whose service provider is not discoverable by Laravel. You'll get a silent no-op plus a symfony/symfony dependency conflict that can wreck your lockfile. Vendor it as a reading reference only.id/text are hard-coded keys. Returning {value, label} results in a blank dropdown with no error. Map explicitly in the controller.pagination.more: true but never pass page back in data, infinite scroll loops on page 1 forever. Conversely, omitting pagination entirely makes Select2 think there's exactly one page.type: 'POST' for long queries, send X-CSRF-TOKEN from meta[name="csrf-token"]; a 419 surfaces as an empty result list, not a visible error.vite.config.js and import Select2 CSS explicitly — it is not auto-injected.width: '100%' almost always needed inside Bootstrap/Tailwind forms, otherwise the widget collapses to the width of the longest option.innerHTML replacement destroys Select2's generated markup while leaving the <select> intact. Destroy with $(el).select2('destroy') before re-init, or wrap in wire:ignore.$(el).data('select2') exposes the instance; network tab should show q and page params; if the AJAX call fires but nothing renders, log the raw response — a 302 to login (expired session) is the usual culprit.ajax.url, and a data-* attribute convention. In Laravel, the cleanest equivalent is a Blade component + a config key select2.defaults published via php artisan vendor:publish.How can I help you explore Laravel packages today?