We’ve all written that Controller method.
You know the one I'm talking about. It starts innocent enough:
return view('products.index', [
'products' => Product::paginate(10)
]);
But then the requirements start pouring in from the product manager:
- "Can we search by SKU?"
- "We need a filter for 'Out of Stock'."
- "Can we sort by Date Created?"
- "We need a bulk delete button."
- "Oh, and export to Excel, please."
Suddenly, your concise Controller method has exploded into 100 lines of if ($request->has(...)) statements, messy query builder logic, and a view file cluttered with spaghetti loops.
I decided to stop this madness. I wanted a solution that felt like magic. A solution that unified all these disjointed components into a single, fluent PHP object.
Meet TapTable.
Here is the story of the headache I endured building it, and why "gluing" components together is harder than it looks.
The Challenge: The "Glue" Problem
Building a Data Table library isn't just about rendering HTML <tr> tags. The real challenge is State Management and Component Unification.
When you separate "Columns", "Filters", and "Actions" into different classes or arrays, they stop talking to each other.
The Export button needs to know the current Search state (to export only filtered results).
The Bulk Action needs to know which Checkboxes are selected across pages.
The Sort Link needs to preserve the Filter parameters in the URL query string.
Trying to glue these pieces together manually usually results in a messy API. My goal for TapTable was to make the Developer Experience (DX) seamless.
The Architecture of Unification
To solve this, I had to treat TapTable not just as a View Renderer, but as a State Container.
Instead of passing variables to a View manually, I built a fluent wrapper that captures the entire lifecycle of the table.
1. The Syntax Goal
I wanted the code to read like English. No configuration arrays, no separate config files. Just fluent method chaining.
Here is what the final result looks like:
// The Goal: Clean, Unified, Expressive
return TapTable::make(Product::query())
->columns([
Column::make('name')->sortable()->searchable(),
Column::make('price')->formatMoney(),
Column::make('status')->badge(),
])
->filters([
SelectFilter::make('category_id', Category::all()),
DateFilter::make('created_at'),
])
->actions([
BulkAction::make('delete')->danger(),
Action::make('edit')->route('products.edit'),
])
->exportable()
->render();
2. Handling the "Under the Hood" Complexity
The hardest part was making the components intelligent.
For example, when you define Column::make('name')->searchable(), TapTable doesn't just render a search input. It acts as a middleware between the request and the response.
I had to build an internal Pipeline System:
- Hydrate: Read the Request (URL params).
- Apply Filters: Loop through defined filters and apply where clauses.
- Apply Search: Loop through "searchable" columns and apply orWhere logic dynamically.
- Apply Sort: Check for sort_by params.
- Execute: Run the query (or trigger the Export download if requested).
- Render: Pass the final collection to the View.
All of this happens inside the ->render() method. The developer never sees the messy logic.
The "Bulk Action" Nightmare
The tricky part of unification is interaction. Bulk Actions are the worst offenders.
In a standard setup, you usually have a
wrapping the table. But what if you have filters inside that form? What if the pagination links are outside?I solved this by ensuring TapTable acts as the single source of truth. When a user checks a box, it doesn't just toggle a DOM element; it updates the library's internal state.
When "Delete" is clicked, the library knows exactly which IDs to process, without the developer having to write a manual foreach loop in the controller.
Why "TapTable"?
Because I wanted something that felt "Tappable". Mobile-friendly, fast, and interactive.
Most data table libraries are heavy. They load jQuery, DataTables.js, and tons of CSS. TapTable is designed to be lightweight and PHP-native. It leverages the power of Laravel to do the heavy lifting on the server, sending only pure HTML/Alpine.js to the client.
Conclusion
Building TapTable taught me that the hardest part of software engineering isn't writing complex logic—it's hiding complex logic behind a simple interface.
Unifying Columns, Filters, Queries, and UI into one cohesive package was a struggle, but seeing a 100-line Controller refactored into 10 lines of TapTable code made it all worth it.
If you are tired of wrestling with manual pagination and filtering, maybe it's time to unify your stack.
I'm currently using this in production for my SaaS project, PagoraPOS, and it has saved me hundreds of hours.
Have you built your own abstraction for tables? Or do you stick to standard Blade views? Let me know in the comments!
Top comments (0)