trinity/grid
Trinity Grid is a Laravel package for building configurable data grids with a fluent PHP API, letting you define columns, rows, and layout while keeping rendering and data handling simple and consistent.
Before you install: understand what you're adopting. trinity/grid is a low-signal package (2 stars, no dependents). That doesn't make it bad — plenty of internal CMS components from modpreneur/trinity are solid — but it does mean you should read the source before you build a feature on it. Budget 20–30 minutes for a source walk-through; it will save you hours later.
Install and wire it up
composer require trinity/grid
Then look for a publishable config/service provider (typical Laravel layout):
php artisan vendor:publish --provider="Trinity\Grid\GridServiceProvider"
If no provider is auto-discovered, register it manually in bootstrap/providers.php (Laravel 11+) or config/app.php. If the package registers no provider at all, it may be a pure library — check composer.json extra.laravel.
Where to look first (in this order)
README.md — often thin for small packages; use it for the intended entry point only.composer.json — confirm required Laravel/PHP versions. If it targets Laravel 8/9 and you're on 11, expect friction.src/ — find the main class (likely Grid, GridBuilder, or similar) and read its constructor and public methods. This is your real API surface.config/ — column types, default page sizes, view paths.tests/ or examples/ — if absent, treat the package as undocumented and write your own smoke test.First use case
The realistic day-one scenario is an admin listing: a paginated, sortable, filterable table over an Eloquent model, rendered in a Blade view with row actions (edit/delete). Build that one screen end-to-end before touching anything else — it forces you through install, config, column definition, data source, and rendering, which is exactly where small packages break.
// Sketch — verify class/method names against src/
$grid = Grid::make(Order::class)
->columns([
'id',
'customer_name',
'total',
'status',
'created_at',
])
->paginate(25);
return view('admin.orders.index', compact('grid'));
Then render in Blade:
{!! $grid->render() !!}
If that works, you have a foundation. If it doesn't, you've spent an hour, not a sprint.
Treat the grid as a presentation layer, not a query layer. The healthiest pattern is: the controller owns the query (with eager loads, scopes, authorization), and the grid owns formatting and rendering. Resist the temptation to let the package build complex queries for you — small grid packages rarely handle joins, withCount, or soft-delete scoping well, and debugging generated SQL through a thin abstraction is painful.
public function index(Request $request)
{
$orders = Order::query()
->with('customer') // eager load anything a column touches
->when($request->status, fn ($q, $s) => $q->where('status', $s))
->latest();
return view('admin.orders.index', [
'grid' => Grid::fromQuery($orders)->columns([...]),
]);
}
Column definition pattern. Most Laravel grid packages follow a fluent column builder. Keep column config close to the grid definition, and extract custom renderers into small classes or Blade partials rather than inline closures repeated across screens:
->column('status', fn ($row) => view('admin.partials.status-badge', ['status' => $row->status]))
->column('total', fn ($row) => number_format($row->total, 2))
One grid per feature, not one god-grid. Create a dedicated class or method per listing (OrderGrid, UserGrid). This keeps column lists readable, makes changes reviewable in diffs, and isolates you from package API changes.
State handling. Sorting, filtering, and pagination usually round-trip through the query string. Standardize the parameter names early (e.g. ?sort=total&dir=desc&page=2&status=paid) and keep filters in the URL so views are shareable and back-button-safe. If the package doesn't read filters from the request itself, pass them in explicitly — don't rely on implicit request sniffing.
Row actions and bulk actions. Keep action URLs in named routes (route('admin.orders.edit', $order)) rather than hardcoded strings so you can move routes without touching grid definitions. Add authorization checks in the controller for every action, not in the grid — the grid should never be the security boundary.
Export / CSV. If export exists, verify it applies the same filters as the visible grid. The common bug is an export that ignores active filters and dumps the whole table. If export doesn't exist, write it in the controller off the same query builder — don't fork the package for this.
Composition with Blade. Wrap the grid in your layout partial so toolbar, filters, and pagination share styling. If the package's markup clashes with your CSS framework (Tailwind vs. Bootstrap), override views via vendor:publish (tag usually views) rather than fighting the package's classes inline.
Adoption risk is the biggest gotcha. Two stars and zero dependents means: no community answers when something breaks, no Stack Overflow coverage, and possibly no releases in months. Mitigations:
composer.json ("trinity/grid": "1.2.3") — not ^1.0.composer why trinity/grid to confirm nothing else pulls it implicitly.Version-compatibility failures surface at runtime, not install time. A package built for Laravel 8 can install cleanly on Laravel 11 and then blow up on a removed helper or a changed LengthAwarePaginator signature. Check composer.json require constraints against your app before wiring the UI, and run a single index action as your compatibility smoke test.
N+1 queries are the default failure mode. Any column that touches a relation ($order->customer->name) triggers a query per row. Audit every column against the query's with() list. Enable DB::listen or Laravel Debugbar and confirm the query count is roughly constant as page size grows from 10 to 100.
Debugging a grid that renders blank or empty:
dump($grid) before rendering — inspect columns, data source, and paginator.dd($grid->getData()) (or equivalent) — confirm rows exist before blaming the view.View [grid::...] not found error rather than failing silently.Config quirks. Small packages often hardcode: default page size, date format, empty-state text, and CSS framework. Publish the config and views on day one so you can change these without patching vendor/. Never edit files in vendor/ — the next composer update erases them.
Extension points to prefer over forking. In rough order of safety:
vendor:publish.render() or column resolution.vendor/ patch.Performance at scale. Sortable columns backed by unindexed DB columns will be slow past a few tens of thousands of rows. Add indexes for the columns you expose as sortable/filterable. If the grid does COUNT(*) pagination on a large table, that's often the slow part — consider a package-agnostic "simple" paginator or a cursor-based approach if counts lag.
Test the grid, not just the controller. A single feature test that hits the index route, asserts a 200, and asserts one known row appears is enough to catch breakage on dependency upgrades. Given the low activity level of this package, that test is your early-warning system.
How can I help you explore Laravel packages today?