ComfyCoders TranquilTools
Docs /Laravel Vue TableBuilder
Repository
v1

Table Classes

Table classes are the primary way to define a table. They extend AbstractTable and encapsulate the data source, authorization, and column/filter configuration in one place.

Creating a Table Class

php artisan make:table UsersTable

Or create manually in app/Tables/:

<?php

namespace App\Tables;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use TranquilTools\TableBuilder\AbstractTable;
use TranquilTools\TableBuilder\TableBuilder;

class UsersTable extends AbstractTable
{
    public function for(): Builder|array
    {
        return User::query();
    }

    public function authorize(Request $request): bool
    {
        return $request->user()->can('viewAny', User::class);
    }

    public function configure(TableBuilder $table): void
    {
        $table
            ->column('id', 'ID', canBeHidden: false)
            ->column('name', 'Name', sortable: true)
            ->column('email', 'Email', sortable: true)
            ->paginate(15);
    }
}

AbstractTable Methods

for()

Returns the data source. Can be an Eloquent query builder, the classname of the model, a model instance, a relation, a collection or an array.

Eloquent query builder:

public function for(): Builder
{
    return User::query()->with('company');
}

Model class name (returns all records, can be filtered/sorted/searched like a query builder):

public function for(): string
{
    return User::class;
}

A relation is unwrapped to its underlying query builder, so sorting, filtering and searching still work:

public function for(): HasMany
{
    return $this->user->posts();
}

A model instance is converted to a query builder scoped to that model:

public function for(): User
{
    return User::find($this->userId);
}

Plain arrays work too, though filtering and sorting happen in-memory:

public function for(): array
{
    return [
        ['id' => 1, 'name' => 'Alice'],
        ['id' => 2, 'name' => 'Bob'],
    ];
}

Even complex data sources can be supported by returning a collection or array:


    public function for(): Collection
    {
        $reactions = Reaction::query()
            ->where([
                // ...
            ])
            ->get()
            ->transform(function (Reaction $reaction) {
                // ...

                return $reaction;
            });

        $tickets = Ticket::query()
            ->where([
                // ...
            ])
            ->get();

        return $tickets
            ->merge($reactions)
            ->sortBy('created_at');
    }

When returning a query builder, the package wraps it in QueryBuilder which handles filtering, sorting, searching, and pagination automatically.

authorize(Request $request)

Return true to allow the action, false to deny it. Defaults to true.

The package routes for bulk actions, reordering and exports consult this method through authorizeBulkAction(), authorizeReorder() and authorizeExport(), which you can override one by one. Rendering a table never calls it. Denying answers 403. Guard the page itself with a middleware, policy or gate in the controller, and see Security for the recommended route setup.

public function authorize(Request $request): bool
{
    return $request->user()->isAdmin();
}

configure(TableBuilder $table)

Define columns, filters, search inputs, bulk actions, and other options. See the individual feature pages for full options.

build(...$arguments): TableBuilder

Static factory that instantiates the table, calls for() and configure(), runs beforeRender() and returns the ready-to-serialize TableBuilder. It does not call authorize().

Pass constructor arguments if your table class needs them:

class OrdersTable extends AbstractTable
{
    public function __construct(private readonly Customer $customer) {}

    public function for(): HasMany
    {
        return $this->customer->orders();
    }
}

// In controller:
'table' => OrdersTable::build($customer),

make(): TableBuilder

Same as build() but without calling beforeRender(). Useful when you need to inspect the builder before it executes the query.

performBulkAction(int $key, array $ids)

Override to handle bulk action execution. $key is the zero-based index of the action as registered in configure(). $ids is the array of selected row keys (or ['*'] for "all results"). The row key is the model's primary key unless the table sets rowKey(); see Bulk Actions.

public function performBulkAction(int $key, array $ids): void
{
    match ($key) {
        0 => User::whereKey($ids)->update(['active' => true]),
        1 => User::whereKey($ids)->delete(),
    };
}

Using in a Controller

use App\Tables\UsersTable;
use Inertia\Inertia;

class UserController extends Controller
{
    public function index()
    {
        return Inertia::render('Users/Index', [
            'table' => UsersTable::build(),
        ]);
    }
}

Without a Table Class

You can build a table inline using TableBuilder::for():

use TranquilTools\TableBuilder\TableBuilder;

public function index()
{
    $table = TableBuilder::for(User::query())
        ->column('id', 'ID')
        ->column('name', 'Name', sortable: true)
        ->paginate(15);

    return Inertia::render('Users/Index', [
        'table' => $table,
    ]);
}

Multiple Tables on One Page

Each table needs a unique name so its query parameters (page, perPage, search, …) don't clash with other tables on the same page.

When you use a table class, the name is derived automatically from the class name — no configuration needed. UsersTable becomes users, MostReportedErrorsTable becomes most_reported_errors (the Table suffix is dropped and the rest is snake-cased).

class UsersTable extends AbstractTable { /* ... */ }   // name: "users"
class RolesTable extends AbstractTable { /* ... */ }   // name: "roles"

return Inertia::render('Admin/Dashboard', [
    'users' => UsersTable::build(),
    'roles' => RolesTable::build(),
]);

Each table then prefixes its query params: users_sort, users_page, roles_sort, etc.

The resolved name is included in the serialized table, so the Vue component picks it up automatically — no name prop required:

<TableBuilder :table="users" />
<TableBuilder :table="roles" />

Spatie QueryBuilder tables: when for() returns a Spatie QueryBuilder (which reads its own global sort/filter parameters), name derivation is skipped and the table keeps the unprefixed parameters. These tables are meant to stand alone on a page; call ->name() explicitly if you really need to namespace one.

Overriding the name

Call ->name() in configure() (or on an inline TableBuilder) to override the derived name, e.g. when two tables share a class or when building tables inline without a class:

$usersTable = TableBuilder::for(User::query())
    ->name('users')
    ->column('name', 'Name')
    ->paginate(15);

When overriding an inline table, still pass a matching name prop in Vue so both sides agree:

<TableBuilder :table="users" name="users" />