printnow/laravel-admin
PrintNow Laravel Admin is a Laravel package for quickly building admin panels and back-office interfaces. It provides CRUD resources, authentication, menus, and role-based access, streamlining dashboards, data tables, and management workflows for developers.
This is a drop-in fork of Dcat Admin: the Composer package is printnow/laravel-admin, but all namespaces, config keys, and Artisan commands remain Dcat\Admin, config/admin.php, and admin:*. That means every Dcat tutorial and existing controller you find applies unchanged — you migrate by swapping the Composer requirement, not by rewriting code.
Install (~5 minutes)
composer require printnow/laravel-admin
php artisan admin:publish # config/admin.php, app/Admin/, assets, migrations
php artisan admin:install # runs migrations + seeds default admin user
Then log in at /admin/auth/login (default admin / admin — change immediately).
Where to look first
config/admin.php — route prefix, auth guard, upload disk, menu, layout, extensions.app/Admin/routes.php — where your admin routes are registered.app/Admin/Controllers/HomeController.php — your dashboard entry point.app/Admin/bootstrap.php — global boot hooks (menus, extensions, Admin::script).First use case — scaffold one CRUD screen
php artisan admin:make PostController --model=App\\Models\\Post
This generates a controller with grid(), form(), and detail() methods. Register it in app/Admin/routes.php:
use Illuminate\Routing\Router;
Admin::routes();
Route::group([
'prefix' => config('admin.route.prefix'),
'namespace' => config('admin.route.namespace'),
'middleware' => config('admin.route.middleware'),
], function (Router $router) {
$router->resource('posts', PostController::class);
});
You now have a working list/create/edit/delete screen. Everything else in this package is a refinement of that one pattern.
Grid — list screens. Build declaratively; column modifiers are chainable and lazy.
protected function grid()
{
return Grid::make(new Post(), function (Grid $grid) {
$grid->model()->with(['author'])->orderByDesc('id');
$grid->column('id')->sortable();
$grid->column('title')->limit(40)->filter(); // quick search
$grid->column('author.name'); // eager-loaded relation
$grid->column('status')->using([1 => 'Draft', 2 => 'Live'])->label();
$grid->column('cover')->image('', 60, 60);
$grid->column('created_at');
$grid->filter(function (Grid\Filter $filter) {
$filter->equal('status')->select([1 => 'Draft', 2 => 'Live']);
$filter->between('created_at')->datetime();
});
$grid->actions(function (Grid\Displayers\Actions $actions) {
$actions->disableDelete();
$actions->append(new PublishAction()); // custom row action
});
$grid->batchActions(function ($batch) { $batch->add(new BulkArchive()); });
$grid->exporter(new PostExporter());
});
}
Form — create/edit screens. The same closure runs for both; use $form->isCreating() / isEditing() for divergence. Hooks are the extension point, not overridden methods.
protected function form()
{
return Form::make(new Post(), function (Form $form) {
$form->display('id');
$form->text('title')->required()->maxLength(120);
$form->slug('slug')->creationRules(['required', 'unique:posts,slug']);
$form->editor('body');
$form->image('cover')->uniqueName()->autoUpload()->disk('public');
$form->multipleImage('gallery')->sortable()->removable();
$form->select('status')->options(Post::STATUSES)->default(1);
$form->belongsTo('author_id', UserController::class);
$form->saving(function (Form $form) {
$form->slug = $form->slug ?: Str::slug($form->title);
});
$form->saved(function (Form $form, $result) {
Cache::forget("post:{$form->getKey()}");
});
});
}
Detail — read-only view. Rarely needed if you have a good Grid, but cheap to produce:
protected function detail($id)
{
return Show::make($id, new Post(), function (Show $show) {
$show->field('title');
$show->field('body')->unescape();
$show->field('created_at');
});
}
Custom pages. Skip the CRUD triad entirely for dashboards and reports:
public function index(Content $content)
{
return $content
->title('Overview')
->body(new RevenueChart()) // any Dcat\Admin\Widgets\* or Blade view
->row(function ($row) {
$row->column(4, new InfoBox('Orders', 'shopping-cart', 'blue', Order::count()));
$row->column(4, new InfoBox('Revenue', 'money', 'green', Order::sum('total')));
});
}
Permission & menu workflow. Menus are database rows, not code. Add routes in app/Admin/routes.php, then create the corresponding menu entry and role permissions at /admin/auth/menu and /admin/auth/roles — or seed them from database/seeders for repeatable environments. Route-level enforcement is the admin.permission middleware, which is already included in config('admin.route.middleware').
Extending with custom Actions. For any repeated operation (approve, sync, export), generate with php artisan admin:action PublishAction --grid-row and implement handle(). Row actions and batch actions are the cleanest way to add business logic without bloating controllers.
Integration tips
app/Admin/bootstrap.php for cross-cutting concerns: register menu items programmatically, add global JS/CSS via Admin::script() / Admin::style(), or extend the navbar.X-CSRF-TOKEN headers) to avoid the guard redirecting to login.Package identity is deliberately confusing. You install printnow/laravel-admin but work entirely with Dcat\Admin\*. Do not rename namespaces or config keys — the fork exists precisely so you can switch requirements without touching application code. If you have the original dcat/laravel-admin installed alongside this one, Composer will conflict on the Dcat\Admin namespace; remove the old requirement explicitly.
Version pinning is your safety net. The star count is low (46) and the fork's Laravel 13 / PHP 8.5 support is forward-looking. Pin the exact version in composer.json and read the changelog before bumping. Diff against upstream Dcat when something behaves unexpectedly — most forks carry a small delta, and the issue is often upstream, not here.
Asset publishing is required after upgrades. php artisan admin:publish (add --force to overwrite) must follow every composer update; otherwise you get blank pages or broken JS with no error in laravel.log.
admin:install is not idempotent. It runs migrations and seeds the default user. On an existing database, run php artisan admin:publish and then php artisan migrate manually, and review the migration names first — the admin tables (admin_users, admin_roles, admin_permissions, admin_menu, admin_operation_log) are prefixed and won't collide, but you may not want them in a shared schema.
Config and menu caching. After editing config/admin.php, run php artisan config:clear. Menus are stored in the admin_menu table and cached; clear with php artisan admin:menu regenerations or by editing through the UI. Stale menus are a common "why isn't my page showing" cause.
Permission middleware will 403 silently on new routes. New routes need a corresponding menu/permission row, or they won't appear and direct access will be denied. When debugging a blank admin section, always check the admin_menu and admin_permissions rows before the controller.
Pjax and custom JavaScript. Pages are loaded via Pjax, so $(document).ready() fires only on the first load. Register scripts with Admin::script() (it runs per page load) or bind on pjax:end. If a page misbehaves, disabling Pjax for it (check your version for Admin::disablePjax()) is the fastest way to isolate the cause.
Uploads. config/admin.php exposes upload.disk and upload.directory. If images 404, the two usual causes are a missing php artisan storage:link, or the public disk being private in config/filesystems.php. ->disk('public') on the field overrides the default per-field.
Admin::user() is null outside HTTP requests. Guard it in Artisan commands, queued jobs, and scheduled tasks — Admin::user()?->id rather than chained property access.
N+1 in Grids is the top performance trap. $grid->column('author.name') will lazy-load per row unless you add $grid->model()->with(['author']). Similarly, $grid->column('comments_count') needs ->withCount('comments') on the model, not a closure.
Soft deletes need explicit handling. Add $grid->model()->withTrashed() and a filter for trashed if you want to see and restore deleted records; otherwise they vanish from the list with no UI affordance.
Debugging checklist. php artisan route:list (confirm the route exists in the admin group), php artisan config:clear (after config changes), storage/logs/laravel.log (server-side errors), the browser Network tab (Pjax/AJAX responses), and APP_DEBUG=true locally to surface the actual exception instead of the friendly error page.
Extension points worth knowing. Admin::navbar() / Admin::menu() for chrome, Widgets\* for reusable panels, custom Grid\Tools\AbstractTool for toolbar buttons, and Form::* field extensions via Form\Field subclasses for domain-specific inputs. Reach for these before overriding framework internals — upgrades stay cheap that way.
How can I help you explore Laravel packages today?