Headless, UI-agnostic table engine for Laravel: describe the grid in PHP, get JSON for Inertia, Livewire, REST or plain AJAX.

Maintainers

Package info

github.com/Power-Components/turbine

pkg:composer/power-components/turbine

Transparency log

Fund package maintenance!

luanfreitasdev

Statistics

Installs: 293

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-08-17 23:48 UTC

This package is auto-updated.

Last update: 2026-08-17 23:59:44 UTC


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

  • 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