Amvionlie CMS
Where the Future Begins

Admin Runtime Sample

Use this as a copyable starting point for `addons/example_notes/src/admin_runtime.php`.

It shows the expected admin boundary: the addon renders inner screen content, validates permissions and CSRF, saves addon-owned records, returns notices, and redirects after successful POST. The Admin shell owns the frame.

Required Inputs

This sample assumes the route passes:

  • `$request`
  • `$route`
  • `$context`

The context must include:

  • authenticated `authContext`
  • `PDO` database connection
  • shared admin/list helpers already loaded by the route/bootstrap

Copyable Skeleton

<?php
declare(strict_types=1);

if (!defined('HC_ACCESS')) {
    exit;
}

function amv_example_notes_escape(mixed $value): string
{
    return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}

function amv_example_notes_admin_can(array $authContext, string $permission): bool
{
    return function_exists('amv_core_permission_allows')
        && amv_core_permission_allows($authContext, $permission);
}

function amv_example_notes_admin_url(array $query = []): string
{
    $query = array_merge(['view' => 'example_notes'], $query);
    return '/admin/index.php?' . http_build_query($query);
}

function amv_example_notes_context_pdo(array $context): ?PDO
{
    return ($context['pdo'] ?? null) instanceof PDO ? $context['pdo'] : null;
}

function amv_example_notes_auth_context(array $context): array
{
    return is_array($context['authContext'] ?? null) ? $context['authContext'] : [];
}

function amv_example_notes_list(PDO $pdo): array
{
    $statement = $pdo->query(
        "SELECT note_uuid, title, slug, state_key, updated_at
         FROM amv_example_notes
         ORDER BY updated_at DESC
         LIMIT 50"
    );

    return array_values(array_filter($statement->fetchAll() ?: [], 'is_array'));
}

function amv_example_notes_note(PDO $pdo, string $noteUuid): ?array
{
    $statement = $pdo->prepare(
        'SELECT note_uuid, title, slug, body_text, state_key
         FROM amv_example_notes
         WHERE note_uuid = :note_uuid
         LIMIT 1'
    );
    $statement->execute(['note_uuid' => $noteUuid]);
    $row = $statement->fetch();

    return is_array($row) ? $row : null;
}

function amv_example_notes_save(PDO $pdo, array $payload): array
{
    $noteUuid = trim((string) ($payload['note_uuid'] ?? ''));
    $title = trim((string) ($payload['title'] ?? ''));
    $body = trim((string) ($payload['body_text'] ?? ''));
    $state = in_array((string) ($payload['state_key'] ?? 'draft'), ['draft', 'published', 'archived'], true)
        ? (string) $payload['state_key']
        : 'draft';

    if ($title === '') {
        return ['ok' => false, 'reason' => 'title_required'];
    }

    $slug = function_exists('amv_core_slug_normalize')
        ? amv_core_slug_normalize((string) ($payload['slug'] ?? $title))
        : strtolower(preg_replace('/[^a-z0-9]+/i', '-', $title) ?? '');

    if ($slug === '') {
        return ['ok' => false, 'reason' => 'slug_required'];
    }

    if ($noteUuid !== '') {
        $statement = $pdo->prepare(
            'UPDATE amv_example_notes
             SET title = :title, slug = :slug, body_text = :body_text, state_key = :state_key, updated_at = NOW()
             WHERE note_uuid = :note_uuid'
        );
        $statement->execute([
            'title' => $title,
            'slug' => $slug,
            'body_text' => $body,
            'state_key' => $state,
            'note_uuid' => $noteUuid,
        ]);

        return ['ok' => true, 'note_uuid' => $noteUuid];
    }

    $noteUuid = function_exists('amv_core_uuid_generate')
        ? amv_core_uuid_generate()
        : 'replace-with-core-uuid';
    $statement = $pdo->prepare(
        'INSERT INTO amv_example_notes
         (note_uuid, title, slug, body_text, state_key, created_at, updated_at)
         VALUES
         (:note_uuid, :title, :slug, :body_text, :state_key, NOW(), NOW())'
    );
    $statement->execute([
        'note_uuid' => $noteUuid,
        'title' => $title,
        'slug' => $slug,
        'body_text' => $body,
        'state_key' => $state,
    ]);

    return ['ok' => true, 'note_uuid' => $noteUuid];
}

function amv_example_notes_render_list(PDO $pdo, string $notice = ''): string
{
    $rows = '';
    foreach (amv_example_notes_list($pdo) as $note) {
        $editUrl = amv_example_notes_admin_url([
            'screen' => 'edit',
            'note_uuid' => (string) ($note['note_uuid'] ?? ''),
        ]);
        $rows .= '<tr>'
            . '<td>' . amv_example_notes_escape($note['title'] ?? '') . '</td>'
            . '<td>' . amv_example_notes_escape($note['slug'] ?? '') . '</td>'
            . '<td>' . amv_example_notes_escape($note['state_key'] ?? '') . '</td>'
            . '<td><a class="button secondary" href="' . amv_example_notes_escape($editUrl) . '">Edit</a></td>'
            . '</tr>';
    }

    if ($rows === '') {
        $rows = '<tr><td colspan="4">No notes found.</td></tr>';
    }

    $table = '<table><thead><tr><th>Title</th><th>Slug</th><th>State</th><th>Actions</th></tr></thead><tbody>'
        . $rows
        . '</tbody></table>';

    return amv_admin_template_list_panel([
        'eyebrow' => 'Addon',
        'title' => 'Example Notes',
        'intro' => 'Manage published and draft notes.',
        'notice' => $notice !== '' ? '<p class="notice">' . amv_example_notes_escape($notice) . '</p>' : '',
        'actionsHtml' => '<a class="button" href="' . amv_example_notes_escape(amv_example_notes_admin_url(['screen' => 'create'])) . '">Create Note</a>',
        'table' => $table,
        'listKey' => 'example-notes',
    ]);
}

function amv_example_notes_render_form(?array $note, string $notice = ''): string
{
    $isEdit = is_array($note);
    $noteUuid = $isEdit ? (string) ($note['note_uuid'] ?? '') : '';
    $csrfAction = $isEdit ? 'example_notes.note.update.' . $noteUuid : 'example_notes.note.create';
    $action = amv_example_notes_admin_url($isEdit
        ? ['screen' => 'edit', 'note_uuid' => $noteUuid]
        : ['screen' => 'create']);

    $form = '<form method="post" action="' . amv_example_notes_escape($action) . '">'
        . '<input type="hidden" name="csrf_token" value="' . amv_example_notes_escape(amv_core_csrf_issue($csrfAction)) . '">'
        . '<input type="hidden" name="note_uuid" value="' . amv_example_notes_escape($noteUuid) . '">'
        . '<label>Title <input name="title" value="' . amv_example_notes_escape($note['title'] ?? '') . '" required></label>'
        . '<label>Slug <input name="slug" value="' . amv_example_notes_escape($note['slug'] ?? '') . '"></label>'
        . '<label>State <select name="state_key">'
        . '<option value="draft">Draft</option><option value="published">Published</option><option value="archived">Archived</option>'
        . '</select></label>'
        . '<label>Body <textarea name="body_text" rows="10">' . amv_example_notes_escape($note['body_text'] ?? '') . '</textarea></label>'
        . '<button type="submit">Save Note</button>'
        . '</form>';

    return amv_admin_template_form_panel([
        'eyebrow' => 'Addon',
        'title' => $isEdit ? 'Edit Note' : 'Create Note',
        'intro' => 'Save addon-owned note content.',
        'notice' => $notice !== '' ? '<p class="notice">' . amv_example_notes_escape($notice) . '</p>' : '',
        'form' => $form,
    ]);
}

function amv_example_notes_handle_post(PDO $pdo, array $authContext, string $screen): array
{
    if (!amv_example_notes_admin_can($authContext, 'example_notes.manage')) {
        return ['notice' => 'Permission denied.', 'redirect' => ''];
    }

    $noteUuid = trim((string) ($_POST['note_uuid'] ?? ''));
    $csrfAction = $noteUuid !== '' ? 'example_notes.note.update.' . $noteUuid : 'example_notes.note.create';
    if (!amv_core_csrf_validate((string) ($_POST['csrf_token'] ?? ''), $csrfAction)) {
        return ['notice' => 'Security token invalid. Reload and try again.', 'redirect' => ''];
    }

    $result = amv_example_notes_save($pdo, $_POST);
    if (($result['ok'] ?? false) !== true) {
        return ['notice' => 'Note could not be saved: ' . (string) ($result['reason'] ?? 'unknown'), 'redirect' => ''];
    }

    return [
        'notice' => 'Note saved.',
        'redirect' => amv_example_notes_admin_url([
            'screen' => 'edit',
            'note_uuid' => (string) ($result['note_uuid'] ?? ''),
            'saved' => '1',
        ]),
    ];
}

function amv_example_notes_admin_route(array $request, array $route, array $context): array
{
    $authContext = amv_example_notes_auth_context($context);
    if (!amv_example_notes_admin_can($authContext, 'admin.access')
        || !amv_example_notes_admin_can($authContext, 'example_notes.view')) {
        return amv_core_error_response('Permission denied.', 403);
    }

    $pdo = amv_example_notes_context_pdo($context);
    if (!$pdo instanceof PDO) {
        return amv_core_response_html('<h1>Database unavailable</h1>', 500);
    }

    $screen = (string) ($_GET['screen'] ?? $_POST['screen'] ?? 'index');
    $notice = isset($_GET['saved']) ? 'Note saved.' : '';

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $postResult = amv_example_notes_handle_post($pdo, $authContext, $screen);
        if ((string) ($postResult['redirect'] ?? '') !== '') {
            return amv_core_response_redirect((string) $postResult['redirect']);
        }
        $notice = (string) ($postResult['notice'] ?? '');
    }

    if ($screen === 'create') {
        return amv_core_response_html(amv_example_notes_render_form(null, $notice));
    }

    if ($screen === 'edit') {
        $note = amv_example_notes_note($pdo, (string) ($_GET['note_uuid'] ?? $_POST['note_uuid'] ?? ''));
        if ($note === null) {
            return amv_core_response_html('<h1>Note not found</h1>', 404);
        }
        return amv_core_response_html(amv_example_notes_render_form($note, $notice));
    }

    return amv_core_response_html(amv_example_notes_render_list($pdo, $notice));
}

Adaptation Checklist

  • Replace `example_notes` with your addon key.
  • Replace table and field names with addon-owned names.
  • Replace permission keys with registered permissions.
  • Keep CSRF action names stable per form/action.
  • Use `amv_core_csrf_issue()` when rendering forms.
  • Use `amv_core_csrf_validate()` before writing.
  • Use `amv_core_permission_allows()` or an addon-local wrapper around it.
  • Redirect after successful POST.
  • Keep admin URLs under `/admin/index.php?view={addon_key}` or the documented alternate admin route.
  • Do not expose admin URLs as Menu Manager public targets.

Hidden-Knowledge Check

A developer should be able to copy this file, replace names, connect the schema and routes, and understand where permissions, CSRF, notices, and redirects belong.

Related: Developer Workflows/Admin Template Rules, Addon Development/Build Your First Addon, Addon Development/Permissions Contract Basics.

Updated: 2026-05-07 02:18:09