# Creating Admin Panels This guide covers the complete process of creating admin panels in the Core PHP Framework, including menu registration, modal creation, and authorization integration. ## Overview Admin panels in Core PHP use: - **AdminMenuProvider** - Interface for menu registration - **Livewire Modals** - Full-page components for admin interfaces - **Authorization Props** - Built-in permission checking on components - **HLCRF Layouts** - Composable layout system ## Menu Registration with AdminMenuProvider ### Implementing AdminMenuProvider The `AdminMenuProvider` interface allows modules to contribute navigation items to the admin sidebar. ```php 'onAdminPanel', ]; public function onAdminPanel(AdminPanelBooting $event): void { // Register views and routes $event->views('blog', __DIR__.'/View/Blade'); $event->routes(fn () => require __DIR__.'/Routes/admin.php'); // Register menu provider app(AdminMenuRegistry::class)->register($this); } public function adminMenuItems(): array { return [ // Dashboard item in standalone group [ 'group' => 'dashboard', 'priority' => self::PRIORITY_HIGH, 'item' => fn () => [ 'label' => 'Blog Dashboard', 'icon' => 'newspaper', 'href' => route('admin.blog.dashboard'), 'active' => request()->routeIs('admin.blog.dashboard'), ], ], // Service item with entitlement [ 'group' => 'services', 'priority' => self::PRIORITY_NORMAL, 'entitlement' => 'core.srv.blog', 'item' => fn () => [ 'label' => 'Blog', 'icon' => 'newspaper', 'href' => route('admin.blog.posts'), 'active' => request()->routeIs('admin.blog.*'), 'color' => 'blue', 'badge' => Post::draft()->count() ?: null, 'children' => [ ['label' => 'All Posts', 'href' => route('admin.blog.posts'), 'icon' => 'document-text'], ['label' => 'Categories', 'href' => route('admin.blog.categories'), 'icon' => 'folder'], ['label' => 'Tags', 'href' => route('admin.blog.tags'), 'icon' => 'tag'], ], ], ], // Admin-only item [ 'group' => 'admin', 'priority' => self::PRIORITY_LOW, 'admin' => true, 'item' => fn () => [ 'label' => 'Blog Settings', 'icon' => 'gear', 'href' => route('admin.blog.settings'), 'active' => request()->routeIs('admin.blog.settings'), ], ], ]; } } ``` ### Menu Item Structure Each item in `adminMenuItems()` follows this structure: | Property | Type | Description | |----------|------|-------------| | `group` | string | Menu group: `dashboard`, `workspaces`, `services`, `settings`, `admin` | | `priority` | int | Order within group (use `PRIORITY_*` constants) | | `entitlement` | string | Optional workspace feature code for access | | `permissions` | array | Optional user permission keys required | | `admin` | bool | Requires Hades/admin user | | `item` | Closure | Lazy-evaluated item data | ### Priority Constants ```php use Core\Front\Admin\Contracts\AdminMenuProvider; // Available priority constants AdminMenuProvider::PRIORITY_FIRST // 0-9: System items AdminMenuProvider::PRIORITY_HIGH // 10-19: Primary navigation AdminMenuProvider::PRIORITY_ABOVE_NORMAL // 20-39: Important items AdminMenuProvider::PRIORITY_NORMAL // 40-60: Standard items (default) AdminMenuProvider::PRIORITY_BELOW_NORMAL // 61-79: Less important AdminMenuProvider::PRIORITY_LOW // 80-89: Rarely used AdminMenuProvider::PRIORITY_LAST // 90-99: End items ``` ### Menu Groups | Group | Description | Rendering | |-------|-------------|-----------| | `dashboard` | Primary entry points | Standalone items | | `workspaces` | Workspace management | Grouped dropdown | | `services` | Application services | Standalone items | | `settings` | User/account settings | Grouped dropdown | | `admin` | Platform administration | Grouped dropdown (Hades only) | ### Using MenuItemBuilder For complex menus, use the fluent `MenuItemBuilder`: ```php use Core\Front\Admin\Support\MenuItemBuilder; public function adminMenuItems(): array { return [ MenuItemBuilder::make('Commerce') ->icon('shopping-cart') ->route('admin.commerce.dashboard') ->inServices() ->priority(self::PRIORITY_NORMAL) ->entitlement('core.srv.commerce') ->color('green') ->badge('New', 'green') ->activeOnRoute('admin.commerce.*') ->children([ MenuItemBuilder::child('Products', route('admin.commerce.products')) ->icon('cube'), MenuItemBuilder::child('Orders', route('admin.commerce.orders')) ->icon('receipt') ->badge(fn () => Order::pending()->count()), ['separator' => true], MenuItemBuilder::child('Settings', route('admin.commerce.settings')) ->icon('gear'), ]) ->build(), MenuItemBuilder::make('Analytics') ->icon('chart-line') ->route('admin.analytics.dashboard') ->inServices() ->entitlement('core.srv.analytics') ->adminOnly() // Requires admin user ->build(), ]; } ``` ### Permission Checking The `HasMenuPermissions` trait provides default permission handling: ```php use Core\Front\Admin\Concerns\HasMenuPermissions; class BlogMenuProvider implements AdminMenuProvider { use HasMenuPermissions; // Override for custom global permissions public function menuPermissions(): array { return ['blog.view']; } // Override for custom permission logic public function canViewMenu(?object $user, ?object $workspace): bool { if ($user === null) { return false; } // Custom logic return $user->hasRole('editor') || $user->isHades(); } } ``` ## Creating Livewire Modals Livewire modals are full-page components that provide seamless admin interfaces. ### Basic Modal Structure ```php 'required|string|max:255', 'content' => 'required|string', 'status' => 'required|in:draft,published,archived', ]; public function mount(?Post $post = null): void { $this->post = $post; if ($post) { $this->title = $post->title; $this->content = $post->content; $this->status = $post->status; } } public function save(): void { $validated = $this->validate(); if ($this->post) { $this->post->update($validated); $message = 'Post updated successfully.'; } else { Post::create($validated); $message = 'Post created successfully.'; } session()->flash('success', $message); $this->redirect(route('admin.blog.posts')); } public function render(): View { return view('blog::admin.post-editor'); } } ``` ### Modal View with HLCRF ```blade {{-- resources/views/admin/post-editor.blade.php --}}

{{ $post ? 'Edit Post' : 'Create Post' }}

Draft Published Archived
{{ $post ? 'Update' : 'Create' }} Post Cancel

Publishing Tips

  • Use descriptive titles
  • Save as draft first
  • Preview before publishing
``` ### Modal with Authorization ```php authorize('update', $post); $this->post = $post; // ... load data } public function save(): void { // Re-authorize on save $this->authorize('update', $this->post); $this->post->update([...]); } public function publish(): void { // Different authorization for publish $this->authorize('publish', $this->post); $this->post->update(['status' => 'published']); } public function delete(): void { $this->authorize('delete', $this->post); $this->post->delete(); $this->redirect(route('admin.blog.posts')); } } ``` ### Modal with File Uploads ```php 'required|image|max:5120', // 5MB max 'altText' => 'required|string|max:255', ]; public function upload(): void { $this->validate(); $path = $this->image->store('media', 'public'); Media::create([ 'path' => $path, 'alt_text' => $this->altText, 'mime_type' => $this->image->getMimeType(), ]); $this->dispatch('media-uploaded'); $this->reset(['image', 'altText']); } } ``` ## Authorization Integration ### Form Component Authorization Props All form components support authorization via `canGate` and `canResource` props: ```blade {{-- Button disabled if user cannot update post --}} Save Changes {{-- Input disabled if user cannot update --}} {{-- Textarea with authorization --}} {{-- Select with authorization --}} Draft Published {{-- Toggle with authorization --}} ``` ### Blade Conditional Rendering ```blade {{-- Show only if user can create --}} @can('create', App\Models\Post::class) New Post @endcan {{-- Show if user can edit OR delete --}} @canany(['update', 'delete'], $post)
@can('update', $post) Edit @endcan @can('delete', $post) @endcan
@endcanany {{-- Show message if cannot edit --}} @cannot('update', $post)

You cannot edit this post.

@endcannot ``` ### Creating Policies ```php isHades()) { return true; } // Enforce workspace isolation if ($model instanceof Post && $user->workspace_id !== $model->workspace_id) { return false; } return null; // Continue to specific method } public function viewAny(User $user): bool { return $user->hasPermission('posts.view'); } public function view(User $user, Post $post): bool { return $user->hasPermission('posts.view'); } public function create(User $user): bool { return $user->hasPermission('posts.create'); } public function update(User $user, Post $post): bool { return $user->hasPermission('posts.edit') || $user->id === $post->author_id; } public function delete(User $user, Post $post): bool { return $user->hasRole('admin') || ($user->hasPermission('posts.delete') && $user->id === $post->author_id); } public function publish(User $user, Post $post): bool { return $user->hasPermission('posts.publish') && $post->status !== 'archived'; } } ``` ## Complete Module Example Here is a complete example of an admin module with menus, modals, and authorization. ### Directory Structure ``` Mod/Blog/ ├── Boot.php ├── Models/ │ └── Post.php ├── Policies/ │ └── PostPolicy.php ├── View/ │ ├── Blade/ │ │ └── admin/ │ │ ├── posts-list.blade.php │ │ └── post-editor.blade.php │ └── Modal/ │ └── Admin/ │ ├── PostsList.php │ └── PostEditor.php └── Routes/ └── admin.php ``` ### Boot.php ```php 'onAdminPanel', ]; public function boot(): void { // Register policy Gate::policy(Post::class, PostPolicy::class); } public function onAdminPanel(AdminPanelBooting $event): void { // Views $event->views('blog', __DIR__.'/View/Blade'); // Routes $event->routes(fn () => require __DIR__.'/Routes/admin.php'); // Menu app(AdminMenuRegistry::class)->register($this); // Livewire components $event->livewire('blog.admin.posts-list', View\Modal\Admin\PostsList::class); $event->livewire('blog.admin.post-editor', View\Modal\Admin\PostEditor::class); } public function adminMenuItems(): array { return [ [ 'group' => 'services', 'priority' => self::PRIORITY_NORMAL, 'entitlement' => 'core.srv.blog', 'permissions' => ['posts.view'], 'item' => fn () => [ 'label' => 'Blog', 'icon' => 'newspaper', 'href' => route('admin.blog.posts'), 'active' => request()->routeIs('admin.blog.*'), 'color' => 'blue', 'badge' => $this->getDraftCount(), 'children' => [ [ 'label' => 'All Posts', 'href' => route('admin.blog.posts'), 'icon' => 'document-text', 'active' => request()->routeIs('admin.blog.posts'), ], [ 'label' => 'Create Post', 'href' => route('admin.blog.posts.create'), 'icon' => 'plus', 'active' => request()->routeIs('admin.blog.posts.create'), ], ], ], ], ]; } protected function getDraftCount(): ?int { $count = Post::draft()->count(); return $count > 0 ? $count : null; } } ``` ### Routes/admin.php ```php prefix('admin/blog') ->name('admin.blog.') ->group(function () { Route::get('/posts', PostsList::class)->name('posts'); Route::get('/posts/create', PostEditor::class)->name('posts.create'); Route::get('/posts/{post}/edit', PostEditor::class)->name('posts.edit'); }); ``` ### View/Modal/Admin/PostsList.php ```php resetPage(); } #[Computed] public function posts() { return Post::query() ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%")) ->when($this->status, fn ($q) => $q->where('status', $this->status)) ->orderByDesc('created_at') ->paginate(20); } public function delete(int $postId): void { $post = Post::findOrFail($postId); $this->authorize('delete', $post); $post->delete(); session()->flash('success', 'Post deleted.'); } public function render(): View { return view('blog::admin.posts-list'); } } ``` ### View/Blade/admin/posts-list.blade.php ```blade

Blog Posts

@can('create', \Mod\Blog\Models\Post::class) New Post @endcan
{{-- Filters --}}
All Statuses Draft Published
{{-- Posts table --}}
@forelse($this->posts as $post) @empty @endforelse
Title Status Date Actions
{{ $post->title }} {{ ucfirst($post->status) }} {{ $post->created_at->format('M d, Y') }} @can('update', $post) Edit @endcan @can('delete', $post) @endcan
No posts found.
{{-- Pagination --}}
{{ $this->posts->links() }}
``` ## Best Practices ### 1. Always Use Entitlements for Services ```php // Menu item requires workspace entitlement [ 'group' => 'services', 'entitlement' => 'core.srv.blog', // Required 'item' => fn () => [...], ] ``` ### 2. Authorize Early in Modals ```php public function mount(Post $post): void { $this->authorize('update', $post); // Fail fast $this->post = $post; } ``` ### 3. Use Form Component Authorization Props ```blade {{-- Declarative authorization --}} Save {{-- Not manual checks --}} @if(auth()->user()->can('update', $post)) @endif ``` ### 4. Keep Menu Items Lazy ```php // Item closure is only evaluated when rendered 'item' => fn () => [ 'label' => 'Posts', 'badge' => Post::draft()->count(), // Computed at render time ], ``` ### 5. Use HLCRF for Consistent Layouts ```blade {{-- Always use HLCRF for admin views --}} ... ... ``` ## Learn More - [Admin Menus](/packages/admin/menus) - [Livewire Modals](/packages/admin/modals) - [Form Components](/packages/admin/forms) - [Authorization](/packages/admin/authorization) - [HLCRF Layouts](/packages/admin/hlcrf-deep-dive)