Administrative pages, resources, access control and navigation for the IviPHP ecosystem.
iviphp/admin provides a framework-independent foundation for building administration panels without coupling the application to a specific router, ORM, database or frontend library.
- PHP 8.2 or later
iviphp/authiviphp/contractsiviphp/httpiviphp/supportiviphp/view
composer require iviphp/admin- Standalone administrative pages
- Administrative resources with multiple pages
- Authentication-aware access rules
- Resource-level and page-level authorization
- Immutable page and resource definitions
- Navigation registration and grouping
- Route-path resolution
- Navigation filtering by authenticated user
- Custom navigation entries
- View rendering through
iviphp/view - Framework-independent architecture
- Safe diagnostic exceptions
An admin page represents one administrative screen.
A page defines:
- a unique name;
- a title;
- a route path;
- a view;
- view data;
- an access rule;
- navigation metadata.
Examples include dashboards, settings pages and reports.
An admin resource groups pages related to one application concept.
Examples include:
- users;
- projects;
- orders;
- products;
- system settings.
A resource may define its own access rule and navigation entry.
Navigation stores immutable menu items and provides:
- unique-name validation;
- route conflict detection;
- deterministic sorting;
- grouping;
- exact and prefix route matching.
AdminManager coordinates:
- page registration;
- resource registration;
- access control;
- route resolution;
- navigation;
- rendering.
Admin is the public application-facing wrapper around AdminManager.
<?php
declare(strict_types=1);
use Ivi\Admin\Pages\AdminPage;
$dashboard = new AdminPage(
name: 'dashboard',
title: 'Dashboard',
path: '/admin',
view: 'admin.dashboard',
data: [
'heading' => 'Administration',
],
navigationLabel: 'Dashboard',
navigationGroup: 'General',
navigationIcon: 'dashboard',
navigationOrder: 10
);The page is immutable after construction.
Page data may be provided through a closure.
<?php
declare(strict_types=1);
use Ivi\Admin\Pages\AdminPage;
$page = new AdminPage(
name: 'reports',
title: 'Reports',
path: '/admin/reports',
view: 'admin.reports',
data: static function (): array {
return [
'generatedAt' => new DateTimeImmutable(),
];
}
);The resolver is executed whenever data() is called.
It must return an array.
A page may use a fixed boolean:
<?php
declare(strict_types=1);
use Ivi\Admin\Pages\AdminPage;
$page = new AdminPage(
name: 'public.status',
title: 'System Status',
path: '/admin/status',
view: 'admin.status',
access: true
);It may also use an access resolver:
<?php
declare(strict_types=1);
use Ivi\Admin\Pages\AdminPage;
use Ivi\Auth\Contracts\AuthenticatableInterface;
$page = new AdminPage(
name: 'system.settings',
title: 'System Settings',
path: '/admin/settings',
view: 'admin.settings',
access: static function (
?AuthenticatableInterface $user
): bool {
if ($user === null) {
return false;
}
return $user->authIdentifier() === 'administrator';
}
);A null identity represents an unauthenticated request.
<?php
declare(strict_types=1);
use Ivi\Admin\Pages\AdminPage;
use Ivi\Admin\Resources\AdminResource;
$listUsers = new AdminPage(
name: 'users.index',
title: 'Users',
path: '/admin/users',
view: 'admin.users.index',
registerNavigation: false
);
$createUser = new AdminPage(
name: 'users.create',
title: 'Create User',
path: '/admin/users/create',
view: 'admin.users.create',
registerNavigation: false
);
$users = new AdminResource(
name: 'users',
label: 'User',
pluralLabel: 'Users',
path: '/admin/users',
pages: [
$listUsers,
$createUser,
],
navigationLabel: 'Users',
navigationGroup: 'Management',
navigationIcon: 'users',
navigationOrder: 20
);All resource pages must use paths inside the resource base path.
For example, a resource using /admin/users may contain:
/admin/users
/admin/users/create
/admin/users/42
/admin/users/42/edit
Resource access is evaluated before page access.
<?php
declare(strict_types=1);
use Ivi\Admin\Resources\AdminResource;
use Ivi\Auth\Contracts\AuthenticatableInterface;
$resource = new AdminResource(
name: 'users',
label: 'User',
pluralLabel: 'Users',
path: '/admin/users',
pages: [],
access: static function (
?AuthenticatableInterface $user
): bool {
return $user !== null;
}
);A user must pass both the resource access rule and the page access rule.
<?php
declare(strict_types=1);
use Ivi\Admin\Admin;
use Ivi\Admin\AdminManager;
/** @var \Ivi\View\ViewInterface $view */
/** @var \Ivi\Auth\Auth $auth */
$manager = new AdminManager(
view: $view,
auth: $auth
);
$admin = new Admin($manager);Authentication is optional:
$manager = new AdminManager(
view: $view
);Without an authentication service, the current user is always null unless an identity is passed explicitly.
$admin->registerPage($dashboard);Determine whether a page exists:
if ($admin->hasPage('dashboard')) {
$page = $admin->page('dashboard');
}Find a page by route path:
$page = $admin->pageByPath('/admin');Replace a standalone page:
$admin->replacePage($updatedDashboard);Remove a standalone page:
$admin->forgetPage('dashboard');Resource-owned pages must be removed with their resource.
$admin->registerResource($users);Retrieve a resource:
$resource = $admin->resource('users');Find a resource by its exact base path:
$resource = $admin->resourceByPath(
'/admin/users'
);Match the most specific resource for a route:
$resource = $admin->matchResource(
'/admin/users/42/edit'
);Replace a resource:
$admin->replaceResource($updatedUsers);Remove a resource and its pages:
$admin->forgetResource('users');Render a page by name:
$html = $admin->render('dashboard');Pass additional data:
$html = $admin->render(
'dashboard',
[
'message' => 'Welcome back.',
]
);Additional data overrides data defined by the page.
Render from a route path:
$html = $admin->renderPath(
'/admin/users'
);The manager injects an adminContext variable into every rendered page.
[
'page' => $page,
'resource' => $resource,
'navigation' => $navigation,
'user' => $user,
'manager' => $manager,
]The adminContext variable cannot be overridden by page data or caller-provided data.
Check access using the current authenticated identity:
if ($admin->canAccessPage('dashboard')) {
// The current user may access the page.
}Check access for an explicit identity:
if ($admin->canAccessPage('dashboard', $user)) {
// The supplied user may access the page.
}Check resource access:
if ($admin->canAccessResource('users', $user)) {
// The supplied user may access the resource.
}Rendering a denied page throws AdminException.
Retrieve navigation visible to the current authenticated identity:
$navigation = $admin->navigation();
foreach ($navigation->all() as $item) {
echo $item->label();
echo $item->path();
}Retrieve navigation for an explicit identity:
$navigation = $admin->navigation($user);The returned navigation contains only pages and resources accessible to that identity.
$groups = $admin
->navigation()
->grouped();
foreach ($groups as $group => $items) {
echo $group === ''
? 'General'
: $group;
foreach ($items as $item) {
echo $item->label();
}
}Retrieve one group:
$management = $admin
->navigation()
->group('Management');Ungrouped entries are retrieved with null:
$general = $admin
->navigation()
->group(null);Find an exact or nested route match:
$item = $admin
->navigation()
->match('/admin/users/42/edit');
if ($item !== null) {
echo $item->name();
}Prefix matching returns the item with the longest matching path.
<?php
declare(strict_types=1);
use Ivi\Admin\Navigation\MenuItem;
$admin->registerNavigationItem(
new MenuItem(
name: 'documentation',
label: 'Documentation',
path: '/admin/documentation',
group: 'Help',
icon: 'book',
order: 100,
metadata: [
'external' => false,
]
)
);Custom items without page or resource metadata are not filtered by admin access rules.
Replace an existing navigation entry:
$admin->registerNavigationItem(
$replacement,
replace: true
);$item = new MenuItem(
name: 'system.health',
label: 'System Health',
path: '/admin/system/health',
metadata: [
'badge' => 'Healthy',
'section' => 'system',
]
);Read metadata:
if ($item->hasMetadata('badge')) {
echo $item->metadataValue('badge');
}Export the item:
$data = $item->toArray();Navigation entries are sorted by:
- group;
- order;
- label;
- name.
Lower order values appear before higher values.
new MenuItem(
name: 'dashboard',
label: 'Dashboard',
path: '/admin',
order: 10
);
new MenuItem(
name: 'users',
label: 'Users',
path: '/admin/users',
order: 20
);The dashboard appears before users.
Custom page implementations may implement:
Ivi\Admin\Contracts\AdminPageInterfaceRequired methods:
public function name(): string;
public function title(): string;
public function path(): string;
public function view(): string;
public function data(): array;
public function canAccess(
?AuthenticatableInterface $user
): bool;
public function shouldRegisterNavigation(): bool;
public function navigationLabel(): string;
public function navigationGroup(): ?string;
public function navigationIcon(): ?string;
public function navigationOrder(): int;Custom resource implementations may implement:
Ivi\Admin\Contracts\AdminResourceInterfaceRequired methods:
public function name(): string;
public function label(): string;
public function pluralLabel(): string;
public function path(): string;
public function pages(): array;
public function canAccess(
?AuthenticatableInterface $user
): bool;
public function shouldRegisterNavigation(): bool;
public function navigationLabel(): string;
public function navigationGroup(): ?string;
public function navigationIcon(): ?string;
public function navigationOrder(): int;
public function metadata(): array;Administrative failures are represented by:
Ivi\Admin\Exceptions\AdminExceptionExamples include:
- duplicate page names;
- duplicate resource names;
- duplicate navigation items;
- conflicting route paths;
- invalid page or resource names;
- invalid configuration;
- denied access;
- rendering failures.
<?php
declare(strict_types=1);
use Ivi\Admin\Exceptions\AdminException;
try {
$html = $admin->render('dashboard');
} catch (AdminException $exception) {
echo $exception->getMessage();
$context = $exception->context();
}Exception context intentionally excludes authenticated-user details, passwords and page data.
Remove all registered pages, resources and navigation items:
$admin->flush();The configured view and authentication services remain attached to the manager.
iviphp/admin follows these principles:
- no dependency on a specific router;
- no dependency on an ORM;
- no dependency on a database;
- no direct HTML generation;
- explicit access rules;
- immutable definitions where practical;
- predictable route and navigation behavior;
- safe exception context;
- compatibility with application-specific admin interfaces.
Applications should still enforce authorization at the controller, service and domain layers.
Admin navigation visibility must not be treated as the only security boundary.
Do not place secrets, passwords, tokens or private user data inside:
- page metadata;
- resource metadata;
- navigation metadata;
- exception context.
Views should escape untrusted output according to the rendering context.
Ivi Admin is open-source software released under the MIT License.
Maintained by Gaspard Kirira and Softadastra.