easyadmin8/easyadmin8-laravel
EasyAdmin8 for Laravel provides a fast, ready-to-use admin backend for Laravel applications, with common admin features such as authentication, menu management, CRUD scaffolding, and role-based access control to help you build management panels quickly.
EasyAdmin8-Laravel is a port of the EasyAdmin8 admin framework into Laravel: RBAC (roles, menus, granular permission nodes), menu administration, CRUD scaffolding, auth, and a bundled admin UI. v8.3.5 is a patch release off 8.3.4 with only a compare link, so treat it as a routine dependency bump, not a migration. The class names and artisan signatures below are indicative — the package's own stubs/README are the source of truth, and the fastest way to learn the API is to generate one module and read what it emits.
Install (verify the exact Packagist name first with composer show -a or the repo README; both easyadmin8/easyadmin8-laravel and a shorter alias appear in the wild):
composer require easyadmin8/laravel
php artisan vendor:publish --provider="EasyAdmin8\Providers\EasyAdminServiceProvider"
php artisan migrate --seed
php artisan storage:link
Configure .env for the things that diverge between staging and production:
ADMIN_PATH=admin
ADMIN_GUARD=admin
ADMIN_UPLOAD_DISK=public
ADMIN_PAGINATION=20
Where to look first, in this order:
config/easyadmin.php — route prefix, guard, upload disk, layout/pagination defaults. This is the contract surface; everything else sits behind it.app/Http/Controllers/Admin/ — where generated controllers land. Read a generated one top to bottom before writing your own.resources/views/ + the published vendor views — grid and form templates you can override.ea_* tables (ea_admin, ea_role, ea_menu, ea_config, plus whatever pivot links roles to menus) — menus and permissions are data, not code. Most "why can't this role see this?" questions end here.First use case — the loop that exercises ~90% of the day-to-day surface:
Pick an existing Eloquent model (say Order). Scaffold one admin CRUD module for it, register its menu node, attach that node to a non-super-admin role, then log in as that role and confirm (a) the menu item renders, and (b) hitting the controller URL directly returns a 403. If you can do that end to end in an afternoon, the package fits. If the permission gate lets the direct URL through, stop and understand the middleware stack before building anything else on top of it.
CRUD modules. The workflow is: create the model/migration in Laravel normally, then generate (or hand-write) an admin controller extending the package's base admin controller. Define the grid (columns, searchable fields, filters, row actions) and the form (fields, validation, relationships) declaratively. Illustratively:
namespace App\Http\Controllers\Admin;
use App\Models\Customer;
use App\Models\Order;
use EasyAdmin8\Controllers\AdminBaseController; // confirm namespace from a generated stub
class OrderController extends AdminBaseController
{
protected $model = Order::class;
protected function grid(): array
{
return [
'columns' => [
'id' => ['label' => 'ID', 'sortable' => true],
'number' => ['label' => 'Order #', 'searchable' => true],
'customer.name'=> ['label' => 'Customer', 'searchable' => true],
'total' => ['label' => 'Total', 'sortable' => true],
'status' => ['label' => 'Status', 'filterable' => true],
'created_at' => ['label' => 'Created', 'sortable' => true],
],
'perPage' => config('easyadmin.pagination'),
];
}
protected function form($id = null): array
{
return [
'fields' => [
'number' => ['type' => 'text', 'rules' => 'required|max:32'],
'customer_id' => ['type' => 'select', 'options' => Customer::pluck('name', 'id')],
'total' => ['type' => 'decimal', 'rules' => 'required|numeric|min:0'],
'status' => ['type' => 'select', 'options' => OrderStatus::options()],
],
];
}
}
Keep grids lean. Every exposed column is a query cost and a rendering cost; every eager-loaded relationship in a grid is an N+1 waiting to happen. Constrain with() explicitly on the list query.
Permissions are data — seed them, don't click them. Menu nodes and role grants live in ea_* tables and are edited through the admin UI, which is fine for exploration and terrible for reproducibility. Put permission nodes in an idempotent seeder keyed on a stable identifier (the permission string), and attach role grants the same way:
$menu = DB::table('ea_menu')->updateOrCreate(
['permission' => 'order.index'], // stable key
[
'name' => 'Orders',
'parent_id' => 0,
'url' => 'order/index',
'icon' => 'fa-shopping-cart',
'sort' => 10,
'status' => 1,
]
);
// Pivot/table name for role→menu grants varies by version — confirm against the schema
DB::table('ea_role_menu')->updateOrInsert(
['role_id' => $opsRoleId, 'menu_id' => $menu->id],
[]
);
Run this seeder in CI so every environment (and every new dev's laptop) has the same permission graph. Idempotency matters more here than anywhere else in the app: a duplicate permission node produces confusing "granted but denied" behavior that is hard to diagnose from the UI.
Route and middleware integration. Admin routes sit behind a dedicated guard and middleware stack. Add your own cross-cutting concerns (tenant scoping, IP allowlist, audit logging, impersonation guards) by appending middleware to the admin group via config or a service provider — never by editing vendor route files, because those get overwritten on upgrade. For front-end or mobile consumption, expose read-only JSON endpoints outside the admin prefix and reuse the same models/services rather than reaching through admin controllers.
Guard discipline in shared code. The admin guard is not web. Any service, policy, observer, or Blade partial that calls Auth::user() may silently get null inside admin context. Resolve explicitly:
Auth::guard(config('easyadmin.guard', 'admin'))->user();
If you share services between the customer-facing app and the admin, pass the acting user in as a parameter rather than reaching for the facade inside the service.
Config and environment. Drive prefix, guard, disk, and pagination from env. Override only the keys you actually change in config/easyadmin.php; don't fork the whole file, or patch releases that add new keys will leave you with a config that's missing them. When a new config key appears in an upgrade diff, merge it into your override rather than regenerating.
Extending the UI. Publish and override Blade views; never edit vendor files. Keep custom CSS/JS in your own Vite/webpack pipeline, not in the published asset directory — a vendor:publish --force during an upgrade will erase anything you put there. Use the package's view-override path or a view namespace so your overrides take precedence deterministically.
Audit and heavy operations. Wire login/operation logs into your existing log channel and alerting. Admin controllers are synchronous by default: wrap multi-table writes in DB::transaction() and push anything slow (exports, bulk status changes, third-party sync) onto a queued job with a progress indicator. An admin request that takes 30 seconds is a support ticket.
Auth::user() returning null in an admin-context service, a policy, or a shared Blade component is almost always the guard default. Check config/auth.php and always resolve the admin guard explicitly in admin-facing code.composer update, re-publish assets and hard-refresh. A half-updated asset set produces layout and JS errors that look like application bugs and waste an hour of debugging.php artisan route:list --path=<prefix> and confirm the middleware and controller actually handling each URL. Register specific routes before wildcards.php artisan storage:link, the configured disk's root and permissions, and PHP's upload_max_filesize / post_max_size. Silent no-op uploads skew heavily toward disk/permission misconfiguration.insert() into ea_menu / grant tables fails on re-run and duplicates nodes. Use updateOrCreate / updateOrInsert keyed on the permission string.storage/logs/laravel.log → APP_DEBUG=true locally → php artisan route:list → log the resolved role and permission set at the gate. Don't guess at why a permission was denied; print the set the middleware actually evaluated."easyadmin8/laravel": "8.3.*" (or the exact constraint matching how you installed it) in composer.json. Because release notes for 8.3.5 are just a compare link, review the commit range yourself before any bump — particularly for changes touching auth, middleware, permissions, or migrations.vendor/. That single discipline is what makes the next patch bump a five-minute composer update instead of a merge project.How can I help you explore Laravel packages today?