machour/laravel-data-table 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

machour/laravel-data-table

Composer 安装命令:

composer require machour/laravel-data-table

包简介

A reusable, server-side DataTable system for Laravel + Inertia.js + React (TanStack Table). Define your table in a single PHP class — get sorting, filtering, pagination, exports, quick views, and a full-featured React UI out of the box.

README 文档

README

A reusable, server-side DataTable system for Laravel + Inertia.js + React (TanStack Table v8). Define your table in a single PHP class — get sorting, filtering, pagination, exports, quick views, and a full-featured React UI out of the box.

Features

  • Single-file backend — One PHP class per model acts as both DTO and table configuration
  • Server-side everything — Sorting, filtering, pagination handled by Spatie QueryBuilder
  • Operator-based filters — URL format filter[price]=gte:1000 with 14 operators (eq, neq, gt, gte, lt, lte, between, in, not_in, contains, before, after, null, not_null)
  • Quick Views — Server-defined filter presets + user-saved custom views (localStorage)
  • Column ordering — Drag-to-reorder via GripVertical handles, persisted to localStorage
  • Column pinning — Automatic sticky columns for checkbox (_select) and actions (_actions)
  • Column groups — Group columns under shared headers with custom background colors
  • Column visibility — Toggle columns on/off, persisted to localStorage
  • Footer aggregations — Per-page computed values (sum, avg, etc.) with custom rendering
  • XLSX/CSV export — Via Maatwebsite Excel (optional peer dependency)
  • Bulk actions — Checkbox selection with configurable action buttons
  • Expandable detail panels — Eager or lazy server payloads rendered beneath each row
  • Row actions — Per-row dropdown menu with visibility and variant support
  • Responsive — Mobile popover for toolbar, horizontal scroll for wide tables
  • Feature flags — Disable any feature via frontend options prop

Requirements

PHP

Package Version
PHP ^8.2
Laravel ^11.0 | ^12.0
spatie/laravel-data ^4.0
spatie/laravel-query-builder ^6.0

Optional:

  • maatwebsite/excel ^3.1 — for XLSX/CSV export
  • spatie/laravel-typescript-transformer ^2.5 — for TypeScript type generation from DTOs

JavaScript

Your project must be set up with shadcn/ui (React + Tailwind CSS).

The shadcn add command will automatically install all required shadcn components (button, table, checkbox, etc.) and npm dependencies (@tanstack/react-table, @inertiajs/react, date-fns, lucide-react).

Installation

1. Install the Composer package

composer require machour/laravel-data-table

2. Install the React components via shadcn

npx shadcn@latest add ./vendor/machour/laravel-data-table/react/public/r/data-table.json

This copies all DataTable components into your project (you own the code!) and installs the required shadcn UI dependencies automatically.

3. (Optional) Install Maatwebsite Excel for export support

composer require maatwebsite/excel

Quick Start

1. Scaffold with the Artisan command

The fastest way to get started:

php artisan make:data-table Product

This generates:

  • app/DataTables/ProductDataTable.php — your DataTable class
  • resources/js/pages/product-table.tsx — a React page stub

Available options:

# Include export support (HasExport trait)
php artisan make:data-table Product --export

# Also append a route to routes/web.php
php artisan make:data-table Product --route

# Custom route file
php artisan make:data-table Product --route --route-file=routes/admin.php

# Custom page output path
php artisan make:data-table Product --page-path=resources/js/pages/admin

2. Or create your DataTable class manually

<?php

namespace App\DataTables;

use Machour\DataTable\AbstractDataTable;
use Machour\DataTable\Columns\Column;
use App\Models\Product;
use Illuminate\Database\Eloquent\Builder;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
class ProductDataTable extends AbstractDataTable
{
    public function __construct(
        public int $id,
        public string $name,
        public float $price,
        public ?string $created_at,
    ) {}

    public static function fromModel(Product $model): self
    {
        return new self(
            id: $model->id,
            name: $model->name,
            price: $model->price,
            created_at: $model->created_at?->format('Y-m-d H:i'),
        );
    }

    public static function tableColumns(): array
    {
        return [
            new Column(id: 'id', label: 'ID', type: 'number', sortable: true),
            new Column(id: 'name', label: 'Nom', type: 'text', sortable: true, filterable: true),
            new Column(id: 'price', label: 'Prix', type: 'number', sortable: true, filterable: true),
            new Column(id: 'created_at', label: 'Créé le', type: 'date', sortable: true, filterable: true),
        ];
    }

    public static function tableBaseQuery(): Builder
    {
        return Product::query();
    }

    public static function tableDefaultSort(): string
    {
        return '-created_at';
    }
}

2. Add a route

use App\DataTables\ProductDataTable;
use Inertia\Inertia;

Route::get('/products', function () {
    return Inertia::render('products', [
        'tableData' => ProductDataTable::makeTable(),
    ]);
});

3. Create your React page

import { DataTable } from "laravel-data-table";
import type { DataTableResponse } from "laravel-data-table";
import { Head } from "@inertiajs/react";

type Row = App.DataTables.ProductDataTable;

interface Props {
    tableData: DataTableResponse<Row>;
}

export default function ProductsPage({ tableData }: Props) {
    return (
        <>
            <Head title="Products" />
      <DataTable<Row> tableData={tableData} tableName="products" />
        </>
    );
}

That's it! You get sorting, filtering, pagination, column visibility, and column ordering out of the box.

Backend API

AbstractDataTable

Extend this class for each model. It extends Spatie\LaravelData\Data, so it's both a DTO and table configuration.

Method Required Description
tableColumns() Yes Returns Column[] defining the table structure
tableBaseQuery() Yes Returns the base Eloquent Builder
tableDefaultSort() No Default sort column (prefix with - for desc). Default: '-id'
tableQuickViews() No Returns QuickView[] for filter presets
tableAllowedFilters() No Auto-derived from filterable: true columns. Override for OperatorFilter or custom filters
tableAllowedSorts() No Auto-derived from sortable: true columns. Override for relation sorts
tableFooter(Collection) No Compute per-page footer aggregations
filterParamName() No URL query parameter name for filters. Default: 'filter'. Override to avoid collisions with multiple tables on one page
makeTable(?Request) Inherited Builds the DataTableResponse — call this in your route

Column

new Column(
    id: 'price',             // Must match DTO property name
    label: 'Prix',           // Display label
    type: 'number',          // text | number | date | option | multiOption | boolean
    sortable: true,          // Allow sorting
    filterable: true,        // Show in filter bar
    visible: true,           // Default visibility (user can toggle)
    options: [...],          // For type=option: [['label' => 'X', 'value' => 'x'], ...]
    min: 0,                  // For number range (optional)
    max: 100000,             // For number range (optional)
    icon: 'check',           // Lucide icon name (optional)
    searchThreshold: 5,      // Show search input in option filter if >= N options
    group: 'Details',        // Group columns under a header (optional)
);

QuickView

new QuickView(
    id: 'recent',
    label: 'Recent',
    params: [
        'filter[created_at]' => 'after:' . now()->subDays(7)->toDateString(),
        'sort' => '-created_at',
    ],
    icon: 'calendar',
    columns: ['id', 'name', 'created_at'],  // Optional: visible columns in display order
);
  • Empty params: [] matches when no filter and no sort in the URL
  • columns defines both visibility AND display order when the view is active

OperatorFilter

Custom Spatie QueryBuilder filter supporting operator:value URL format:

use Machour\DataTable\Filters\OperatorFilter;
use Spatie\QueryBuilder\AllowedFilter;

public static function tableAllowedFilters(): array
{
    return [
        AllowedFilter::custom('price', new OperatorFilter('number')),
        AllowedFilter::custom('name', new OperatorFilter('text')),
        AllowedFilter::custom('status', new OperatorFilter('option')),
        AllowedFilter::custom('created_at', new OperatorFilter('date')),
        AllowedFilter::custom('enabled', new OperatorFilter('boolean')),
        // Remap filter name to a different DB column:
        AllowedFilter::custom('display_name', new OperatorFilter('text', 'real_column')),
    ];
}
Type Default Operator Available Operators
text contains contains, eq
number eq eq, neq, gt, gte, lt, lte, between
date eq eq, before, after, between
option in in, not_in
boolean eq eq

All types also support null and not_null.

Expandable row detail panels

Expansion is opt-in. Add HasExpansion to a table, then return JSON-serializable detail data from tableExpandedData():

use Illuminate\Database\Eloquent\Model;
use Machour\DataTable\Concerns\HasExpansion;

class ProductDataTable extends AbstractDataTable
{
    use HasExpansion;

    public static function tableExpansionName(): string { return 'products'; }
    public static function tableExpansionMode(): string { return 'lazy'; } // lazy | eager
    public static function tableExpansionKey(): string { return 'id'; }
    public static function tableExpansionCache(): bool { return true; }

    public static function tableExpandedData(Model $model): mixed
    {
        return [
            'description' => $model->description,
            'stock_movements' => $model->stockMovements()->latest()->limit(10)->get(),
        ];
    }
}

Lazy expansion uses a consumer-owned route, so authentication and authorization middleware remain under application control:

use Machour\DataTable\Http\Controllers\DataTableExpansionController;

DataTableExpansionController::register('products', ProductDataTable::class);

Route::get('/data-table/{table}/expansion/{row}', DataTableExpansionController::class)
    ->middleware('auth')
    ->name('data-table.expansion');

The controller resolves rows through tableBaseQuery(). Apply tenant and authorization scopes there when they must also protect expansion requests.

Render the payload in React:

<DataTable<Product>
  tableData={tableData}
  tableName="products"
  renderExpandedRow={({ row, data }) => {
    const details = data as { description: string };
    return <ProductDetails product={row} details={details} />;
  }}
/>

eager mode computes expansion data for the current page during makeTable(). lazy mode fetches it on first expansion. With tableExpansionCache() enabled, loaded data is reused after collapse; disabled caching refetches after every collapse. Open rows remain expanded across Inertia refreshes in the current browser tab and their lazy payload is refreshed when table data changes. The whole row toggles expansion unless the click targets an interactive control. Multiple rows may be expanded simultaneously. Tables without HasExpansion, and components without renderExpandedRow, behave exactly as before.

Use getRowProps when rows need drag-and-drop or other native row handlers:

<DataTable<Product>
  tableData={tableData}
  tableName="products"
  renderExpandedRow={({ data }) => <ProductDetails details={data} />}
  getRowProps={(row) => ({
    draggable: true,
    onDragStart: (event) =>
      event.dataTransfer.setData("text/plain", String(row.id)),
  })}
/>

Export (HasExport trait)

use Machour\DataTable\Concerns\HasExport;

class ProductDataTable extends AbstractDataTable
{
    use HasExport;

    public static function tableExportEnabled(): bool { return true; }
    public static function tableExportName(): string { return 'products'; }
    public static function tableExportFilename(): string|\Closure { return 'products-export'; }
}

Register the export route:

use Machour\DataTable\Http\Controllers\DataTableExportController;

// Register table → class mapping
DataTableExportController::register('products', ProductDataTable::class);

// Add the export route
Route::get('/data-table/export/{table}', DataTableExportController::class)->name('data-table.export');

Requires maatwebsite/excel to be installed.

Frontend API

<DataTable> Props

interface DataTableProps<TData extends object> {
    tableData: DataTableResponse<TData>;  // Server response from makeTable()
    tableName: string;                     // Unique name for localStorage keys
    filterParam?: string;                  // URL param name for filters (default: from server or 'filter')
    actions?: DataTableAction<TData>[];    // Row actions dropdown
    bulkActions?: DataTableBulkAction<TData>[]; // Bulk actions with checkbox selection
  renderCell?: (
    columnId: string,
    value: unknown,
    row: TData,
  ) => ReactNode | undefined;
    renderHeader?: Record<string, ReactNode>;
  renderFooterCell?: (
    columnId: string,
    value: unknown,
  ) => ReactNode | undefined;
    rowClassName?: (row: TData) => string;
    groupClassName?: Record<string, string>;
    options?: Partial<DataTableOptions>;   // Feature flags (all default to true)
}

Options (Feature Flags)

All default to true. Pass options prop to disable:

<DataTable
    tableData={tableData}
    tableName="products"
    options={{
        quickViews: false,
        customQuickViews: false,
        exports: false,
        filters: false,
        columnVisibility: false,
        columnOrdering: false,
    }}
/>

Custom Cell Rendering

<DataTable<Row>
    tableData={tableData}
    tableName="products"
    renderCell={(columnId, value, row) => {
    if (columnId === "price")
      return <span className="font-bold">{value} DT</span>;
        return undefined; // Fall back to default
    }}
/>

Row Actions

const actions: DataTableAction<Row>[] = [
    {
        label: "Edit",
        onClick: (row) => router.visit(`/products/${row.id}/edit`),
    },
    {
        label: "Delete",
        variant: "destructive",
        onClick: (row) => router.delete(`/products/${row.id}`),
        visible: (row) => row.canDelete,
    },
];

Bulk Actions

import type { DataTableBulkAction } from "laravel-data-table";
import { Trash2 } from "lucide-react";

const bulkActions: DataTableBulkAction<Row>[] = [
    {
        id: "delete",
        label: "Delete",
        icon: Trash2,
        variant: "destructive",
        disabled: (rows) => rows.length === 0,
    onClick: (rows) =>
      router.post("/products/bulk-delete", { ids: rows.map((r) => r.id) }),
    },
];

Footer Aggregations

Backend:

public static function tableFooter(\Illuminate\Support\Collection $items): array
{
    return [
        'price' => $items->sum('price'),
    ];
}

Frontend (custom rendering):

<DataTable<Row>
    tableData={tableData}
    tableName="products"
    renderFooterCell={(columnId, value) => {
    if (columnId === "price")
      return <span className="text-emerald-600">{value} DT</span>;
        return undefined;
    }}
/>

Column Group Styling

<DataTable<Row>
    tableData={tableData}
    tableName="products"
    groupClassName={{
        Details: "bg-emerald-50/60 dark:bg-emerald-950/20",
        Specs: "bg-violet-50/60 dark:bg-violet-950/5",
    }}
/>

Multiple Tables on One Page

Override filterParamName() on each DataTable class so their URL parameters don't collide:

class InvoiceDataTable extends AbstractDataTable
{
    public static function filterParamName(): string
    {
        return 'invoice_filter';
    }
}

The frontend picks up the param name automatically from the server response. You can also set it explicitly via the filterParam prop:

<DataTable tableData={productsData} tableName="products" />
<DataTable tableData={invoicesData} tableName="invoices" filterParam="invoice_filter" />

URL Format

All state is URL-driven and bookmarkable:

/products?filter[price]=gte:1000&filter[name]=contains:widget&sort=-price,name&page=2&per_page=50
  • Filters: filter[column]=operator:value1,value2
  • Sort: sort=column (asc) or sort=-column (desc), comma-separated for multi-sort
  • Pagination: page=N&per_page=N

localStorage Keys

The component persists user preferences under these keys:

Key Purpose
dt-columns-{tableName} Column visibility state
dt-column-order-{tableName} Column display order
dt-quickviews-{tableName} Custom saved quick views

Testing

cd vendor/machour/laravel-data-table
composer install
./vendor/bin/pest

License

MIT

machour/laravel-data-table 适用场景与选型建议

machour/laravel-data-table 是一款 基于 TypeScript 开发的 Composer 扩展包,目前已累计 1.26k 次下载、GitHub Stars 达 2, 最近一次更新时间为 2026 年 02 月 16 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「react」 「datatable」 「spatie」 「inertia」 「tanstack-table」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 machour/laravel-data-table 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 machour/laravel-data-table 我们能提供哪些服务?
定制开发 / 二次开发

基于 machour/laravel-data-table 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.26k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 29
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 2
  • Watchers: 0
  • Forks: 1
  • 开发语言: TypeScript

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-16