Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Datatables Editor Laravel Package

yajra/laravel-datatables-editor

Laravel DataTables Editor plugin for handling DataTables Editor CRUD actions (create, edit, remove) through Laravel DataTables. Requires Laravel 12.x, DataTables 2.x, and a premium DataTables Editor license.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the gate before you write code. This package is only the server-side glue; the client half is DataTables Editor 2.x, which requires a paid license. If procurement is not done, your spike stops at the first new DataTable.Editor(...) call regardless of how clean the PHP side is.

  1. Check the version matrix against README compatibility table. Everything moves in lockstep:

    Laravel Package Also pinned
    5–10 1.x laravel-datatables 12.x, DataTables 2.x, Editor 2.x
    11 11.x same
    12 12.x same
    13 13.x same

    On Laravel 12/13: composer require yajra/laravel-datatables-editor:^13.

  2. Read these two docs first, in this order:

    • https://yajrabox.com/docs/laravel-datatables/editor-installation — the Laravel-side contract, the class shape, and the artisan scaffold.
    • https://editor.datatables.net/manual/index — the wire protocol (action, data, error, fieldErrors, id). You will debug against this, not against the PHP code.
  3. Scaffold one table. The artisan generator is the fastest way to learn the intended shape:

    php artisan datatables:editor UsersDataTable --model=User
    

    That produces a class with a model() method (the Eloquent target), a fields() method (the write surface), a rules() method (the validation surface), and lifecycle hook stubs (saving, saved, etc.) — hooks have been emitted by default since v11.0.0. Read the generated file top to bottom; it is the package's real documentation.

  4. Wire one route, serve both directions. Since v12.3.0 an Editor class is usable directly as a route action, which removes the boilerplate controller:

    Route::middleware(['auth', 'can:manage-users'])
        ->match(['get', 'post'], '/admin/users', UsersDataTable::class);
    
  5. First use case: inline edit + delete on a single column set. Fill in only id, name, email:

    public function fields(): array
    {
        return [
            Field::make('users.id', 'id')->render(false),
            Field::make('users.name', 'name')->rules('required'),
            Field::make('users.email', 'email')->rules('required|email'),
        ];
    }
    
    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'email'],
        ];
    }
    

    And in Blade, point Editor at the same URL:

    const editor = new DataTable.Editor({
        ajax: {
            url: '/admin/users',
            headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
        },
        table: '#users',
        fields: [
            { label: 'Name:',  name: 'name' },
            { label: 'Email:', name: 'email' },
        ],
    });
    

    If the row updates in place and a bad email comes back as a red field under the input, your setup is correct end to end. Everything after this is the same pattern repeated per table.

Implementation Patterns

One Editor subclass per table, and treat it as the reviewable seam. The strongest reason to adopt this package is that fields() + rules() + hooks put the entire write surface of a table in one file. Keep it that way: no CRUD logic in controllers, no $request->all() passthrough.

Scope the model, don't scope the request. There are no FormRequests and no automatic policy traversal here. Multi-tenancy and ownership belong in model():

public function model(): Model
{
    return User::query()->where('tenant_id', auth()->user()->tenant_id);
}

Every action — create, edit, remove, read — flows through that query. If a record is out of scope it simply is not found, which is the behaviour you want. Do the same for soft-deleted rows (withTrashed() only for admins) and for restore actions.

Use the lifecycle hooks for derived data, not for validation. creating/created, saving/saved, updating/updated, deleting/deleted map onto Eloquent's save cycle. Typical day-to-day uses:

public function saving(Model $model, array $data): void
{
    if (empty($data['password'])) {
        unset($model->password);      // don't blank it out on profile edit
    } else {
        $model->password = Hash::make($data['password']);
    }
    $model->updated_by = auth()->id();
}

Returning a falsy value from a pre-hook aborts the operation and surfaces to the client; post-hooks are for side effects (audit rows, cache invalidation, notifications). Throw an AuthorizationException inside a hook when an action needs a policy check the query scope cannot express.

Handle per-action rule differences by branching on the action. Editing usually needs unique:users,email,{currentId} while creating does not. Derive the current row id from the payload (for edit, the key of the data array is the row id) and use Rule::unique(...)->ignore($id). Keep rules() returning an array keyed by field name, because those keys are what populate fieldErrors in the response.

Bulk edit and bulk delete are the same endpoint, different payload shape. For a bulk action, data is keyed by each row id. Anything you write in hooks or customActions must iterate that payload rather than assume a single row. Note the historical pain here: multiple errors during a batch action were only displayed correctly from v11.0.1, and the 422 status code was fixed in v1.23.0 — pin a recent patch of your major.

Custom actions — the extension point you will actually reach for. The customActions property (v1.23.0, refined as "custom action class" in v12.1.0) lets you add non-CRUD operations surfaced from the Editor UI: "approve", "resend invite", "archive", "reset password". Pattern:

protected array $customActions = [
    ApproveUser::class,
    ResendInvite::class,
];

Each action class gets the selected ids and returns an Editor-style response. This keeps one-off button behaviour out of saving() conditionals — a saving() hook that switches on a hidden request flag is the anti-pattern to avoid here.

File uploads: two override levels. The default upload handling is fine for local avatars. Override the narrow methods rather than the whole pipeline (all three became overridable in v1.25.0):

protected function getDisk(): string            { return 's3'; }
protected function getUploadDirectory(): string { return 'uploads/users/'.auth()->id(); }

protected function storeUploadedFile($field, UploadedFile $file)
{
    // e.g. run through a resize pipeline before returning the stored path
}

Because storeUploadedFile() returns the value written to the model, this is also the right place to normalise paths, strip originals, or populate a separate *_path column.

Choose your read strategy deliberately. The classic split — a yajra/laravel-datatables DataTable class for GET (with its eager loading, filters, and column search) and an Editor class for POST (writes) — is still the right default. Since v12.2.0 the Editor endpoint can also serve the read action server-side, which is convenient for small tables where you want one URL and one class. Do not do that for large datasets; you lose the familiar filter/search ergonomics of the DataTable class.

Testing pattern. Editor endpoints are just POST routes; you can assert them like any JSON endpoint:

$this->actingAs($admin)
    ->postJson('/admin/users', [
        'action' => 'edit',
        'data'   => [$user->id => ['name' => 'New Name', 'email' => '[email protected]']],
    ])
    ->assertOk()
    ->assertJsonPath("data.{$user->id}.name", 'New Name');

Add the negative cases every time: an out-of-tenant id (expect the row to be "not found"), a validation failure (expect 422 with the exact field key), and an unauthenticated request. These three tests catch almost every real regression in this layer.

Call it from PHP when you need to. Since v12.1.0, process() takes an optional $request and toJson() is public. That lets you drive an Editor class from a console command, seed script, or nested admin flow without an HTTP round trip.

Gotchas and Tips

  • The license is the real dependency. composer require succeeds without it; nothing works in the browser without it. Budget line, procurement lead time, and a compliance artifact all at once. Confirm which tier covers your environments and named developers.
  • Four version axes must align — Laravel major, this package major, laravel-datatables 12.x, and DataTables/Editor 2.x on the front end. Nothing degrades gracefully on mismatch; you get vague 500s or an Editor "Unknown field" error. Pin all four in composer.json and in your JS asset pipeline.
  • The id field is load-bearing. Omit it, or leave it renderable, and edit/remove silently fail or update the wrong row. Standard: Field::make('users.id', 'id')->render(false) and add { label: 'ID:', name: 'id' } (or nothing) on the JS side.
  • Qualify column names whenever you join. Field::make('users.name', 'name') inside a model() that joins profiles avoids the ambiguous-id/ambiguous-name class of bugs that produce confusing update failures.
  • Validation errors must round-trip. The package returns 422 with fieldErrors (fixed in v1.23.0). Your JS must actually render them — if you swallow the error callback you will spend an afternoon wondering why "it saves but doesn't save". Ensure your Editor fields[].name values match the keys returned in rules() exactly.
  • No policies, no FormRequests, by default. Authorization is whatever your route middleware plus your model() scoping provide. Treat every new Editor class as a security review item: can user A reach user B's row by tampering with data? The query scope is your only automatic answer.
  • Mass assignment matters more here than usual. Writes go through the model, so a wide-open $guarded = [] turns your fields() list into the only thing standing between a client and arbitrary column writes. Keep $fillable tight and remember the package's unguard behaviour can mask this in tests.
  • CSRF is the #1 first-day failure. Editor POSTs go through the web middleware group; without the X-CSRF-TOKEN header you get a 419 and Editor shows a generic "A system error has occurred" toast with no detail. Always wire the token header.
  • Types are strings on the wire. Booleans arrive as 1/0 or "true"/"false", dates as whatever format your getFormatter produced. Normalise with Field::setFormatter() / getFormatter() and rely on Eloquent $casts rather than ad-hoc checks in hooks.
  • Soft deletes need a decision per table. remove maps to Eloquent delete; a restore concern was added in v11.0.2 for undeleting. Decide whether admins see trashed rows (scope withTrashed() in model()) and whether "delete" means soft or force — and document it in the class.
  • Unique rules will bite you on edit. unique:users,email rejects the row's own existing email. Pull the current id from the request payload and use Rule::unique(...)->ignore($id); this cannot be inferred for you.
  • Debug by logging the payload, not by reading the stack trace. Log action, the data keys, and the resulting model state. Most failures are shape mismatches (missing id, wrong nesting for bulk) rather than exceptions. Editor's own server-side debug flag gives you more than Laravel's handler will.
  • Watch the read side for N+1. If a relationship is displayed in the grid, eager-load it in the DataTable class; if you moved read onto the Editor class (v12.2.0), you own that optimisation there instead.
  • No built-in optimistic locking. Two admins editing the same row: last write wins. If that matters, add an updated_at comparison in saving() and abort on mismatch.
  • Extension points, ranked by how often you will use them: model() scoping → hooks (saving/saved) → rules() → customActions → storeUploadedFile()/getDisk()/getUploadDirectory() → process()/toJson() overrides. Stay in the top three for 90% of tables.
  • Strategic check before you build the fifth table. This is a deliberate fit for Blade + jQuery admin surfaces and a poor fit for Inertia/React/Vue or Livewire/Filament stacks. If an SPA or Livewire migration is on the 12–18 month roadmap, the license and the four-axis upgrade treadmill become technical debt on day one — and the payoff only scales once you have a dozen tables sharing the pattern.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core