Navigation

Laravel

Building Admin Panels in Laravel: Beyond Nova and Filament

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.

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

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:

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.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel