neophp-package / neo-admin
Modular administration area for NeoPHP
Package info
github.com/NeoPHP-Framework/neo-package-admin
Type:neophp-package
pkg:composer/neophp-package/neo-admin
Requires
- php: >=8.2
- neophp/framework: ^2.1
- twig/twig: ^3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-11 07:38:33 UTC
README
Modular administration area for NeoPHP: a Tailwind layout, a menu described in YAML, the CRUD of the ORM entities and a dashboard with widgets.
Installation
php bin/neo neophp:package:install neophp-package/neo-admin
The command copies config/packages/admin/admin.yaml and imports the routes (resource: '@admin' in config/routes.yaml). The administration is served on /admin and requires ROLE_ADMIN.
Requirements: PHP 8.2, neophp/framework 2.1 with the ORM, Security, Form and Translation modules, Twig.
Configuration
config/packages/admin/admin.yaml:
title: Administration role: ROLE_ADMIN per_page: 25 entity_namespace: App\Entity menu: - { type: dashboard } - { type: section, label: Content } - { type: entity, entity: BlogPost, icon: file } - { type: entity, entity: Category, icon: tag } - type: submenu label: Users icon: users role: ROLE_SUPER_ADMIN children: - { type: entity, entity: User } - { type: entity, entity: Group } - { type: link, label: Website, url: '/', icon: external, target: _blank } dashboard: widgets: - { type: count, entity: BlogPost, icon: file } - { type: count, entity: User, label: Members, icon: users } - { type: latest, entity: BlogPost, limit: 5, sort: { createdAt: desc } } - type: links label: Shortcuts links: - { label: New article, route: admin_crud_new, parameters: { entity: blog-post }, icon: plus }
Menu items
| Type | Keys |
|---|---|
dashboard |
label (default Dashboard) |
entity |
entity (required): the short name of an entity of entity_namespace |
submenu |
label, children (required) |
link |
label, url or route + parameters, target |
section |
label (required): a title between groups of items |
Every item accepts icon, role and badge. An item whose role is not granted is hidden, as is an empty submenu or section.
Icons: home, users, user, file, folder, tag, list, settings, link, external, chart, shield, mail, box, plus, edit, trash, eye, search, logout, menu, x, chevron-down, chevron-right, sun, moon, circle.
Dashboard widgets
| Type | Keys |
|---|---|
count |
entity: the number of records, linked to the list |
latest |
entity, limit (5), sort ({ field: desc }): the last records |
links |
label, links: a list of links (same keys as a link menu item) |
custom |
widget: a class implementing WidgetInterface (see Custom widgets) |
Every widget accepts label, icon, role and size (small or large). Without widgets, the dashboard shows a card per entity.
Entities
Each entity of the menu gets a list, a show page and forms guessed from its ORM mapping. To customize them, generate its file:
php bin/neo make:admin:entity BlogPost
config/packages/admin/entities/BlogPost.yaml:
label: Articles icon: file role: ROLE_EDITOR actions: [index, show, new, edit, delete] default: index roles: { delete: ROLE_SUPER_ADMIN } index: fields: [title, status, author, createdAt] search: [title, content] sort: { createdAt: desc } per_page: 20 show: fields: title: ~ content: { label: Body } createdAt: { format: 'd/m/Y H:i' } form: fields: title: { help: 'Displayed on the home page.' } content: { type: textarea, attr: { rows: 12 } } status: { expanded: true } author: ~
| Key | Description |
|---|---|
class |
The class when it is not in entity_namespace |
label, icon |
The name and the icon of the entity |
role |
The role required for the entity, on top of the role of the administration |
actions |
The enabled pages among index, show, new, edit and delete |
default |
The page of /admin/<entity>: index or new |
roles |
A role per action |
index.fields, show.fields, form.fields |
The fields of each page, as a list or a mapping with options |
index.search |
The fields searched by the search box (the text fields by default) |
index.sort |
The default order |
index.per_page |
The number of rows per page |
Options of a field: label, format (dates), truncate (list), sortable, and for the forms type (text, textarea, email, url, tel, password, integer, number, checkbox, choice, enum, entity, date, datetime, time, color, hidden or a form type class), help, required, disabled, attr, placeholder, choices, multiple, expanded and options (any other option of the form type).
A field which is not mapped by the ORM can be displayed when the entity has a property or a getter with its name.
Routes
| Name | Path |
|---|---|
admin_dashboard |
GET /admin |
admin_crud |
GET /admin/{entity} |
admin_crud_index |
GET /admin/{entity}/list |
admin_crud_new |
GET|POST /admin/{entity}/new |
admin_crud_show |
GET /admin/{entity}/{id} |
admin_crud_edit |
GET|POST /admin/{entity}/{id}/edit |
admin_crud_delete |
POST /admin/{entity}/{id}/delete |
{entity} is the slug of the entity: BlogPost gives blog-post.
Pages
Build the pages of the administration in the controller: extend NeoPHP\NeoAdmin\Contract\AbstractAdminController, describe the page with $this->page() and render it. The layout, the components and the styles come from the package: no template is needed.
namespace App\Controller\Admin; use App\Service\Analytics; use NeoPHP\Component\Http\Request\Request; use NeoPHP\Component\Http\Response\Response; use NeoPHP\Component\Routing\Attribute\Route; use NeoPHP\NeoAdmin\Contract\AbstractAdminController; use NeoPHP\NeoAdmin\Page\Action; use NeoPHP\NeoAdmin\Page\Block\Grid; use NeoPHP\NeoAdmin\Page\Column; class AnalyticsController extends AbstractAdminController { #[Route('/admin/analytics', name: 'admin_analytics', methods: ['GET'])] public function index(Request $request, Analytics $analytics): Response { $this->denyUnlessAdmin(); $days = in_array($days = $request->query->getInt('days', 7), [7, 30, 90], true) ? $days : 7; $summary = $analytics->summary($days); return $this->page('Analytics') ->description(sprintf('Page views of the last %d days.', $days)) ->action(Action::link('Open the site', '/', 'external')->blank()) ->tabs('days', [7 => '7 days', 30 => '30 days', 90 => '90 days'], $days) ->grid(3, fn (Grid $grid) => $grid ->stat('Page views', $summary['views'], 'eye', trend: 8.2) ->stat('Unique visitors', $summary['visitors'], 'users') ->stat('Average response', $summary['avg_ms'] . ' ms', 'chart')) ->chart('Views per day', $analytics->perDay($days), ['views' => 'Views', 'visitors' => 'Visitors']) ->grid(2, fn (Grid $grid) => $grid ->table('Top pages', $analytics->topPaths($days), [ 'path' => Column::code('path', 'Page')->url(fn (array $row) => $row['path']), 'views' => 'Views', ], icon: 'file', empty: 'No page view yet.') ->table('Not found (404)', $analytics->notFound($days), ['path' => 'Page', 'views' => 'Views'], icon: 'search')) ->render(); } }
The routes of the application are matched before the ones of the package, so /admin/analytics is not taken for an entity. Add the page to the menu with - { type: link, label: Analytics, route: admin_analytics, icon: chart }.
Page
| Method | Description |
|---|---|
heading(string) |
A title above the content (the page title is shown in the top bar) |
description(string) |
A line of text under the heading |
action(Action) |
A button at the top right, repeatable |
render(int $status = 200) |
Renders the page in the layout of the administration |
Blocks
Every block method returns the page (or the grid / card), so the calls chain. A block also accepts span (1 to 4) to take several columns of a grid.
| Method | Description |
|---|---|
grid(int $columns, callable) |
A responsive grid of 1 to 4 columns: one column on phones, two on tablets |
card(string $title, callable, ?string $icon) |
A card holding other blocks |
stat(label, value, ?icon, ?trend, ?help, ?url) |
A number with an icon, an optional trend in percent (8.2 or -3.1) and a "View all" link |
table(?title, rows, columns, ?icon, ?empty, ?actions) |
A table of arrays or objects; actions is a callable returning the Action list of a row |
chart(title, rows, series, ?icon, ?empty) |
A bar chart; rows are keyed by label or date (Y-m-d), series maps up to two keys to their label |
links(title, links, ?icon, ?empty, ?more) |
A list of links: label, url, icon, meta, target |
tile(label, url, ?icon) |
A link card |
tabs(parameter, options, current) |
Links changing a query parameter, like a period selector |
form(FormInterface, submit, ?cancel, ?title) |
A form of the framework, rendered with the theme of the administration |
alert(message, type) |
An info, success, warning or error message |
text(string) |
A paragraph |
empty(title, ?text, ?icon) |
An empty state |
template(name, data) |
A Twig template of the project, for anything else |
add(BlockInterface) |
A block object, for example (new Table(...))->paginate($total, $page, 20) |
Columns
A column is a label ('views' => 'Views', its type is guessed from the values) or a NeoPHP\NeoAdmin\Page\Column:
| Column | Rendering |
|---|---|
Column::text(key, label)->truncate(60) |
Text |
Column::number(key, label, decimals) |
A number formatted for the locale, aligned to the right |
Column::date(key, label, 'd/m/Y') |
A date |
Column::bool(key, label) |
A Yes / No badge |
Column::badge(key, label, ['draft' => 'warning']) |
A badge: primary, success, warning, danger, info |
Column::code(key, label) |
Monospace text |
Column::link(key, label, fn ($row) => $url) |
A link; ->url() and ->blank() work on every column |
Actions
Action::link('Edit', $url, 'edit')->primary(); Action::link('Open', $url, 'external')->blank(); Action::post('Delete', $url, 'delete_' . $id, 'Delete this item?', 'trash')->danger();
A POST action sends a CSRF token for the given id: check it with $this->isCsrfTokenValid('delete_' . $id, $request->request->get('_token')).
Forms
$form = $this->createForm(SettingsType::class, $settings); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->addFlash('success', 'Saved.'); return $this->redirectToRoute('admin_settings'); } return $this->page('Settings')->form($form, 'Save', $this->generateUrl('admin_dashboard'))->render($form->isSubmitted() ? 422 : 200);
Custom widgets
A widget of the dashboard can be any block, built by a class of the project:
namespace App\Admin\Widget; use App\Service\Analytics; use NeoPHP\NeoAdmin\Page\Block\Stat; use NeoPHP\NeoAdmin\Page\Contract\BlockInterface; use NeoPHP\NeoAdmin\Page\Contract\WidgetInterface; class TrafficWidget implements WidgetInterface { public function __construct(protected Analytics $analytics) { } public function build(): BlockInterface { return new Stat('Views today', $this->analytics->summary(1)['views'], 'eye'); } }
dashboard: widgets: - { type: custom, widget: App\Admin\Widget\TrafficWidget }
The class is created by the container, so its constructor receives the services it needs.
Templates and translations
Every template can be overridden in templates/packages/admin/ (layout.html.twig, page.html.twig, components/table.html.twig, crud/index.html.twig...). The texts are in the admin domain (English and French): override them in translations/admin.<locale>.yaml.
Styles
The package ships its compiled stylesheet, assets/css/admin.css: the projects build nothing.
Theme
The colors are CSS variables. Change them in a stylesheet of the project, declared in config/packages/admin/admin.yaml and loaded after the one of the package:
assets: stylesheets: [css/admin.css] javascripts: []
:root { --na-primary: #e11d48; --na-primary-hover: #f43f5e; --na-primary-soft: #fff1f2; --na-primary-fg: #be123c; } .dark { --na-primary: #f43f5e; --na-primary-soft: rgb(244 63 94 / .12); --na-primary-fg: #fda4af; }
Other variables: --na-font, --na-radius, --na-bg, --na-surface, --na-border, --na-text, --na-text-muted, --na-chart-1, --na-chart-2, and the success, warning, danger and info colors.
Components
The templates of a project can use the classes of the package: na-card, na-card-header, na-card-title, na-card-body, na-stat, na-table, na-btn (na-btn-primary, na-btn-danger, na-btn-ghost), na-badge (na-badge-success...), na-alert, na-tabs, na-grid (na-grid-2 to na-grid-4), na-empty, na-input, na-link, na-muted... A set of Tailwind utilities is included too (grid columns, flex, gaps, margins, paddings, text sizes).
Development of the package
assets/css/admin.css is compiled from resources/tailwind/admin.css with the Tailwind CSS standalone CLI 4, after a change of the styles or of the templates:
tailwindcss -i resources/tailwind/admin.css -o assets/css/admin.css --minify
License
MIT