power-components / turbine
Headless, UI-agnostic table engine for Laravel: describe the grid in PHP, get JSON for Inertia, Livewire, REST or plain AJAX.
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/pagination: ^12.0 || ^13.0
- illuminate/pipeline: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.29
- laravel/scout: ^11.3
- openspout/openspout: ^4.0 || ^5.0
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.0 || ^5.0
Suggests
- laravel/scout: Required to use Laravel Scout as a datasource.
- openspout/openspout: Required to export XLS and CSV (supports v4 and v5)
README
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.
Contents
- 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
Requirements & Install
- PHP 8.3+
- Laravel 12 / 13 (
illuminate/*components)
composer require power-components/turbine
How it fits together
?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.
Quickstart
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);
Builder reference
| 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. |
The Request Contract
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 |
The Response Envelope
->toArray() or ->toResponse() returns a structured JSON payload:
{
"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" } }
]
}
}
Actions & Rules
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.
Conditional Rules
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.
Filters
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}%"]));
Datasources
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']]));
Custom Datasources
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);
Exporting Data
Generate CSV or Excel (.xlsx) files directly from your grid context:
composer require openspout/openspout # Optional: required for XLSX exports
use 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);
State Persistence
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');
Reusing Components & Low-Level API
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();
Credits
Originally extracted from versions 6.x and 7.x of Livewire PowerGrid. Special thanks to all contributors!
License
MIT