iviphp/admin

Admin pages, resources and navigation for the IviPHP ecosystem.

Maintainers

Package info

github.com/iviphp/admin

pkg:composer/iviphp/admin

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

v0.1.0 2026-07-21 15:58 UTC

This package is not auto-updated.

Last update: 2026-07-21 22:24:21 UTC


README

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.

Requirements

  • PHP 8.2 or later
  • iviphp/auth
  • iviphp/contracts
  • iviphp/http
  • iviphp/support
  • iviphp/view

Installation

composer require iviphp/admin

Features

  • 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

Core concepts

Page

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.

Resource

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

Navigation stores immutable menu items and provides:

  • unique-name validation;
  • route conflict detection;
  • deterministic sorting;
  • grouping;
  • exact and prefix route matching.

Admin manager

AdminManager coordinates:

  • page registration;
  • resource registration;
  • access control;
  • route resolution;
  • navigation;
  • rendering.

Admin API

Admin is the public application-facing wrapper around AdminManager.

Creating a page

<?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.

Lazy page data

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.

Page access rules

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.

Creating a resource

<?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 rules

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.

Creating the admin manager

<?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.

Registering pages

$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.

Registering resources

$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');

Rendering pages

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.

Checking access

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.

Navigation

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.

Navigation groups

$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);

Active navigation item

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.

Custom navigation items

<?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
);

Menu item metadata

$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 ordering

Navigation entries are sorted by:

  1. group;
  2. order;
  3. label;
  4. 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.

Page interface

Custom page implementations may implement:

Ivi\Admin\Contracts\AdminPageInterface

Required 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;

Resource interface

Custom resource implementations may implement:

Ivi\Admin\Contracts\AdminResourceInterface

Required 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;

Exceptions

Administrative failures are represented by:

Ivi\Admin\Exceptions\AdminException

Examples 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.

Resetting the admin registry

Remove all registered pages, resources and navigation items:

$admin->flush();

The configured view and authentication services remain attached to the manager.

Design principles

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.

Security

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.

License

Ivi Admin is open-source software released under the MIT License.

Maintainer

Maintained by Gaspard Kirira and Softadastra.