The framework-agnostic data engine behind the Turbine table component.
It runs search, filters, sort, pagination, and row transformations over Eloquent, Query Builder, Collections, or Scout, and returns a plain JSON envelope. There is no Blade and no JavaScript in the box — feed Inertia (React / Vue), Livewire, REST APIs, or plain AJAX with the exact same engine.
You describe the grid once in PHP. The engine does the rest. Your front-end just renders JSON.
- Requirements & Install
- How it fits together
- Quickstart
- The Request Contract
- The Response Envelope
- Actions & Rules
- Filters
- Datasources
- Exporting Data
- State Persistence
- Reusing Components & Low-Level API
- Credits
- License
- PHP 8.3+
- Laravel 12 / 13 (
illuminate/*components)
composer require power-components/turbine ?turbine[...] + ?page
Front-end ───────────────────────────────▶ Turbine (PHP definition)
(React / Vue / │
Livewire / AJAX) ▼
Turbine engine
search · filter · sort · paginate
│
JSON envelope { data, meta, columns, filters, actions }
Front-end ◀───────────────────────────────────────┘
renders table
The front-end owns rendering and interaction; the core owns data and rules. State travels in the request; results travel in the envelope.
Describe your grid in PHP using the Turbine builder:
use PowerComponents\Turbine\{Turbine, Column, Fields, Button};
use PowerComponents\Turbine\Components\Filters\FilterInputText;
class UserGridController
{
public function __invoke(Request $request)
{
return Turbine::make()
->datasource(fn () => User::query())
->fields(
(new Fields())
->add('id')
->add('name')
->add('email')
)
->columns([
Column::make('ID', 'id')->sortable(),
Column::make('Name', 'name')->searchable()->sortable(),
Column::make('Email', 'email')->searchable()->sortable(),
])
->filters([
new FilterInputText('name'),
])
->actions(fn (User $user) => [
Button::add('edit')->slot('Edit')->route('users.edit', ['user' => $user->id]),
])
->fromRequest($request)
->toArray(); // or ->toResponse() for JsonResponse
}
}// routes/web.php or routes/api.php
Route::get('/users/grid', UserGridController::class);| Method | Description |
|---|---|
datasource(Closure) |
Returns Eloquent Builder, Scout Builder, or Collection. Required. |
fields(Fields) |
Maps row shape and output keys. |
columns(array) |
List of Column instances for schema, sorting, and search. |
filters(array) |
List of Filter* components. |
actions(Closure) |
fn ($row) => Button[] — per-row actions. |
actionRules(Closure) |
fn ($row) => Rule[] — conditional rules per row/action/cell. |
relationSearch(array) |
Search across relations, e.g. ['category' => ['name']]. |
fromRequest(Request) |
Reads state from the turbine request parameter. |
state(array) |
Sets state from a raw array (Inertia JSON body, tests, etc.). |
toArray() / toResponse() |
Output as array or JsonResponse. |
Grid state is passed via the turbine query parameter and standard page:
| Parameter | Purpose | Example |
|---|---|---|
turbine[search] |
Global search term | turbine[search]=maria |
turbine[sortField] |
Column to sort by | turbine[sortField]=name |
turbine[sortDirection] |
Direction (asc / desc) |
turbine[sortDirection]=desc |
turbine[filters][<key>][<field>] |
Filter value by type and field | turbine[filters][input_text][name]=ana |
page |
Page number | page=2 |
->toArray() or ->toResponse() returns a structured JSON payload:
Actions use the Button DSL and resolve on the server:
Button::add('edit')->slot('Edit')->route('users.edit', ['user' => $user->id]);
Button::add('delete')->slot('Delete')->dispatch('deleteUser', ['id' => $user->id])->confirm('Are you sure?');Event types sent to client: link, dispatch, dispatchTo, dispatchSelf, modal, toggleDetail, call.
Apply server-side rules per row, button, or cell:
use PowerComponents\Turbine\Components\Rules\{RuleActions, RuleRows, RuleCheckbox, RuleToggleable};
Turbine::make()
->actionRules(fn (User $user) => [
// Hide delete action for admins
(new RuleActions('delete'))->when(fn ($u) => $u->is_admin)->hide(),
// Highlight admin rows
(new RuleRows())->when(fn ($u) => $u->is_admin)->setAttribute('class', 'bg-blue-50'),
// Disable checkboxes for system users
(new RuleCheckbox())->when(fn ($u) => $u->is_system)->disable(),
]);Supported rule target classes: RuleActions, RuleRows, RuleCheckbox, RuleRadio, RuleToggleable, RuleEditOnClick.
Turbine provides built-in filter components in PowerComponents\Turbine\Components\Filters\*:
| Filter Class | Request Key | Description |
|---|---|---|
FilterInputText |
input_text |
Text matching (contains, starts_with, exact, etc.) |
FilterSelect / FilterEnumSelect |
select |
Single choice from collection, array, or PHP Enum |
FilterMultiSelect / FilterMultiSelectAsync |
multi_select |
Multiple choices in-memory or from async endpoint |
FilterBoolean |
boolean |
True / false toggle |
FilterNumber |
number |
Numeric range filter |
FilterDatePicker / FilterDateTimePicker |
date / datetime |
Date and date-time range filters |
FilterDynamic |
custom |
Custom front-end component props |
// Standard filter usage
new FilterSelect('status')->dataSource(UserStatusEnum::cases());
// Custom query logic callback
new FilterInputText('title')->builder(fn ($query, $value) => $query->whereRaw('LOWER(title) LIKE ?', ["%{$value}%"]));Pass any supported data source to datasource():
// Eloquent Query Builder or Model
Turbine::make()->datasource(fn () => User::query());
// Laravel Scout
Turbine::make()->datasource(fn () => User::search($term));
// Array or Collection
Turbine::make()->datasource(fn () => collect([['id' => 1, 'name' => 'Alice']]));Implement DataSourceProcessor to handle custom APIs or repositories:
use PowerComponents\Turbine\Contracts\DataSourceProcessor;
use PowerComponents\Turbine\DataSource\Processors\DataSourceBase;
class CustomApiProcessor extends DataSourceBase implements DataSourceProcessor
{
public static function match(mixed $datasource): bool => $datasource instanceof MyCustomClient;
public function process(array $properties = [], mixed $datasource = null): array
{
// Fetch data, return length-aware paginator in results
return ['results' => $paginator, 'actionsByRow' => []];
}
}
// Register globally
Turbine::registerDataSource(CustomApiProcessor::class);Generate CSV or Excel (.xlsx) files directly from your grid context:
composer require openspout/openspout # Optional: required for XLSX exportsuse PowerComponents\Turbine\Export\ExportEngine;
$filePath = app(ExportEngine::class)->build(
context: $turbine->context(),
exportType: 'xlsx', // 'xlsx' or 'csv'
fileName: 'users_export'
);
return response()->download($filePath)->deleteFileAfterSend(true);Save grid state (filters, sorting, column visibility) across requests:
use PowerComponents\Turbine\Support\State\StatePersister;
$persister = new StatePersister();
// Save state to Cookie, Session, or Cache
$persister->serializeState(['columns', 'filters', 'sorting'], 'users', $stateArray);
// Restore saved state
$savedState = $persister->getPersistedState('users');Re-use an existing Turbine component as a data engine endpoint:
return (new UserTable())->toDataResponse($request);Or drop down to low-level context primitives:
use PowerComponents\Turbine\Response;
use PowerComponents\Turbine\Support\State\{ArrayGridContext, State};
$context = new ArrayGridContext(
state: State::fromRequest($request),
datasourceResolver: fn () => User::query(),
fields: $fields,
columns: $columns,
);
$envelope = Response::make($context)->toArray();Originally extracted from versions 6.x and 7.x of Livewire PowerGrid. Special thanks to all contributors!
MIT
{ "data": [ { "id": 1, "name": "Ana", "email": "ana@acme.test" } ], "meta": { "pagination": { "current_page": 1, "per_page": 15, "total": 84, "last_page": 6 }, "sort": { "field": "name", "direction": "desc" }, "search": "ana", "filters": { "input_text": { "name": "ana" } } }, "columns": [ { "field": "name", "title": "Name", "sortable": true, "searchable": true } ], "filters": [ { "key": "input_text", "field": "name", "column": "name" } ], "actions": { "1": [ { "id": "edit", "label": "Edit", "visible": true, "disabled": false, "event": { "type": "link", "href": "/users/1/edit" } } ] } }