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 Admin Laravel Package

dcat-plus/laravel-admin

A Laravel admin panel package for quickly building backends: CRUD scaffolding, form and table builders, role-based access control, and a customizable UI. Built on the Dcat Admin ecosystem with extra components for rapid admin development.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and bootstrap

composer require dcat-plus/laravel-admin
php artisan admin:publish        # config + assets + migrations
php artisan admin:install        # run migrations, seed admin user
php artisan admin:import <your-seed-or-module>   # optional, per Dcat Admin conventions

After install you get:

  • config/admin.php — the single source of truth. Note route.namespace (default App\Admin\Controllers); everything else (Actions, Renderable, Forms namespaces) is derived from it.
  • app/Admin/ — controllers, routes, bootstrap, and the folders you will actually live in: Actions/, Renderable/, Forms/.
  • public/vendor/dcat-admin/ — published assets. Re-publish after every upgrade or the UI silently breaks. 1.3.0 ships new frontend behaviour for tree and import — this step is mandatory, not optional.

Where to look first

  1. skills/references/action-system.md in the repo — still the most complete document in the package. Read sections 2 (type table) and 5 (hooks) before writing anything.
  2. Whatever reference file covers tree and grid import — 1.3.0 (extended tree capabilities, Model-based grid data import) is the first release where those two subsystems are first-class, so their docs are the freshest part of the bundle. Read them before you subclass anything, because 1.3.0 may have changed the contracts you would otherwise copy from an older tutorial.
  3. app/Admin/routes.php — confirms the URL prefix and the /dcat-api/action endpoints the frontend posts to.
  4. config/admin.php → route.namespace — memorize how the generator derives namespaces from it, because that is the #1 source of "class not found".

First use case

Build one grid screen with a row-level action button, end to end. Do not start with forms, trees, or import — a grid-row action exercises the whole loop (generator → class → AJAX → handle() → Response) in about twenty lines:

php artisan admin:action grid-row PublishPost
// app/Admin/Actions/Grid/PublishPost.php
public $title = 'Publish';

public function handle(Request $request): Response
{
    Post::findOrFail($this->getKey())->update(['status' => 'published']);

    return $this->response()->success('Published')->refresh();
}
// in your Grid
$grid->actions(function (Grid\Displayers\Actions $actions) {
    $actions->append(new \App\Admin\Actions\Grid\PublishPost());
});

Once that button round-trips, every other action type is a variation on a theme — including the built-in import flow, which is a toolbar action plus a modal under the hood.

Implementation Patterns

Always generate, never hand-write the skeleton. The generator resolves base class, namespace, and the required hooks for you; typos in the parents are the most common self-inflicted wound.

php artisan admin:action                        # interactive, 4 questions
php artisan admin:action grid-batch ApproveAll  # non-interactive: good for CI / AI / scripts
php artisan admin:action lazy-form LoginUserForm --force

Pick the type by container, not by vibe.

Need Type Parent
Button inside a page/widget default Actions\Action
Per-row button in a grid grid-row Grid\RowAction
Multi-select operation grid-batch Grid\BatchAction
Toolbar button (grid/form/show/tree) grid-tool / form-tool / show-tool / tree-tool AbstractTool
Tree node button tree-row Tree\RowAction
Modal body that loads a grid lazy-table Grid\LazyRenderable
Modal body that loads a form lazy-form Widgets\Form + LazyWidget

Compose behaviour through hooks instead of overriding rendering. The four you will use daily:

public function confirm()          { return ['Publish?', 'This becomes visible immediately.']; }
protected function parameters()   { return ['scene' => 'publish', 'flag' => $this->row('status')]; }
protected function authorize($u)  { return $u->isAdministrator(); }
protected function actionScript() { return 'function (data, target, action) { /* return false to abort */ }'; }

parameters() is the clean way to pass server-rendered state into the request — better than re-querying in handle(), and it keeps the payload inspectable in DevTools.

Choose the return shape deliberately. Response is a fluent builder and every terminal call maps to a frontend behaviour:

return $this->response()->success('Done')->refresh();                  // stay in place
return $this->response()->success('Done')->redirect(admin_url('posts')); // navigate away
return $this->response()->success('Copied')->script("Dcat.info('{$this->getKey()}')"); // server → client value
return $this->response()->error('Forbidden');                          // abort
return $this->response()->alert(true)->success('Done');                // Swal instead of Toastr

Batch actions: getKey() is an array. Treat it as a collection and chunk it — the frontend will happily send 500 IDs.

public function handle(Request $request): Response
{
    Post::whereIn('id', (array) $this->getKey())->update(['status' => 'approved']);
    return $this->response()->success('Approved')->refresh();
}

Model-based grid import (new in 1.3.0). The split is deliberate: enable the entry point on the grid, declare the contract on the model. Keep the mapping and validation there — not in the controller — so the same import works from any grid that renders that model.

// app/Admin/Controllers/PostController.php
$grid->import();          // adds the toolbar/upload entry; confirm exact method name in skills/references

// app/Models/Post.php — the import contract
// column label → validation/transformation, e.g. title, slug, status, published_at

Design rules that survive contact with real spreadsheets:

  • Validate per row, then import in chunks inside a transaction. Fail the whole file on a structural error (missing header, wrong column count); report per-row on data errors. Never let half a file land.
  • Normalise before you cast. Excel sends dates as serial numbers, codes as floats (leading zeros lost), and CSV frequently arrives with a BOM. Coerce in the model's import hook, not in handle().
  • Make it idempotent. Decide up front whether a re-upload inserts, upserts on a unique key, or skips. Whoever runs it twice will be you.
  • Stamp the actor. created_by / updated_by from admin_user() is the only audit trail you will have when a bad file lands.
  • Big files belong in a queue. Anything past a few thousand rows should dispatch a job and report through a notification, not hold the request open behind set_time_limit.

Lazy rendering is a different subsystem — do not mix it with Actions. lazy-table / lazy-form post to /dcat-api/render and /dcat-api/form, not /dcat-api/action. They are mounted through Modal / Card / LazyTable:

// app/Admin/Renderable/UserPostsTable.php  (grid-row action opens this)
return Modal::make()
    ->lg()
    ->title('Posts')
    ->body(UserPostsTable::make(['user_id' => $this->getKey()]))
    ->success($this->response());

Tree screens got more capable in 1.3.0. The same action model applies — tree-row for node buttons, tree-tool for the toolbar — but you can now lean on the tree layer for more of the interaction instead of hand-rolling it:

// node-level actions, unchanged shape
$tree->actions(function (Tree\Actions $actions) {
    $actions->append(new EditNode());
    $actions->append(new DeleteNode());
});

Practical guidance: keep the tree model's parent_id, ordering, and depth rules in one place (the model or a base Tree class), because 1.3.0's expanded tree surface means more code paths — drag/sort, node actions, lazy children — can each mutate the same structure. Guard against cycles and cap depth before the UI lets a user create a 40-level tree.

Prefer href() over handle() for anything that is really a link. Defining href() flips allowHandler off, so there is no useless AJAX round-trip:

public function href(): string { return admin_url('posts/'.$this->getKey().'/preview'); }

Group crowded row menus. Three or more actions per row should collapse into a dropdown:

$actions->group(function ($g) {
    $g->append(new Approve());
    $g->append(new Reject());
}, 'feather icon-more-vertical', 'More');

Gate visibility server-side. $grid->disableDeleteButton() / $actions->disableDelete() removes the button; authorize() still protects the endpoint. Do both — the first is UX, the second is security. This applies double to import: the upload entry is a mutating endpoint, so gate it with authorize() (or an explicit permission check) and not just a hidden button.

Gotchas and Tips

  • Upgrade order matters for 1.3.0. composer update → php artisan admin:publish → hard refresh → then touch tree/import code. Skipping the publish step leaves you debugging a UI that is older than the PHP classes calling it.
  • $title and handle() are mandatory for every type except href()-style tools. Forgetting $title yields a nameless blank button with no error.
  • Namespace derivation is implicit. The generator takes config('admin.route.namespace'), strips the trailing Controllers, and appends Actions / Renderable / Forms. Change that config — or pass --namespace= — or the classes land somewhere the container will never find them.
  • Interactive vs. parameter mode is decided by the first positional argument. Passing an invalid type key errors and lists the valid values; passing nothing drops you into a four-question prompt that will hang a non-TTY process. In scripts and CI, always pass the type explicitly.
  • --force is destructive and silent. It will clobber a hand-edited generated class without prompting.
  • Response terminal calls overwrite, they do not queue. redirect()->refresh() refreshes; only the last then-family call wins. Similarly, error() short-circuits the whole chain — set your success message after the business logic succeeds, not before.
  • JS hooks are string-eval'd. actionScript() / resolverScript() / rejectScript() must return a JS function expression as a heredoc string. Returning false inside actionScript aborts the request; returning false inside resolverScript suppresses the default success handling — useful when you want the modal to stay open.
  • authorize() defaults to true. A generated action is publicly triggerable by anyone who can guess _action + _key. Override it on anything mutating.
  • Debugging a "button does nothing": open DevTools → Network → the POST to /dcat-api/action. The payload contains _action (snake_cased class name) and _key. A 403 means authorization, a 500 with a serialization error means the class is not autoloadable (namespace mismatch, stale composer dump-autoload).
  • Debugging an import that "does nothing" or silently imports zero rows: watch the Network tab for the upload POST and the follow-up render/action call. The usual causes are a header row that does not match the model's declared columns (off-by-one space, translated label vs. field name), a validation rule that rejects every row and is reported only in the toast, or a BOM making the first header unrecognisable. Log the first rejected row with its line number — without that, users will report "the file is broken" and you will have nothing.
  • Import is a mutating endpoint wearing a friendly UI. It bypasses the grid's form rules, so any invariant enforced only in a Form must be re-asserted in the model's import path — otherwise import becomes the hole through which bad data enters.
  • Silent UI breakage after upgrade = unpublished assets. Re-run the publish command and hard-refresh; the package has moved the JS before, and 1.3.0 changes tree rendering.
  • alert(true) vs. Toastr is a per-action decision, not a global one. Use Swal for destructive confirmations, Toastr for routine feedback — mixing them in one grid feels inconsistent fast.
  • Extension points worth knowing: subclass your own App\Admin\Actions\BaseAction to standardise authorize(), confirm(), and a logging wrapper; override protected html() to render icon buttons; override handleHtmlResponse() when an action returns partial HTML instead of JSON. For import, wrap the model's import contract in a base trait so every importable model gets the same validation, actor stamping, and error reporting.
  • Project-risk caveat — improved but unresolved: a 1.3.0 feature release (tree extension, Model-based import) is real evidence the fork is maintained, which moves this from "stale fork" to "small fork under active development". It is still a thin derivative of mainline Dcat Admin with a small user base and no dependents, and the version scheme (1.x) does not track upstream (2.x), so don't assume API parity from version numbers. Before committing: diff the Action / Grid / Tree APIs against upstream dcat/laravel-admin, pin the version in composer.json, and vendor the skills/references/ docs into your repo — upstream documentation remains your only support channel, and a minor-version tree change is exactly the kind of thing that can bite a subclass.
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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect