neophp/admin-package

There is no license information available for the latest version (v3.0.0) of this package.

Admin panel skeleton for NeoPHP: login, sidebar, dashboard, fully overridable by the host project

Maintainers

Package info

github.com/NeoPHP-Dev/neo-admin-package

pkg:composer/neophp/admin-package

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.0 2026-08-06 16:20 UTC

This package is auto-updated.

Last update: 2026-08-06 20:37:26 UTC


README

A self-contained admin panel skeleton for NeoPHP: its own login system, sidebar, and dashboard — fully overridable by the host project. The package owns the layout (sidebar, topbar) and authentication; the project owns everything inside {% block content %} for every page it adds to the sidebar.

Structure

admin-package/
├── composer.json
├── README.md
├── src/
│   ├── NeoAdminPackage.php
│   ├── Controllers/
│   │   ├── AuthController.php
│   │   └── DashboardController.php
│   ├── Middleware/
│   │   └── AdminAuthMiddleware.php
│   ├── Service/
│   │   ├── AdminAuthManager.php
│   │   └── SidebarResolver.php
│   ├── Extension/
│   │   ├── AdminControllerExtension.php   # $this->adminConfig() / $this->adminSidebar() in PHP
│   │   └── AdminViewExtension.php          # adminConfig() / adminSidebar() in Twig
│   ├── Profiler/
│   │   └── AdminAuthCollector.php          # "Admin" tab in the dev toolbar
│   ├── Assets/
│   │   ├── css/
│   │   │   ├── base.css       # reset, body, main layout skeleton
│   │   │   ├── sidebar.css     # sidebar, groups, dropdowns
│   │   │   ├── header.css       # topbar
│   │   │   └── content.css       # content area, login form
│   │   └── js/
│   │       └── admin.js           # sidebar group toggle
│   └── Templates/
│       ├── layouts/
│       │   └── admin_layout.html.twig
│       ├── partials/
│       │   ├── sidebar.html.twig
│       │   └── macros/
│       │       └── Icon.macro.html.twig
│       └── pages/
│           ├── login.html.twig
│           └── dashboard.html.twig
├── config/
│   └── admin-system.config.php
└── database/
    ├── Entity/
    │   ├── AdminUser.php
    │   └── AdminRole.php
    ├── Repository/
    │   └── AdminUserRepository.php
    └── Migrations/
        ├── MigrationVersion_NeoAdmin_1.php   # neo_admin_roles table
        └── MigrationVersion_NeoAdmin_2.php   # neo_admin_users table

The package's PHP namespace is Vendor\NeoPHP\AdminPackage, and its Twig namespace (used for @NeoAdmin/... template paths) is NeoAdmin — two different things, both referenced throughout this document.

This package has its own authentication system

Unlike a typical NeoPHP feature, this package does not rely on the host project's Config/auth.config.php / AuthManager. It ships its own AdminAuthManager, its own session key, and its own database tables:

  • neo_admin_roles — a simple role table, seeded with ROLE_ADMIN by its migration
  • neo_admin_users — email, hashed password, name, role_id

This is intentional: a project may already have its own user system for regular site visitors (customers, members, etc.), completely separate from who is allowed into the admin panel. There is no shared table, no shared session, and no dependency on the project's auth configuration.

If you want the admin panel to authenticate against your project's existing users instead, this package is not built for that — you would need to fork or extend AdminAuthManager yourself.

Installation

php bin/neo package:require neophp/admin-package --project=MyProject

Then register the package in the project's Config/app.config.php:

return [
    // ...
    'packages' => [
        \Vendor\NeoPHP\AdminPackage\NeoAdminPackage::class,
    ],
];

Run the package's migrations to create its tables:

php bin/neo database:migration:migrate --project=MyProject

This creates neo_admin_roles (with ROLE_ADMIN seeded) and neo_admin_users (empty — see below to create your first admin).

Creating your first admin user

There is currently no CLI command for this — insert directly:

<?php
require "vendor/autoload.php";
$pm = new Neo\Core\Security\Auth\PasswordManager();
echo $pm->hash("your-password-here");
INSERT INTO neo_admin_users (email, password, name, role_id, created_at)
VALUES ('admin@example.com', '<hash from above>', 'Admin', 1, NOW());

role_id = 1 corresponds to ROLE_ADMIN, seeded by the first migration.

Configuration

config/admin-system.config.php is copied once to Config/Packages/NeoAdmin/admin-system.config.php in the target project:

<?php

declare(strict_types=1);

return [
    'title' => 'My Project Admin',
    'description' => '',

    'route_prefix' => '/admin',

    'auth' => [
        'required_role' => 'ROLE_ADMIN',
        'redirect_after_login' => 'admin.panel.index',
    ],

    'sidebar' => [
        'dashboard' => [
            'controller' => \Vendor\NeoPHP\AdminPackage\Controllers\DashboardController::class,
            'icon' => 'layout-dashboard',
            'title' => 'Dashboard',
        ],

        'users' => [
            'controller' => \Neo\Src\MyProject\App\Controllers\NeoAdmin\UsersController::class,
            'icon' => 'users',
            'title' => 'Users',
        ],

        'Settings' => [
            'general' => [
                'controller' => \Neo\Src\MyProject\App\Controllers\NeoAdmin\SettingsGeneralController::class,
                'icon' => 'settings',
                'title' => 'General',
            ],
            'theme' => [
                'controller' => \Neo\Src\MyProject\App\Controllers\NeoAdmin\SettingsThemeController::class,
                'icon' => 'settings',
                'title' => 'Theme',
            ],
        ],
    ],
];

Sidebar entries: links vs. groups

Each top-level key in sidebar is either a link or a group, determined automatically by SidebarResolver:

  • Link — has a controller key. Rendered as a single clickable item.
  • Group — has no controller key at its own level, only nested entries (each of which must itself have a controller). Rendered as a collapsible dropdown containing its children.
'Settings' => [               // group: no 'controller' key here
    'general' => [ 'controller' => ..., 'icon' => ..., 'title' => ... ],
    'theme'   => [ 'controller' => ..., 'icon' => ..., 'title' => ... ],
],

A child entry with 'controller' => null is not silently skipped — it throws a clear error (does not reference a valid controller class) so a misconfigured group fails loudly rather than rendering an empty dropdown.

Every link-type entry (top-level or nested inside a group) must reference a controller that:

  • Declares a #[MainRoute(...)] attribute on the class
  • Has a public method named exactly index, carrying a #[Route(name: 'index', ...)] attribute

The link's URL is resolved automatically from these two attributes ({mainRoute.name}.index) — there is no separate route field to keep in sync manually.

auth.redirect_after_login

The route name to redirect to after a successful login. Defaults to the package's own admin.panel.index. Change this if you override the default dashboard with your own controller under a different route name (see below).

Important: 'requiredRole' passed to #[Middleware(..., params: [...])] on each of your own controllers is a static value — it is not re-read from admin-system.config.php at runtime. If you change auth.required_role in the config, update the params on every protected controller (including any dashboard you write yourself) to match.

Adding your own admin pages

Create a controller anywhere in your project (conventionally under App/Controllers/NeoAdmin/), following the index() convention, and protect it with the package's middleware:

<?php

declare(strict_types=1);

namespace Neo\Src\MyProject\App\Controllers\NeoAdmin;

use Neo\Core\Controller\AbstractController;
use Neo\Core\Http\Response\Types\Response;
use Neo\Core\Routing\Attribute\MainRoute;
use Neo\Core\Routing\Attribute\Route;
use Neo\Core\Security\Middleware\Attribute\Middleware;
use Vendor\NeoPHP\AdminPackage\Middleware\AdminAuthMiddleware;

#[MainRoute(path: '/admin/users', name: 'admin.users')]
#[Middleware(
    use: AdminAuthMiddleware::class,
    onError: 'block',
    params: ['requiredRole' => 'ROLE_ADMIN'],
)]
final class UsersController extends AbstractController
{
    #[Route(path: '/', name: 'index', methods: ['GET'])]
    public function index(): Response
    {
        return $this->render('pages/admin/users.html.twig');
    }
}

Then extend the package's layout in your view — only write what goes inside the content area, the sidebar and topbar are never duplicated. Note the Twig namespace @NeoAdmin, distinct from the package's PHP namespace:

{# src/MyProject/Templates/pages/admin/users.html.twig #}
{% extends '@NeoAdmin/layouts/admin_layout.html.twig' %}

{% block content %}
    <h1>Users</h1>
    {# your content here #}
{% endblock %}

Register the controller in admin-system.config.php's sidebar array (as a top-level link, or nested inside a group) to have it appear in the menu automatically. Clear the router cache after adding a new controller in dev mode if it doesn't show up immediately.

Adding your own CSS or JS to an admin page

admin_layout.html.twig exposes two extra blocks, on top of content, that your own pages can fill without ever touching the package:

{# src/MyProject/Templates/pages/admin/users.html.twig #}
{% extends '@NeoAdmin/layouts/admin_layout.html.twig' %}

{% block stylesheets %}
    <link rel="stylesheet" href="{{ asset('css/admin-custom.css') }}">
{% endblock %}

{% block content %}
    <h1>Users</h1>
{% endblock %}

{% block javascripts %}
    <script src="{{ asset('js/admin-custom.js') }}"></script>
{% endblock %}

Use your project's own asset() pipeline for this CSS/JS (compiled and versioned the normal way) — this is different from how the package serves its own assets (see below), and is the right place for anything specific to a page you added yourself.

Overriding the default dashboard

The package's own DashboardController renders @NeoAdmin/pages/dashboard.html.twig at /admin/panel by default.

To replace it, create your own controller — using a different URL path than /admin/panel, since NeoPHP's router does not allow two controllers to register the same [method, path] pair (it will throw a Route conflict error in dev mode):

#[MainRoute(path: '/admin/dashboard', name: 'admin.dashboard.custom')]
#[Middleware(
    use: AdminAuthMiddleware::class,
    onError: 'block',
    params: ['requiredRole' => 'ROLE_ADMIN'],
)]
final class DashboardController extends AbstractController
{
    #[Route(path: '/', name: 'index', methods: ['GET'])]
    public function index(): Response
    {
        return $this->render('pages/admin/dashboard.html.twig');
    }
}

Then update both the sidebar entry and the post-login redirect:

'auth' => [
    'redirect_after_login' => 'admin.dashboard.custom.index',
],

'sidebar' => [
    'dashboard' => [
        'controller' => \Neo\Src\MyProject\App\Controllers\NeoAdmin\DashboardController::class,
        'icon' => 'layout-dashboard',
        'title' => 'Dashboard',
    ],
],

Known limitation: the package's own /admin/panel route remains registered and reachable even after you stop referencing it from the sidebar — the router scans every controller the package ships, regardless of whether your config points elsewhere. It stays behind the same AdminAuthMiddleware, so this is not a security gap, just an orphaned route serving the package's generic dashboard view if visited directly.

Available template helpers

Two Twig functions are available in every template, via AdminViewExtension:

Function Returns
adminConfig() The full contents of admin-system.config.php
adminSidebar() Resolved sidebar entries — see Sidebar entries: links vs. groups for the shape of each item

The same two methods are also available in PHP inside any controller, via AdminControllerExtension:

$this->adminConfig();
$this->adminSidebar();

Icons

Sidebar and layout icons are inline SVGs (Lucide-style paths embedded directly in the package — no external font or CDN dependency), rendered via a macro:

{% import '@NeoAdmin/partials/macros/Icon.macro.html.twig' as Icon %}
{{ Icon.render('users', 18) }}

Currently available names: layout-dashboard, users, settings, log-out, external-link, chevron-down. Unknown names fall back to a plain circle. Add more by extending Icon.macro.html.twig directly.

Serving package assets

The package's own CSS and JS are served through NeoPHP's generic package asset route, not copied into the project:

/packages-assets/NeoAdmin/css/base.css
/packages-assets/NeoAdmin/css/sidebar.css
/packages-assets/NeoAdmin/css/header.css
/packages-assets/NeoAdmin/css/content.css
/packages-assets/NeoAdmin/js/admin.js

The stylesheet is intentionally split into four files instead of one, so that a heavily customized project (or a future version of this README) can reference only what it actually needs — for example, login.html.twig only loads base.css and content.css, since the login page has no sidebar or topbar.

Already wired into admin_layout.html.twig and login.html.twig — no action needed. admin.js currently only handles toggling collapsible sidebar groups open/closed.

Dev toolbar integration

If NeoPHP's profiler is enabled (environment: dev), an Admin tab appears in the dev toolbar showing whether an admin session is currently active, and which email/role is logged in. This reflects AdminAuthManager's own session — it is intentionally separate from whatever the toolbar shows for the project's own AuthManager, since the two systems don't share state.

Middleware

AdminAuthMiddleware checks, in order:

  1. AdminAuthManager::check() — is an admin session active at all
  2. AdminAuthManager::hasRole($requiredRole) — exact role match (no role hierarchy — this package does not depend on any role-hierarchy add-on)

Apply it to any controller you want protected under /admin, passing the required role via the #[Middleware] attribute's params.

Migrations

Both migrations are run automatically alongside the project's own via database:migration:migrate, following NeoPHP's package migration convention (MigrationVersion_NeoAdmin_{n}). They are never copied into the project — read directly from the package.

License

MIT