---
name: admin-controllers
description: Guide for building Vvveb CMS admin controllers — Crud/Listing base classes and traits, request/view/response/session services, validation rule files, CSRF and permissions, admin menu registration, admin Vtpl templates and admin HTML sources with gulp. Use when creating a new admin controller, adding save/delete actions, wiring validation, adding admin menu entries, or debugging an admin page that 404s, denies permission or fails CSRF.
---

# Vvveb Admin Controllers

Admin controllers live in `admin/controller/<group>/<name>.php` with class `Vvveb\Controller\<Group>\<Name>` and are reached via query string: `?module=<group>/<name>&action=<method>`. The `FrontController` injects `$this->request`, `$this->view`, `$this->response`, `$this->session` before `init()` runs.

Reference implementations: `admin/controller/base.php` (services, CSRF, permissions, menu), `admin/controller/crud.php` + `system/traits/crud.php`, `admin/controller/listing.php` + `system/traits/listing.php`, `admin/controller/settings/settings.php` (validation example), `admin/controller/tools/*-backup.php` (AJAX stepped actions). Full narrative: `docs/developer/controller-guide.md`.

## Workflow

1. Pick the base: `Crud` for single-record edit pages, `Listing` for paginated lists, plain `Base` for custom tools.
2. Set the `$type`/`$list`/`$module` properties so traits and templates resolve the model and URLs.
3. Add validation rule files only where user input is saved; register menu + permissions.
4. Verify with `php -l`, load `?module=<group>/<name>` as admin, test save/delete including CSRF failure and denied-role paths.

## Crud vs Listing

```php
// admin/controller/product/products.php
namespace Vvveb\Controller\Product;

class Products extends \Vvveb\Controller\Listing {
    public string $type = 'product';
    public string $list = 'product';
    public string $module = 'product';
}
```

- `Crud` provides `index()` (load one record into `$this->view->{$type}`), `save()`, `delete()` via `system/traits/crud.php`.
- `Listing` provides paginated `index()` (sets `view->{list}`, `view->count/limit/filter`) and row `url`/`edit-url`/`delete-url` via `\Vvveb\url(['module' => "$module/$controller", ...])`.
- Override `index()` and call the trait alias first (`$results = $this->get();`), then add extras like `$this->view->status`.

## Request / view / response

```php
$page  = $this->request->get['page'] ?? 1;
$name  = $this->request->post['name'] ?? '';
$file  = $this->request->files['upload'] ?? null;
if ($this->request->getMethod() === 'post') { ... }
if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url)])); }

$this->view->title = 'Page Title';
$this->view->set($results);            // many props at once
$this->view->errors[]  = __('Name is required');
$this->view->success[] = __('Saved successfully');

$this->response->setType('json');
return $this->results;                 // JSON output
return false;                          // no template (API-style)
```

URLs always use module names, never patterns: `\Vvveb\url('product/products/index')`, `\Vvveb\url(['module' => 'tools/backup', 'action' => 'save'])`.

## Validation

Rule files in `admin/validate/<name>.php` return field → rule arrays; invoke with the rule id:

```php
$validator = new \Vvveb\Validator(['settings']);
$errors = $validator->validate($this->request->post);
if ($errors !== true) { $this->view->errors = $errors; }
```

Reference: `admin/controller/settings/settings.php`, `admin/controller/content/edit.php` (dual `Validator([$this->object])` + `Validator([$this->object . '_content'])`).

## CSRF, permissions, menu

- Forms carry `<input type="hidden" name="csrf" data-v-csrf>`; POST actions must pass `$this->checkCsrf()` (admin `Base::init()` enforces the session token).
- `$this->permission()` in `Base::init()` denies by role — new actions need role permissions or they 403 for non-superadmins.
- Menu lives in `config/admin-menu.php` (cached as `admin-menu`); plugins inject via `Event::on('Vvveb\Controller\Base', 'init-menu', __CLASS__, fn ($menu) => [$menu])` — four args, must return the tuple.

## Templates and admin HTML

- Tpl lookup: `admin/template/<group>/<name>.tpl`, fallback `admin/template/common.tpl`; reusable bases `admin/template/crud.tpl`, `listing.tpl`, `pagination.tpl`.
- Admin HTML sources live in `public/admin/default/src/` and build to `public/admin/default/` via `npx gulp fileinclude` (gulpfile at `public/admin/default/gulpfile.js`) — never hand-edit built files.
- Admin JS controllers live in `public/js/admin/controller/*.js` and are included per page.

## Pitfalls

- Wrong namespace/case (`product` vs `Product`) or missing `$type` breaks model resolution and row URLs.
- Forgetting `checkCsrf()` handling makes POSTs fail closed; forgetting permissions makes them fail for editors.
- Returning nothing renders the default template; return `false` for raw/API output.
- Stale `admin-menu` cache hides new menu entries — clear cache while developing.
