ringlesoft/laravel-selectable
Laravel Selectable generates HTML select options directly from Laravel collections using a fluent, chainable API. Customize labels and values with strings or closures, set selected/disabled options, add data attributes, grouping, and export arrays for AJAX or SPAs.
Architecture fit. This is a presentation-layer convenience package, not infrastructure. It registers collection macros (toSelectable(), toSelectOptions(), toSelectItems()) that turn an Eloquent/Support collection into an HTML <option> string or an array of selectable items. It fits a Blade-first, server-rendered Laravel application where forms are authored in .blade.php files and options are derived from models or config collections. It fits poorly with an API-first / Inertia / Vue or React frontend where markup should be produced client-side — there the only relevant surface is toSelectItems(), which is a thin mapping we could write in ~30 lines. It also overlaps directly with Laravel's native @selected() / @disabled() Blade directives and with a form-component library (e.g. Blade UI kit), so adopting it means standardizing on one idiom across the codebase or tolerating two.
The design is idiomatic (fluent builder, closure-or-string field selection, Collection pass-through including groupBy()), and the toSelectItems() escape hatch is the strongest architectural decision because it decouples the package from markup for SPA/AJAX consumers.
Integration feasibility. Very high. Single composer require, no config, no publishable assets, no migrations, MIT license, additive-only — it cannot break existing rendering because nothing calls it until we do. Cost of a spike is measured in hours, not days. Auto-discovery via a service provider is implied but not documented; that is a five-minute verification.
Technical risk.
{!! $users->toSelectOptions() !!}. If labels derive from user-controlled columns (names, titles, org names), the package must escape internally. The README never states that it does. This is a must-verify before any use on non-admin surfaces; if it does not escape, we either wrap output ourselves or restrict usage to trusted label sources.selected. Default value is id (int), while docs also demonstrate uuid (string) and int arrays. Loose vs. strict comparison in selection matching is a classic source of "the wrong role is preselected" bugs, which are silent and high-consequence in permission/payment forms.$item->status = 'inactive' (assignment) rather than a comparison, and one snippet has a stray brace (!!}}). Sloppy docs on the primary feature page suggest thin review of test coverage around disabled/selected semantics.Model::all() inside Blade. A note discourages it, but the examples teach it. Left unchecked, this becomes N+1 queries and full-table loads in views.Key questions.
toSelectOptions() escape labels/attribute values, or emit raw model data into HTML?php and illuminate/support version constraints, and is Laravel 11/12 covered by the CI matrix?withSelected() — strict or loose, and how are int/string IDs reconciled?groupBy() nesting?selected when the collection is re-keyed by groupBy()?Stack fit. Best fit: Laravel 10–12, PHP 8.2+, Blade-rendered admin and internal tooling forms — settings pages, filters, assignment dropdowns, exports. Acceptable: Livewire/Alpine screens consuming toSelectItems() as JSON. Poor fit: public-facing forms with user-supplied labels (until escaping is confirmed), and any Inertia/SPA frontend, where the package reduces to a mapper we should own internally.
Migration path. Purely additive; no data or schema migration. Adopt in a branch, pin an exact version in composer.json, and immediately place an internal seam — a small App\Support\SelectOptions wrapper or a Blade component — so every call site goes through our own API. That makes the exit (drop the dependency, or replace with a @foreach) a single-file change, and lets us add escaping centrally if the package does not.
Compatibility. Verify against our actual version matrix before rollout: PHP constraint, illuminate/support constraint, and one smoke test per supported Laravel major. Check for existing collection macros that could collide. Run Larastan with the shipped IDE helper and confirm no new baseline errors. Confirm auto-discovery registers the provider under our optimized/config:cache deployment path — packages that rely on runtime macro registration occasionally behave differently in cached production boots, which must be caught in staging, not production.
Sequencing.
<script>/"), run the type-mismatch probe (string vs. int selected), confirm Laravel version support.::all()-in-view pattern.toSelectItems() payload, never queried inline in Blade; add a code-review checklist item.Maintenance. Low absolute maintenance cost: small package, low churn, MIT fork rights. The real overhead is governance — pinning, a per-Laravel-upgrade smoke test, and a standing owner for the wrapper. Budget roughly a day per year plus a small share of each framework upgrade. Because the surface is a handful of classes, an abandoned upstream is recoverable in a day via fork or in-house reimplementation; the credible worst case is two days of engineering, not a stalled roadmap.
Support. No vendor SLA; best-effort community support from a single maintainer. Internal documentation must carry the weight: which methods we sanction, the escaping rule, the "prepare data outside the view" rule, and the pinned version. Bug reports upstream are unlikely to be prioritized given the adoption level, so assume we fix our own issues.
Scaling. Option rendering happens in the request cycle and builds a full HTML string in memory. That is fine to a few hundred options; beyond roughly 500–1,000 rows per select it becomes measurable in response size, memory, and browser render cost. Large catalogs should be served via toSelectItems() + AJAX/typeahead, or cached option arrays (config/reference data is the ideal case: render once, cache the fragment). Watch for closure-based labels touching lazy-loaded relations — that turns one select into N+1 queries, invisible in a small dataset and painful in production.
Failure modes. Ranked by blast radius:
| Failure | Impact | Detection |
|---|---|---|
| Unescaped label/attribute output | Stored XSS on admin or public forms | Probe in spike; CSP and code review as backstops |
| Selected/disabled mismatch from type coercion | Wrong record preselected on edit forms; data written to the wrong entity | Regression test on int vs. string keys; QA on every edit form |
Closure bug in withDisabled/withSelected returning truthy always |
Every option disabled — form unusable | Unit test the actual predicates |
| Query-in-Blade pattern copied from docs | Latency, memory growth as tables grow | Query-count assertions in CI; review checklist |
| Macro collision or rename on a version bump | Runtime exception rendering a page | Pinned version + smoke tests per Laravel major |
Grouped output mis-nesting with groupBy() |
Malformed <select>; selections lost on submit |
Visual + submit round-trip test |
None of these are systemic; all are contained by the wrapper and a handful of tests, which is precisely why the wrapper is non-negotiable in the integration plan.
Ramp-up. A mid-level Laravel developer is productive in under an hour — the fluent API reads well and the README's examples are directly copyable. That same copyability is the main onboarding hazard: new developers will query models inside Blade and emit raw output because that is what the documentation shows. The onboarding artifact that matters is therefore not a tutorial but a short convention doc plus the wrapper's enforced signature, so correct usage is the path of least resistance.
How can I help you explore Laravel packages today?