paksuco/table
paksuco/table is a Laravel package for building configurable data tables in PHP. It helps define columns and rows for admin lists and grids, with straightforward Blade integration. Lightweight and easy to add to existing Laravel apps.
Read the source before you read anything else. There is no meaningful documentation beyond a one-line changelog and a feature blurb. Treat src/ as the spec. Budget the first hour of your spike for orientation, not integration:
composer.json for the illuminate/* and php constraints and diff them against your actual matrix before you run composer require. If you're on Laravel 10/11 or PHP 8.2+, assume you may need a fork; find that out in ten minutes rather than after wiring screens.main. For a 0.1.0 with an empty changelog, "paksuco/table": "0.1.0" (exact) and a committed composer.lock is the only sane posture. No ^, no dev-main.composer require paksuco/table:0.1.0
php artisan vendor:publish --provider="Paksuco\Table\TableServiceProvider"
Inspect config/ and resources/views/ in the package before publishing — the published views become yours to maintain forever, so publish only what you intend to override.First use case — pick a read-only index screen. Take one existing index view backed by an Eloquent paginator, ideally a model with 5–8 scalar columns and no complex relations. Do not start with your most complex CRUD screen, and do not start on a write path — this package is a presentation layer, and the spike is meant to measure friction on the cheapest possible surface. Success criterion: the screen renders identically (or better), sorting and filtering work, and the integration is confined to one or two files.
If the spike exceeds your timebox, that friction is the deliverable. Report it.
One table class per resource, in app/Tables/. Don't build column arrays inline in controllers. The package's stated value is "reusable table configurations," so lean into it:
// app/Tables/UserTable.php — wraps the vendor base class
final class UserTable extends \Paksuco\Table\Table
{
protected function columns(): array
{
return [
// exact key names: confirm against src/, the README does not specify
[ 'key' => 'id', 'label' => 'ID', 'sortable' => true ],
[ 'key' => 'name', 'label' => 'Name', 'sortable' => true, 'searchable' => true ],
[ 'key' => 'email', 'label' => 'Email', 'sortable' => true, 'searchable' => true ],
[ 'key' => 'created_at', 'label' => 'Created', 'sortable' => true, 'format' => fn ($row) => $row->created_at->diffForHumans() ],
];
}
}
Put a one-file adapter between you and the vendor. This is the single most important pattern given the package's maturity. Every controller and view talks to your abstract base class (App\Tables\BaseTable), and only that class extends the vendor. When the package stalls, forks, or gets swapped, the blast radius is one file. It also gives you a place to add project-wide behaviour — default per-page, consistent date formatting, an actions column — without editing published views.
Drive sorting and filtering from the request, but validate against an allowlist. The package will almost certainly take the sort column from the query string. Never let it touch the query builder unvalidated:
$allowedSorts = ['id', 'name', 'email', 'created_at'];
$sort = in_array($request->query('sort'), $allowedSorts, true)
? $request->query('sort')
: 'created_at';
$dir = $request->query('dir') === 'asc' ? 'asc' : 'desc';
$table = app(UserTable::class)->from(
User::query()->with('team') // eager-load anything a column touches
)->sort($sort, $dir)->search($request->query('q'));
If the package doesn't enforce an allowlist itself, enforce it in your adapter — a whitelist in BaseTable covers every screen at once.
Keep the query yours, the rendering theirs. The safest integration boundary is: you build the Builder (with scopes, eager loads, tenancy constraints, policy-derived filters), and hand it to the table purely for presentation. If the package starts composing queries you don't control, stop and reconsider — that coupling is expensive to unwind.
Eager-load for relation columns. A column that renders $row->team->name inside the Blade cell is a classic N+1. Because the cell rendering is hidden behind the package's view, the query count is invisible unless you look for it. Write one test that asserts DB::getQueryCount() (or use Model::preventLazyLoading() in local) on each table screen.
Pagination must preserve filters. Whatever the package hands to links(), ensure the paginator calls withQueryString() — otherwise page 2 silently drops the active search and sort. If the package's view doesn't do this, override that one partial rather than forking the whole view.
Override at the cell level, not the template level. The package advertises "extensible cells, actions, and formatting hooks." Prefer, in order: a format/render closure on the column definition → a named cell partial you publish → overriding the table view. Each step up that ladder costs you more on upgrade. Record every published/overridden view in a docs/vendor-overrides.md so upgrades are auditable.
Test the rendered output, not the internals. One feature test per table screen asserting the presence of expected rows, the correct order after ?sort=name&dir=asc, and filtered results after ?q=. That suite is your upgrade safety net — it will catch behavioural drift in a package that won't tell you about it in a changelog.
src/ and resources/views/ between tags. Read release diffs, not release notes.--ignore-platform-reqs.orderBy( and whereRaw( and read those two lines yourself — this is a five-minute security review you should not skip on unvetted code.LIKE %term% per column. Fine for small admin tables, quietly awful on large ones. Check whether it accepts a custom query callback; if not, add an explicit filter in your adapter rather than relying on the built-in search for anything performance-sensitive.toSql()/bindings before the render call. Ninety percent of failures are an empty column array, a renamed key that the view silently ignores, or a builder that was cloned and lost its constraints.config()->set() before the provider boots or a rebind. A classic hour-lost.Bottom line for a day-to-day developer: treat paksuco/table as a shortcut for commodity admin index screens, adopted behind a single adapter class, exact-pinned, with feature tests on rendered output. It can save you an afternoon of Blade and sort-link plumbing on a prototype; it cannot be handed a production critical path at 0.1.0 with a 2021 release date. Timebox the spike, measure the friction honestly, and re-evaluate at a later tag.
How can I help you explore Laravel packages today?