staccato/listable
Staccato Listable is a Laravel package for adding list functionality to Eloquent models. Create named lists, attach any model via a polymorphic relationship, and manage list items with simple traits and helpers—useful for favorites, watchlists, collections, and more.
staccato/listable is an abstraction layer that lets you treat an Eloquent model, an array, a collection, or an arbitrary data source as a uniform "listable" repository with a shared list / paginate / filter / sort contract. With only 2 stars and zero dependents, treat the source and the README as the authoritative docs — there is no ecosystem of blog posts to fall back on.
Minimal steps:
composer require staccato/listable
README.md — the package's only real usage guide.src/ — identify the base class/interface (likely something like Listable, AbstractListable, or a ListableRepository contract) and the contract methods it declares.tests/ — the fastest way to see real, working calls without guessing method signatures.LengthAwarePaginator-compatible).Request-derived filter/sort array, and return the paginator. This proves the wiring end-to-end before you build custom filters.If the README is thin (common at this star count), a 10-minute tests/ read saves hours of trial-and-error with method names.
One listable class per resource. The abstraction pays off when you have parallel resources — UserListable, OrderListable, ProductListable — each extending the same base, so controller and API code becomes uniform. Avoid one giant listable that serves everything.
Filter and sort as first-class inputs. The package's raison d'être is turning ?filter[status]=active&sort=-created_at into a deterministic result. Pattern:
class OrderListable extends BaseListable
{
protected array $sortable = ['created_at', 'total', 'status'];
protected array $filterable = ['status', 'customer_id'];
}
Keep an explicit allow-list. Never pass the raw request array into the listable and hope it ignores unknown keys.
Driver-agnostic repositories. Because the contract is "listable source," you can swap the backing store without changing the controller:
EloquentOrderListable → query builder + paginator.ArrayOrderListable → for tests and fixtures.ApiOrderListable → for an external service, mapping to the same filter/sort semantics.Write controllers against the contract, bind the concrete driver in the container. This is the pattern the package exists for; if you only ever use one driver, plain Eloquent is simpler.
Compose filters, don't branch. If the package exposes a filter hook, implement small filter classes (e.g., StatusFilter, DateRangeFilter) registered per listable. This keeps conditional logic out of the listable and makes individual filters unit-testable against the array driver.
Wire to API Resources. Return the paginator and let JsonResource::collection() handle the shape. Don't let the listable own response formatting — that couples your transport to your data layer.
Testing pattern. Always test listables against the array/collection driver: fast, no database, and it forces you to keep the contract honest. Add one integration test per concrete Eloquent driver to catch query bugs.
Caching seam. If the package exposes a "resolve" step, wrap the source in a cache decorator that implements the same listable contract. Cross-driver consistency (same filter+sort+page ⇒ same shape) is what makes this safe.
Documentation is the biggest risk. With 2 stars and 0 dependents, expect the README to lag the code. When README and src/ disagree, src/ wins. Pin an exact version (composer require staccato/listable:^1.x with a composer.lock committed) — a maintainer patch could silently rename the base class.
Filter/sort injection is on you. The abstraction centralizes query input, which also centralizes the attack surface. If the package forwards filter keys to where() without validating against your $filterable allow-list, a client can filter on password or a relation column. Audit the filter application path in src/ before exposing it publicly.
Sorting has a hidden cost. A sort=-created_at on an unindexed column will full-scan once your table grows. Keep $sortable tight and aligned with indexes. Also confirm the package validates direction — a hand-rolled orderBy($request->sort) style implementation is a classic injection point.
Cross-driver behavioral drift. "Any listable source" sounds uniform, but pagination boundaries, null sort ordering, and case-insensitive filtering differ between SQL, arrays, and external APIs. Define expected semantics in tests per driver; don't assume the Eloquent behavior transfers.
Watch pagination contract mismatches. If your concrete driver returns a custom paginator shape and Laravel's JsonResource::collection() expects LengthAwarePaginator, you'll get subtly broken meta (total, links). Assert the shape in a test.
Extension points to look for in src/:
filter() / sort() hook to override per listable,paginate() you can override to change page size resolution,Debugging recipe. When results look wrong, instrument in this order: (1) dump the normalized filter/sort array your listable receives, (2) dump the query the driver builds, (3) run the same filter/sort against the array driver. If array output matches expectations and SQL doesn't, it's a driver bug; if array output is already wrong, it's filter parsing.
N+1 trap. Listables that paginate an Eloquent source and then render relationships will N+1 unless the package exposes eager-load hooks. If it doesn't, override the source query in your concrete driver to preload relations.
Low-adoption reality check. At 2 stars / 0 dependents, this is a "read the source, adopt the pattern" package as much as a dependency. Copy the contract into your app (App\Contracts\Listable) and implement it locally if you're uncomfortable depending on an unmaintained library — you get the same design benefit with none of the upgrade risk. If you do depend on it, wrap it behind your own interface so a fork or replacement is a one-file change.
How can I help you explore Laravel packages today?