Summary: Master the art of building custom admin panels in Laravel from scratch. Learn advanced techniques for creating flexible, maintainable admin interfaces without relying on pre-built packages, with complete control over functionality and design.
Table Of Contents
- Introduction
- Foundation Architecture
- Dynamic Form Builder
- Advanced Data Table System
- Permission and Role Management
- Dashboard and Analytics
- Related Posts
- Conclusion
Introduction
While Laravel Nova and Filament are excellent solutions for rapid admin panel development, there are scenarios where building a custom admin panel from scratch provides better long-term value. Custom admin panels offer complete control over functionality, design, and user experience, enabling you to create tailored solutions that perfectly match your business requirements.
Building admin panels from scratch isn't just about avoiding third-party dependencies—it's about creating maintainable, scalable systems that can evolve with your application. This approach provides deeper understanding of your codebase, eliminates vendor lock-in, and offers unlimited customization possibilities while maintaining the flexibility to adapt to changing requirements.
Foundation Architecture
Core Admin Panel Structure
Let's start by establishing a solid foundation for our custom admin panel:
// app/Http/Controllers/Admin/BaseController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
abstract class BaseController extends Controller
{
protected string $viewPath;
protected string $routePrefix;
protected array $breadcrumbs = [];
public function __construct()
{
$this->middleware(['auth', 'admin']);
$this->setupController();
}
abstract protected function setupController(): void;
protected function view(string $view, array $data = []): View
{
$data['breadcrumbs'] = $this->breadcrumbs;
$data['routePrefix'] = $this->routePrefix;
return view("{$this->viewPath}.{$view}", $data);
}
protected function redirectWithSuccess(string $route, string $message = 'Operation completed successfully'): RedirectResponse
{
return redirect()->route($route)->with('success', $message);
}
protected function redirectWithError(string $route, string $message = 'An error occurred'): RedirectResponse
{
return redirect()->route($route)->with('error', $message);
}
protected function addBreadcrumb(string $title, ?string $route = null): void
{
$this->breadcrumbs[] = [
'title' => $title,
'route' => $route
];
}
}
Resource Management System
Create a flexible resource management system:
// app/Http/Controllers/Admin/ResourceController.php
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
abstract class ResourceController extends BaseController
{
protected string $model;
protected string $resourceName;
protected array $searchableFields = [];
protected array $filterableFields = [];
protected array $sortableFields = [];
protected int $perPage = 25;
abstract protected function getValidationRules(Request $request, ?Model $model = null): array;
abstract protected function prepareForSave(array $data, ?Model $model = null): array;
public function index(Request $request): View
{
$query = $this->model::query();
// Apply search
if ($search = $request->get('search')) {
$this->applySearch($query, $search);
}
// Apply filters
$this->applyFilters($query, $request);
// Apply sorting
$this->applySorting($query, $request);
$resources = $query->paginate($this->perPage);
return $this->view('index', [
'resources' => $resources,
'searchableFields' => $this->searchableFields,
'filterableFields' => $this->filterableFields,
'sortableFields' => $this->sortableFields,
'filters' => $request->all()
]);
}
public function create(): View
{
$resource = new $this->model;
return $this->view('create', compact('resource'));
}
public function store(Request $request): RedirectResponse
{
$rules = $this->getValidationRules($request);
$validated = $request->validate($rules);
$data = $this->prepareForSave($validated);
$resource = $this->model::create($data);
return $this->redirectWithSuccess(
"admin.{$this->resourceName}.show",
"{$this->resourceName} created successfully"
)->with('resource', $resource);
}
public function show(Model $resource): View
{
return $this->view('show', compact('resource'));
}
public function edit(Model $resource): View
{
return $this->view('edit', compact('resource'));
}
public function update(Request $request, Model $resource): RedirectResponse
{
$rules = $this->getValidationRules($request, $resource);
$validated = $request->validate($rules);
$data = $this->prepareForSave($validated, $resource);
$resource->update($data);
return $this->redirectWithSuccess(
"admin.{$this->resourceName}.show",
"{$this->resourceName} updated successfully"
)->with('resource', $resource);
}
public function destroy(Model $resource): RedirectResponse
{
$resource->delete();
return $this->redirectWithSuccess(
"admin.{$this->resourceName}.index",
"{$this->resourceName} deleted successfully"
);
}
public function bulkAction(Request $request): JsonResponse
{
$request->validate([
'action' => 'required|string',
'ids' => 'required|array',
'ids.*' => 'integer|exists:' . (new $this->model)->getTable() . ',id'
]);
$action = $request->get('action');
$ids = $request->get('ids');
$result = $this->executeBulkAction($action, $ids);
return response()->json($result);
}
protected function applySearch($query, string $search): void
{
if (empty($this->searchableFields)) {
return;
}
$query->where(function ($q) use ($search) {
foreach ($this->searchableFields as $field) {
if (str_contains($field, '.')) {
// Handle relationship searches
[$relation, $column] = explode('.', $field, 2);
$q->orWhereHas($relation, function ($subQuery) use ($column, $search) {
$subQuery->where($column, 'LIKE', "%{$search}%");
});
} else {
$q->orWhere($field, 'LIKE', "%{$search}%");
}
}
});
}
protected function applyFilters($query, Request $request): void
{
foreach ($this->filterableFields as $field => $config) {
$value = $request->get($field);
if ($value === null || $value === '') {
continue;
}
match ($config['type']) {
'select' => $query->where($field, $value),
'date_range' => $this->applyDateRangeFilter($query, $field, $value),
'boolean' => $query->where($field, (bool) $value),
'numeric_range' => $this->applyNumericRangeFilter($query, $field, $value),
default => $query->where($field, 'LIKE', "%{$value}%")
};
}
}
protected function applySorting($query, Request $request): void
{
$sortBy = $request->get('sort', 'created_at');
$sortDirection = $request->get('direction', 'desc');
if (in_array($sortBy, $this->sortableFields)) {
$query->orderBy($sortBy, $sortDirection);
} else {
$query->orderBy('created_at', 'desc');
}
}
protected function executeBulkAction(string $action, array $ids): array
{
$resources = $this->model::whereIn('id', $ids);
return match ($action) {
'delete' => $this->bulkDelete($resources),
'restore' => $this->bulkRestore($resources),
'export' => $this->bulkExport($resources),
default => ['success' => false, 'message' => 'Unknown action']
};
}
protected function bulkDelete($query): array
{
$count = $query->count();
$query->delete();
return [
'success' => true,
'message' => "Successfully deleted {$count} {$this->resourceName}(s)"
];
}
}
Advanced User Management Example
// app/Http/Controllers/Admin/UserController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Models\User;
use App\Models\Role;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
class UserController extends ResourceController
{
protected function setupController(): void
{
$this->model = User::class;
$this->resourceName = 'users';
$this->viewPath = 'admin.users';
$this->routePrefix = 'admin.users';
$this->searchableFields = ['name', 'email', 'roles.name'];
$this->filterableFields = [
'status' => ['type' => 'select', 'options' => ['active', 'inactive']],
'role_id' => ['type' => 'select', 'relationship' => 'roles'],
'created_at' => ['type' => 'date_range'],
'last_login_at' => ['type' => 'date_range']
];
$this->sortableFields = ['name', 'email', 'created_at', 'last_login_at'];
$this->addBreadcrumb('Dashboard', 'admin.dashboard');
$this->addBreadcrumb('Users');
}
protected function getValidationRules(Request $request, ?Model $user = null): array
{
return [
'name' => 'required|string|max:255',
'email' => [
'required',
'email',
'max:255',
Rule::unique('users')->ignore($user?->id)
],
'password' => $user ? 'nullable|string|min:8|confirmed' : 'required|string|min:8|confirmed',
'roles' => 'array',
'roles.*' => 'exists:roles,id',
'status' => 'required|in:active,inactive',
'profile.bio' => 'nullable|string|max:1000',
'profile.phone' => 'nullable|string|max:20',
'profile.avatar' => 'nullable|image|max:2048'
];
}
protected function prepareForSave(array $data, ?Model $user = null): array
{
// Handle password
if (isset($data['password']) && $data['password']) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
// Handle avatar upload
if (isset($data['profile']['avatar'])) {
$data['profile']['avatar_path'] = $data['profile']['avatar']->store('avatars', 'public');
unset($data['profile']['avatar']);
}
return $data;
}
public function store(Request $request)
{
$rules = $this->getValidationRules($request);
$validated = $request->validate($rules);
$userData = $this->prepareForSave($validated);
$roles = $userData['roles'] ?? [];
unset($userData['roles']);
$user = User::create($userData);
// Handle profile data
if (isset($validated['profile'])) {
$user->profile()->create($validated['profile']);
}
// Sync roles
if (!empty($roles)) {
$user->roles()->sync($roles);
}
return $this->redirectWithSuccess(
'admin.users.show',
'User created successfully'
)->with('user', $user);
}
public function update(Request $request, User $user)
{
$rules = $this->getValidationRules($request, $user);
$validated = $request->validate($rules);
$userData = $this->prepareForSave($validated, $user);
$roles = $userData['roles'] ?? [];
unset($userData['roles']);
$user->update($userData);
// Handle profile data
if (isset($validated['profile'])) {
$user->profile()->updateOrCreate([], $validated['profile']);
}
// Sync roles
$user->roles()->sync($roles);
return $this->redirectWithSuccess(
'admin.users.show',
'User updated successfully'
)->with('user', $user);
}
public function impersonate(User $user)
{
if (!auth()->user()->can('impersonate-users')) {
abort(403);
}
session(['impersonating' => $user->id]);
return redirect()->route('dashboard')
->with('info', "You are now impersonating {$user->name}");
}
public function stopImpersonating()
{
session()->forget('impersonating');
return redirect()->route('admin.users.index')
->with('info', 'Impersonation stopped');
}
public function export(Request $request)
{
$request->validate([
'format' => 'required|in:csv,xlsx,pdf',
'filters' => 'array'
]);
$query = User::query();
// Apply same filters as index
if ($request->has('filters')) {
$this->applyFilters($query, new Request($request->get('filters')));
}
$filename = 'users_export_' . now()->format('Y-m-d_H-i-s');
return match ($request->get('format')) {
'csv' => $this->exportToCsv($query->get(), $filename),
'xlsx' => $this->exportToExcel($query->get(), $filename),
'pdf' => $this->exportToPdf($query->get(), $filename),
};
}
}
Dynamic Form Builder
Form Field System
Create a flexible form building system:
// app/Services/Admin/FormBuilder.php
<?php
namespace App\Services\Admin;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class FormBuilder
{
private array $fields = [];
private array $sections = [];
private ?Model $model = null;
public function setModel(?Model $model): self
{
$this->model = $model;
return $this;
}
public function addSection(string $title, string $key = null): self
{
$key = $key ?? \Str::slug($title);
$this->sections[] = [
'key' => $key,
'title' => $title,
'fields' => []
];
return $this;
}
public function addField(string $type, string $name, array $options = []): self
{
$field = [
'type' => $type,
'name' => $name,
'label' => $options['label'] ?? \Str::title(str_replace('_', ' ', $name)),
'value' => $this->getFieldValue($name, $options['default'] ?? null),
'options' => $options,
'section' => $options['section'] ?? 'default'
];
$this->fields[$name] = $field;
// Add field to appropriate section
$sectionIndex = $this->findSectionIndex($field['section']);
if ($sectionIndex !== -1) {
$this->sections[$sectionIndex]['fields'][] = $name;
}
return $this;
}
public function text(string $name, array $options = []): self
{
return $this->addField('text', $name, $options);
}
public function email(string $name, array $options = []): self
{
return $this->addField('email', $name, $options);
}
public function password(string $name, array $options = []): self
{
return $this->addField('password', $name, $options);
}
public function textarea(string $name, array $options = []): self
{
return $this->addField('textarea', $name, array_merge(['rows' => 4], $options));
}
public function select(string $name, array $selectOptions, array $options = []): self
{
$options['select_options'] = $selectOptions;
return $this->addField('select', $name, $options);
}
public function multiSelect(string $name, array $selectOptions, array $options = []): self
{
$options['select_options'] = $selectOptions;
$options['multiple'] = true;
return $this->addField('multi_select', $name, $options);
}
public function checkbox(string $name, array $options = []): self
{
return $this->addField('checkbox', $name, $options);
}
public function radio(string $name, array $radioOptions, array $options = []): self
{
$options['radio_options'] = $radioOptions;
return $this->addField('radio', $name, $options);
}
public function file(string $name, array $options = []): self
{
return $this->addField('file', $name, $options);
}
public function date(string $name, array $options = []): self
{
return $this->addField('date', $name, $options);
}
public function datetime(string $name, array $options = []): self
{
return $this->addField('datetime', $name, $options);
}
public function number(string $name, array $options = []): self
{
return $this->addField('number', $name, $options);
}
public function richText(string $name, array $options = []): self
{
return $this->addField('rich_text', $name, $options);
}
public function relationSelect(string $name, string $relationModel, array $options = []): self
{
$options['relation_model'] = $relationModel;
$options['relation_display'] = $options['display_field'] ?? 'name';
$options['relation_value'] = $options['value_field'] ?? 'id';
return $this->addField('relation_select', $name, $options);
}
public function dependentSelect(string $name, string $dependsOn, string $endpoint, array $options = []): self
{
$options['depends_on'] = $dependsOn;
$options['endpoint'] = $endpoint;
return $this->addField('dependent_select', $name, $options);
}
public function render(): array
{
return [
'sections' => $this->sections,
'fields' => $this->fields,
'model' => $this->model
];
}
private function getFieldValue(string $name, $default = null)
{
if (!$this->model) {
return old($name, $default);
}
// Handle nested field names (e.g., 'profile.bio')
if (str_contains($name, '.')) {
$parts = explode('.', $name);
$value = $this->model;
foreach ($parts as $part) {
$value = $value?->{$part};
}
return old($name, $value ?? $default);
}
return old($name, $this->model->{$name} ?? $default);
}
private function findSectionIndex(string $sectionKey): int
{
foreach ($this->sections as $index => $section) {
if ($section['key'] === $sectionKey) {
return $index;
}
}
// Create default section if not found
if ($sectionKey === 'default') {
$this->addSection('General Information', 'default');
return count($this->sections) - 1;
}
return -1;
}
}
Dynamic Form Component
// app/View/Components/Admin/DynamicForm.php
<?php
namespace App\View\Components\Admin;
use App\Services\Admin\FormBuilder;
use Illuminate\View\Component;
class DynamicForm extends Component
{
public array $formData;
public string $action;
public string $method;
public function __construct(
FormBuilder $builder,
string $action,
string $method = 'POST'
) {
$this->formData = $builder->render();
$this->action = $action;
$this->method = $method;
}
public function render()
{
return view('admin.components.dynamic-form');
}
}
Advanced Data Table System
Configurable Data Tables
// app/Services/Admin/DataTableBuilder.php
<?php
namespace App\Services\Admin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
class DataTableBuilder
{
private Builder $query;
private array $columns = [];
private array $actions = [];
private array $bulkActions = [];
private array $filters = [];
private ?string $searchPlaceholder = null;
private bool $showCheckboxes = true;
private int $perPage = 25;
public function __construct(Builder $query)
{
$this->query = $query;
}
public function addColumn(string $key, array $config): self
{
$this->columns[$key] = array_merge([
'label' => \Str::title(str_replace('_', ' ', $key)),
'sortable' => false,
'searchable' => false,
'type' => 'text',
'format' => null,
'relation' => null,
'accessor' => null
], $config);
return $this;
}
public function textColumn(string $key, string $label = null, bool $sortable = true): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'text',
'sortable' => $sortable,
'searchable' => true
]);
}
public function numberColumn(string $key, string $label = null, string $format = null): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'number',
'format' => $format,
'sortable' => true
]);
}
public function dateColumn(string $key, string $label = null, string $format = 'Y-m-d'): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'date',
'format' => $format,
'sortable' => true
]);
}
public function booleanColumn(string $key, string $label = null): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'boolean',
'sortable' => true
]);
}
public function imageColumn(string $key, string $label = null, array $options = []): self
{
return $this->addColumn($key, array_merge([
'label' => $label,
'type' => 'image',
'width' => 50,
'height' => 50
], $options));
}
public function relationColumn(string $key, string $relation, string $field, string $label = null): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'relation',
'relation' => $relation,
'field' => $field,
'sortable' => true,
'searchable' => true
]);
}
public function customColumn(string $key, callable $accessor, string $label = null): self
{
return $this->addColumn($key, [
'label' => $label,
'type' => 'custom',
'accessor' => $accessor
]);
}
public function addAction(string $key, array $config): self
{
$this->actions[$key] = array_merge([
'label' => \Str::title($key),
'route' => null,
'permission' => null,
'class' => 'btn btn-sm btn-primary',
'icon' => null,
'condition' => null
], $config);
return $this;
}
public function editAction(string $route = null): self
{
return $this->addAction('edit', [
'label' => 'Edit',
'route' => $route ?? 'admin.{resource}.edit',
'class' => 'btn btn-sm btn-outline-primary',
'icon' => 'edit'
]);
}
public function deleteAction(string $route = null): self
{
return $this->addAction('delete', [
'label' => 'Delete',
'route' => $route ?? 'admin.{resource}.destroy',
'class' => 'btn btn-sm btn-outline-danger',
'icon' => 'trash',
'confirm' => 'Are you sure you want to delete this item?'
]);
}
public function addBulkAction(string $key, string $label, string $endpoint = null): self
{
$this->bulkActions[$key] = [
'label' => $label,
'endpoint' => $endpoint,
'confirm' => $key === 'delete' ? 'Are you sure you want to delete selected items?' : null
];
return $this;
}
public function addFilter(string $key, array $config): self
{
$this->filters[$key] = array_merge([
'type' => 'text',
'label' => \Str::title(str_replace('_', ' ', $key)),
'options' => []
], $config);
return $this;
}
public function setSearchPlaceholder(string $placeholder): self
{
$this->searchPlaceholder = $placeholder;
return $this;
}
public function hideCheckboxes(): self
{
$this->showCheckboxes = false;
return $this;
}
public function setPerPage(int $perPage): self
{
$this->perPage = $perPage;
return $this;
}
public function build(Request $request): array
{
$this->applyFilters($request);
$this->applySearch($request);
$this->applySorting($request);
$data = $this->query->paginate($this->perPage);
return [
'data' => $data,
'columns' => $this->columns,
'actions' => $this->actions,
'bulk_actions' => $this->bulkActions,
'filters' => $this->filters,
'search_placeholder' => $this->searchPlaceholder,
'show_checkboxes' => $this->showCheckboxes,
'current_filters' => $request->all()
];
}
private function applyFilters(Request $request): void
{
foreach ($this->filters as $key => $filter) {
$value = $request->get($key);
if ($value === null || $value === '') {
continue;
}
match ($filter['type']) {
'select' => $this->query->where($key, $value),
'date_range' => $this->applyDateRangeFilter($key, $value),
'boolean' => $this->query->where($key, (bool) $value),
default => $this->query->where($key, 'LIKE', "%{$value}%")
};
}
}
private function applySearch(Request $request): void
{
$search = $request->get('search');
if (!$search) {
return;
}
$searchableColumns = array_filter($this->columns, fn($col) => $col['searchable']);
if (empty($searchableColumns)) {
return;
}
$this->query->where(function ($q) use ($search, $searchableColumns) {
foreach ($searchableColumns as $key => $column) {
if ($column['type'] === 'relation') {
$q->orWhereHas($column['relation'], function ($subQuery) use ($column, $search) {
$subQuery->where($column['field'], 'LIKE', "%{$search}%");
});
} else {
$q->orWhere($key, 'LIKE', "%{$search}%");
}
}
});
}
private function applySorting(Request $request): void
{
$sortBy = $request->get('sort');
$sortDirection = $request->get('direction', 'asc');
if ($sortBy && isset($this->columns[$sortBy]) && $this->columns[$sortBy]['sortable']) {
if ($this->columns[$sortBy]['type'] === 'relation') {
$relation = $this->columns[$sortBy]['relation'];
$field = $this->columns[$sortBy]['field'];
$this->query->join(
\Str::plural($relation),
$this->query->getModel()->getTable() . '.' . $relation . '_id',
'=',
\Str::plural($relation) . '.id'
)->orderBy(\Str::plural($relation) . '.' . $field, $sortDirection);
} else {
$this->query->orderBy($sortBy, $sortDirection);
}
} else {
$this->query->orderBy('created_at', 'desc');
}
}
}
Permission and Role Management
Advanced Permission System
// app/Services/Admin/PermissionManager.php
<?php
namespace App\Services\Admin;
use App\Models\User;
use App\Models\Permission;
use App\Models\Role;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class PermissionManager
{
public function getUserPermissions(User $user): Collection
{
return Cache::remember(
"user_permissions_{$user->id}",
3600,
fn() => $this->calculateUserPermissions($user)
);
}
public function hasPermission(User $user, string $permission, $resource = null): bool
{
$permissions = $this->getUserPermissions($user);
// Check direct permission
if ($permissions->contains('name', $permission)) {
return true;
}
// Check wildcard permissions
$wildcardPermission = explode('.', $permission)[0] . '.*';
if ($permissions->contains('name', $wildcardPermission)) {
return true;
}
// Check resource-specific permissions
if ($resource) {
return $this->hasResourcePermission($user, $permission, $resource);
}
return false;
}
public function hasRole(User $user, string $role): bool
{
return $user->roles()->where('name', $role)->exists();
}
public function hasAnyRole(User $user, array $roles): bool
{
return $user->roles()->whereIn('name', $roles)->exists();
}
public function assignRole(User $user, string|Role $role): void
{
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$user->roles()->syncWithoutDetaching([$role->id]);
$this->clearUserPermissionCache($user);
}
public function removeRole(User $user, string|Role $role): void
{
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$user->roles()->detach($role->id);
$this->clearUserPermissionCache($user);
}
public function grantPermission(User $user, string|Permission $permission): void
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
$user->permissions()->syncWithoutDetaching([$permission->id]);
$this->clearUserPermissionCache($user);
}
public function revokePermission(User $user, string|Permission $permission): void
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
$user->permissions()->detach($permission->id);
$this->clearUserPermissionCache($user);
}
public function createPermissionHierarchy(): array
{
$permissions = Permission::all();
$hierarchy = [];
foreach ($permissions as $permission) {
$parts = explode('.', $permission->name);
$current = &$hierarchy;
foreach ($parts as $part) {
if (!isset($current[$part])) {
$current[$part] = [];
}
$current = &$current[$part];
}
$current['_permission'] = $permission;
}
return $hierarchy;
}
public function getResourcePermissions(string $resource): Collection
{
$patterns = [
"{$resource}.*",
"{$resource}.view",
"{$resource}.create",
"{$resource}.edit",
"{$resource}.delete"
];
return Permission::whereIn('name', $patterns)->get();
}
private function calculateUserPermissions(User $user): Collection
{
// Get permissions from roles
$rolePermissions = $user->roles()
->with('permissions')
->get()
->pluck('permissions')
->flatten();
// Get direct permissions
$directPermissions = $user->permissions;
// Merge and deduplicate
return $rolePermissions->merge($directPermissions)->unique('id');
}
private function hasResourcePermission(User $user, string $permission, $resource): bool
{
// Check if user owns the resource
if (method_exists($resource, 'user') && $resource->user_id === $user->id) {
$ownResourcePermission = str_replace('.', '.own.', $permission);
if ($this->getUserPermissions($user)->contains('name', $ownResourcePermission)) {
return true;
}
}
return false;
}
private function clearUserPermissionCache(User $user): void
{
Cache::forget("user_permissions_{$user->id}");
}
}
Middleware for Admin Access
// app/Http/Middleware/AdminMiddleware.php
<?php
namespace App\Http\Middleware;
use App\Services\Admin\PermissionManager;
use Closure;
use Illuminate\Http\Request;
class AdminMiddleware
{
public function __construct(
private PermissionManager $permissionManager
) {}
public function handle(Request $request, Closure $next, ...$permissions)
{
$user = auth()->user();
if (!$user) {
return redirect()->route('login');
}
// Check if user has admin access
if (!$this->permissionManager->hasRole($user, 'admin') &&
!$this->permissionManager->hasPermission($user, 'admin.access')) {
abort(403, 'Access denied to admin panel');
}
// Check specific permissions if provided
if (!empty($permissions)) {
foreach ($permissions as $permission) {
if (!$this->permissionManager->hasPermission($user, $permission)) {
abort(403, "Permission denied: {$permission}");
}
}
}
return $next($request);
}
}
Dashboard and Analytics
Advanced Dashboard System
// app/Services/Admin/DashboardService.php
<?php
namespace App\Services\Admin;
use App\Models\User;
use App\Models\Post;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
class DashboardService
{
public function getDashboardData(string $period = '30_days'): array
{
return Cache::remember(
"dashboard_data_{$period}",
600, // 10 minutes
fn() => $this->calculateDashboardData($period)
);
}
private function calculateDashboardData(string $period): array
{
[$startDate, $endDate] = $this->getPeriodDates($period);
return [
'summary_cards' => $this->getSummaryCards($startDate, $endDate),
'charts' => $this->getChartData($startDate, $endDate, $period),
'recent_activities' => $this->getRecentActivities(),
'top_performers' => $this->getTopPerformers($startDate, $endDate),
'alerts' => $this->getSystemAlerts()
];
}
private function getSummaryCards(Carbon $startDate, Carbon $endDate): array
{
$previousStart = $startDate->copy()->sub($endDate->diffInDays($startDate), 'days');
$previousEnd = $startDate->copy()->subDay();
$currentUsers = User::whereBetween('created_at', [$startDate, $endDate])->count();
$previousUsers = User::whereBetween('created_at', [$previousStart, $previousEnd])->count();
$currentRevenue = Order::whereBetween('created_at', [$startDate, $endDate])
->where('status', 'completed')
->sum('total_amount');
$previousRevenue = Order::whereBetween('created_at', [$previousStart, $previousEnd])
->where('status', 'completed')
->sum('total_amount');
return [
[
'title' => 'New Users',
'value' => number_format($currentUsers),
'change' => $this->calculatePercentageChange($currentUsers, $previousUsers),
'icon' => 'users',
'color' => 'primary'
],
[
'title' => 'Revenue',
'value' => '$' . number_format($currentRevenue, 2),
'change' => $this->calculatePercentageChange($currentRevenue, $previousRevenue),
'icon' => 'dollar-sign',
'color' => 'success'
],
[
'title' => 'Orders',
'value' => number_format(Order::whereBetween('created_at', [$startDate, $endDate])->count()),
'change' => $this->calculatePercentageChange(
Order::whereBetween('created_at', [$startDate, $endDate])->count(),
Order::whereBetween('created_at', [$previousStart, $previousEnd])->count()
),
'icon' => 'shopping-cart',
'color' => 'info'
],
[
'title' => 'Conversion Rate',
'value' => '3.2%',
'change' => 0.5,
'icon' => 'trending-up',
'color' => 'warning'
]
];
}
private function getChartData(Carbon $startDate, Carbon $endDate, string $period): array
{
$groupFormat = match ($period) {
'7_days' => '%Y-%m-%d',
'30_days' => '%Y-%m-%d',
'90_days' => '%Y-%m-%d',
'1_year' => '%Y-%m',
default => '%Y-%m-%d'
};
// User registration chart
$userRegistrations = User::selectRaw("DATE_FORMAT(created_at, '{$groupFormat}') as date, COUNT(*) as count")
->whereBetween('created_at', [$startDate, $endDate])
->groupBy('date')
->orderBy('date')
->get();
// Revenue chart
$revenueData = Order::selectRaw("DATE_FORMAT(created_at, '{$groupFormat}') as date, SUM(total_amount) as revenue")
->whereBetween('created_at', [$startDate, $endDate])
->where('status', 'completed')
->groupBy('date')
->orderBy('date')
->get();
return [
'user_registrations' => [
'labels' => $userRegistrations->pluck('date'),
'data' => $userRegistrations->pluck('count')
],
'revenue' => [
'labels' => $revenueData->pluck('date'),
'data' => $revenueData->pluck('revenue')
]
];
}
private function getRecentActivities(): array
{
// This would typically come from an audit log or activity table
return [
[
'type' => 'user_registered',
'message' => 'New user John Doe registered',
'timestamp' => now()->subMinutes(5),
'icon' => 'user-plus',
'color' => 'success'
],
[
'type' => 'order_placed',
'message' => 'Order #1234 placed by Jane Smith',
'timestamp' => now()->subMinutes(15),
'icon' => 'shopping-cart',
'color' => 'info'
]
];
}
private function getTopPerformers(Carbon $startDate, Carbon $endDate): array
{
return [
'top_products' => DB::table('order_items')
->select('products.name', DB::raw('SUM(order_items.quantity) as total_sold'))
->join('products', 'order_items.product_id', '=', 'products.id')
->join('orders', 'order_items.order_id', '=', 'orders.id')
->whereBetween('orders.created_at', [$startDate, $endDate])
->where('orders.status', 'completed')
->groupBy('products.id', 'products.name')
->orderBy('total_sold', 'desc')
->limit(5)
->get(),
'top_customers' => User::select('users.name', DB::raw('COUNT(orders.id) as order_count'))
->join('orders', 'users.id', '=', 'orders.user_id')
->whereBetween('orders.created_at', [$startDate, $endDate])
->groupBy('users.id', 'users.name')
->orderBy('order_count', 'desc')
->limit(5)
->get()
];
}
private function getSystemAlerts(): array
{
$alerts = [];
// Check for low stock products
$lowStockCount = DB::table('products')
->where('stock_quantity', '<', 10)
->count();
if ($lowStockCount > 0) {
$alerts[] = [
'type' => 'warning',
'message' => "{$lowStockCount} products are running low on stock",
'action_url' => route('admin.products.index', ['filter' => 'low_stock'])
];
}
// Check for pending orders
$pendingOrders = Order::where('status', 'pending')
->where('created_at', '<', now()->subHours(24))
->count();
if ($pendingOrders > 0) {
$alerts[] = [
'type' => 'danger',
'message' => "{$pendingOrders} orders have been pending for over 24 hours",
'action_url' => route('admin.orders.index', ['status' => 'pending'])
];
}
return $alerts;
}
private function getPeriodDates(string $period): array
{
return match ($period) {
'7_days' => [now()->subDays(7), now()],
'30_days' => [now()->subDays(30), now()],
'90_days' => [now()->subDays(90), now()],
'1_year' => [now()->subYear(), now()],
default => [now()->subDays(30), now()]
};
}
private function calculatePercentageChange(float $current, float $previous): float
{
if ($previous == 0) {
return $current > 0 ? 100 : 0;
}
return round((($current - $previous) / $previous) * 100, 1);
}
}
Related Posts
For more insights into Laravel application development and advanced patterns, explore these related articles:
- Laravel Service Container: Dependency Injection Deep Dive
- Laravel View Composers: Cleaner Controllers and Views
- Building Multi-tenant Applications with Laravel
Conclusion
Building custom admin panels in Laravel provides unparalleled flexibility and control over your application's administrative interface. While packages like Nova and Filament offer rapid development, custom solutions enable you to create perfectly tailored experiences that align with your specific business requirements.
Key advantages of custom admin panels:
- Complete Control: Full ownership of functionality, design, and user experience
- No Vendor Lock-in: Independence from third-party package updates and limitations
- Perfect Integration: Deep integration with your existing application architecture
- Unlimited Customization: Ability to implement any feature or design requirement
- Performance Optimization: Optimized specifically for your use cases and data patterns
- Security: Complete control over security implementations and access patterns
The foundation we've built provides a solid starting point for creating sophisticated admin interfaces. The modular architecture allows you to extend functionality incrementally while maintaining clean, maintainable code. Whether you're building simple CRUD interfaces or complex analytical dashboards, this approach scales to meet your needs.
Remember that building custom admin panels requires more initial investment than using pre-built solutions, but the long-term benefits of flexibility, maintainability, and perfect fit to your requirements often justify this investment for serious applications.
Add Comment
No comments yet. Be the first to comment!