Skip to content

marko/admin-panel

Server-rendered admin panel UI --- provides login, dashboard, and permission-filtered sidebar navigation out of the box. Admin Panel delivers the server-rendered HTML interface for the admin area. It includes a login/logout flow, a dashboard that displays registered admin sections, and a menu builder that filters navigation items by the current user’s permissions. Templates are rendered via ViewInterface using the admin-panel:: namespace. All routes are protected by AdminAuthMiddleware.

Terminal window
composer require marko/admin-panel

Requires marko/admin, marko/admin-auth, marko/security, a view driver, and a template sibling package. Install marko/admin-panel-latte with marko/view-latte, or marko/admin-panel-twig with marko/view-twig.

The panel registers these routes automatically:

MethodPathDescription
GET/admin/loginLogin form
POST/admin/loginAuthenticate (requires valid CSRF token)
POST/admin/logoutLogout (requires valid CSRF token)
GET/adminDashboard (requires auth)

The login and logout POST routes are protected by CsrfMiddleware from marko/security. The login form receives a CSRF token via the csrfToken template variable; the template must include it as a hidden field named _token. Requests without a valid token are rejected before authentication logic runs.

AdminMenuBuilderInterface produces a navigation structure filtered by the user’s permissions:

use Marko\AdminPanel\Menu\AdminMenuBuilderInterface;
use Marko\AdminAuth\Entity\AdminUserInterface;
readonly class LayoutHelper
{
public function __construct(
private AdminMenuBuilderInterface $adminMenuBuilder,
) {}
public function getSidebar(
AdminUserInterface $user,
string $currentPath,
): array {
return $this->adminMenuBuilder->build($user, $currentPath);
// Returns: [
// ['section' => ..., 'items' => [...], 'active' => true, 'activeItemId' => 'products'],
// ['section' => ..., 'items' => [...], 'active' => false, 'activeItemId' => null],
// ]
}
}

Each entry includes:

  • section --- the AdminSectionInterface instance
  • items --- filtered and sorted MenuItemInterface array
  • active --- whether this section contains the current page
  • activeItemId --- the ID of the active menu item, or null

Get sections the user can access for the dashboard overview:

$sections = $this->adminMenuBuilder->buildDashboardSections($user);
// Returns AdminSectionInterface[] with at least one accessible menu item

Admin panel templates live under the admin-panel:: namespace. Override them by placing templates in your module’s resources/views/ directory with the same path:

mymodule/
resources/
views/
admin-panel/
dashboard/
index.latte # Overrides the default dashboard

Replace the menu builder via Preferences to customize navigation behavior:

use Marko\Core\Attributes\Preference;
use Marko\AdminPanel\Menu\AdminMenuBuilder;
use Marko\AdminAuth\Entity\AdminUserInterface;
#[Preference(replaces: AdminMenuBuilder::class)]
class CustomMenuBuilder extends AdminMenuBuilder
{
public function build(
AdminUserInterface $user,
string $currentPath = '',
): array {
$menu = parent::build($user, $currentPath);
// Add custom sections or reorder
return $menu;
}
}
interface AdminMenuBuilderInterface
{
public function build(AdminUserInterface $user, string $currentPath = ''): array;
public function buildDashboardSections(AdminUserInterface $user): array;
}
use Marko\Routing\Attributes\Get;
use Marko\Routing\Attributes\Middleware;
use Marko\Routing\Attributes\Post;
use Marko\Security\Middleware\CsrfMiddleware;
#[Get(path: '/admin/login')] // showLoginForm
#[Post(path: '/admin/login')]
#[Middleware(CsrfMiddleware::class)] // authenticate — CSRF required
#[Post(path: '/admin/logout')]
#[Middleware(CsrfMiddleware::class)] // logout — CSRF required
use Marko\Routing\Attributes\Get;
use Marko\Routing\Attributes\Middleware;
use Marko\AdminAuth\Middleware\AdminAuthMiddleware;
#[Get(path: '/admin')] // index
#[Middleware(AdminAuthMiddleware::class)] // requires auth
interface AdminPanelConfigInterface
{
public function getPageTitle(): string;
public function getItemsPerPage(): int;
}