dcat/laravel-admin
dcat/laravel-admin is a fast, elegant admin panel for Laravel. Build CRUD interfaces with rich grids, forms, filters, and widgets in minutes. Includes authentication, permissions, menus, and customizable UI components, making backend development simple and productive.
Dcat Admin is a Laravel package (not a standalone app) that registers its own service provider, auth guard, routes, and asset pipeline under a configurable prefix (default /admin). Think of it as a model-driven CRUD framework: you write Grid, Form, and Show builders in a controller, and the package renders a full Bootstrap 4 admin UI around them.
Note on release state: the current line is still the 2.2.x-beta track (latest: 2.2.3-beta). There is no stable 2.x tagged release, and no official Laravel 11/12 support. Treat composer require dcat/laravel-admin as pulling a beta — pin the exact version in composer.json.
Install (2.x, Laravel 8–10):
composer require dcat/laravel-admin
php artisan admin:publish # publishes config/admin.php, migrations, assets, app/Admin skeleton
php artisan admin:install # runs migrations, seeds admin user, creates app/Admin
Then log in at /admin with admin / admin and change that immediately.
Where to look first, in order:
config/admin.php — route prefix, auth.guard, DB connection, upload disk, directory (the app/Admin path).app/Admin/routes.php — every admin page is a normal Laravel route registered here; nothing is auto-discovered.app/Admin/bootstrap.php — global extension points (Grid::resolving(), Form::resolving(), Admin::css(), Admin::jsVariable(), menu/asset tweaks).app/Admin/Controllers/* — the scaffolding is the best documentation the project has.First use case — a working CRUD in three commands:
php artisan admin:make UserController --model=App\\Models\\User
Add to app/Admin/routes.php:
$router->resource('users', UserController::class);
Reload /admin, then open Auth → Menu and add a menu item pointing at users. You now have list/create/edit/delete, a filter panel, pagination, and a detail page — with roughly zero HTML. From there, edit the generated grid() and form() methods; that loop (scaffold → refine builder → add menu) is 80% of day-to-day work.
Treat scaffolding as a draft, not the deliverable. admin:make produces generic builders with every column exposed. Immediately prune columns, add ->sortable(), add a $grid->filter(), and tighten the form with ->required()/->rules(). Reviewers will otherwise see password hashes and internal IDs in the list view. (2.2.3-beta also fixes the scaffolded primary key — if you generate against non-incrementing / UUID keys on an older beta, expect to hand-correct $grid->model()->getKeyName().)
Grid: query shaping belongs on the model.
protected function grid()
{
return Grid::make(new User(), function (Grid $grid) {
$grid->model()->with(['roles', 'profile'])->where('is_banned', 0);
$grid->column('id')->sortable();
$grid->column('name')->display(fn ($v) => Str::limit($v, 20));
$grid->column('status')->using([0 => 'Off', 1 => 'On'])->label();
$grid->column('created_at')->sortable();
$grid->filter(function (Grid\Filter $filter) {
$filter->equal('id');
$filter->like('name');
$filter->between('created_at')->datetime();
});
$grid->quickSearch('name', 'email');
$grid->disableCreateButton(); // when appropriate
});
}
Inline editing beats round-tripping to the edit form. ->switch(), ->editable(), and ->toggle() handle the common "flip a status" case without a controller change. These submit via AJAX using the grid's own update path, so keep field names aligned with real columns.
Column actions now accept a payload (2.2.3-beta). When you render a ->actions() entry from data, you can pass row context through instead of baking it into the URL:
$grid->actions(function (Grid\Displayers\Actions $actions) {
$actions->append(
new Grid\Actions\Action(
'Publish',
fn ($payload) => ... // payload carried per-row
)
);
});
If you were string-building row actions before, this is the cleaner replacement for anything that previously interpolated IDs into a JS handler.
Form: use saving/saved hooks instead of controller code.
$form->text('name')->required();
$form->email('email')->required()
->rules(['required', 'email', Rule::unique('users', 'email')->ignore($form->model()->id)]);
$form->password('password')->minLength(8);
$form->select('role_id')->options(Role::pluck('name', 'id'));
$form->saving(function (Form $form) {
if ($form->password && $form->password !== $form->model()->password) {
$form->password = bcrypt($form->password);
}
});
The $form->model() accessor (current record) and $form->isCreating()/$form->isEditing() cover nearly every conditional-field need, including $form->hidden() / $form->ignore() for fields that should never be persisted.
File/multi-file fields: the File field's multi-upload count limit was fixed in 2.2.3-beta — ->limit()/->max() was off-by-one (or ignored entirely) in older betas. If you shipped a workaround around the widget JS, remove it before upgrading.
Row and batch actions are the extension primitive. Generate with php artisan admin:action Post\\Publish --grid-row / --grid-batch / --detail-tool / --form-tool. Actions are self-contained classes with handle() and confirm()/dialog(); use them for anything with business logic (approve, resend, export a subset) rather than cramming more into saving. ->dialog() on a form action gives you a modal with a mini-form — very good for "reason for rejection" flows.
Detail page: Show::make($id, new User(), ...) with $show->field('col')->as(...), and $show->relation('orders', ...) for nested grids. Keep the detail page read-only and push mutations into actions.
Menus and RBAC are UI-managed database rows. Roles/permissions live in admin_roles, admin_permissions, admin_menu. Don't hardcode menu structure in code — build it in the UI so non-developers can reorder it. Permission checks in custom code: Admin::user()->can('users.edit') and Admin::user()->inRoles([...]).
Authorization is now first-class (2.2.3-beta). The Administrator model implements Laravel's Authorizable contract, so Admin::user()->can(), ->cannot(), ->canAny() and @can behave like any other authenticated model. Combine with the new authorization check on RenderableController (Show/detail pages and any custom renderables now enforce permissions) — meaning: assign permissions to existing roles after upgrading or users who previously saw detail widgets may suddenly be denied.
Custom pages: generate a controller, then return Admin::view('admin::your-view') or a Card/Box widget from Dcat\Admin\Widgets\*. For dashboard KPI cards, use Widgets\Metrics\Line|Bar|Doughnut|Card and wire data in app/Admin/Controllers/HomeController.php.
Integration tips: the admin area is a separate guard (admin) — do not expect Auth::user() to work inside admin pages. Share data via app/Admin/bootstrap.php (Admin::script(), Admin::css(), view composers) rather than editing vendor views. For per-locale JS strings, Admin::jsVariable() is now overridable — extend it in bootstrap.php to inject custom translations into the front-end instead of patching vendor lang files.
2.2.3-beta; the project has never shipped a stable 2.x and has no official Laravel 11/12 branch. This package is deeply coupled to Laravel internals (route registration, pagination views, auth), so a major framework bump reliably breaks it. Pin laravel/framework to a supported major, pin dcat/laravel-admin to an exact beta, and budget time for a fork before bumping Laravel.admin:install — it now runs migration files rather than issuing ad-hoc "create table" statements, so custom schema tweaks belong in migrations, not edits to the installer.RenderableController now enforces authorization (see above). Silent 403s after upgrade are almost always this.embeds — uploads were being flagged as edits and saved unintentionally; that bug is fixed, so any custom "don't save the embed body" workaround can go.saving hook shown above, or $form->password('password')->ignore() on edit.Rule::unique(...)->ignore($form->model()->id) over the {{id}} string syntax — the string form is order-sensitive and silently wrong on create.->display() touching a relation loads per row. Declare $grid->model()->with(...) up front; the list view will otherwise hammer the DB at 50 rows/page.php artisan cache:clear fixes config-level staleness); also confirm the menu's URI exactly matches the controller action.<script> and $(document).ready() in custom views won't re-run on navigation. Use Admin::script() / Admin::scriptAfter() or bind to the pjax:complete event. This accounts for most "my JS works once then stops" reports.php artisan admin:publish --force when the CSS/JS bundle changes; a stale public/vendor/dcat-admin produces ghost layout bugs. Same rule after every beta bump — the vendor bundle is not tracked by Composer.config/admin.php upload.disk/directory and run php artisan storage:link; otherwise uploads succeed but render 404.web that assumes JSON responses can break the grid's AJAX calls.admin:import <vendor/package> registers them, but many extensions predate Laravel 9/10 — and almost none are tested against the current beta. Audit before depending on one; a small custom action/field is usually cheaper than debugging a stale plugin.Admin::disablePjax() (or setting a debug flag in app/Admin/bootstrap.php) makes normal full-page loads and browser devtools behave predictably — temporarily disable it when a bug is "invisible."How can I help you explore Laravel packages today?