# 0006. Declarative admin module

- **Status:** Accepted
- **Date:** 2025-06-28

## Context

The back office needs CRUD over roughly fifty entities: storages, locations, rentals, payments, customers,
content. Hand-writing a grid, a filter, a create form, an edit form and a delete confirmation for each is
fifty times the same work, and fifty places for the same bug.

## Decision

A screen is **one controller declaring `Field` objects**. Generic actions supply the behaviour.

```php
class ArticlesController extends BaseController {
    public ?string $modelClass = Article::class;
    protected array $submodules = [ Photo::class ];

    public function getFields() : array {
        return [ ...parent::getFields(),
            TextField::make( 'name' )->colClasses( 'w-100' ),
            CkEditorField::make( 'body' )->height( '450px' ),
            CheckboxField::make( 'isPublished' )->defaultValue( true )
        ];
    }

    protected function getFormLayout() : string { return '<div class="row">…{name}{body}…</div>'; }
}
```

`BaseController::actions()` maps `index`, `create`, `edit`, `delete`, `toggle`, `sort` and `inline-grid` to
`Action` classes. 28 field types cover the input surface; 21 action classes cover the behaviour.

Rights are **derived** from the controller and action rather than stored: `StoragesController` + `edit` →
`STORAGES_EDIT`. Labels are derived too: `bt( 'articlesName' )`, `…Hint`, `…Grid`.

## Alternatives

- **Gii-generated CRUD.** Generates once; every later change is fifty hand edits.
- **An off-the-shelf admin package.** Would not know about the schema-owning models, the JSON translations or
  the widget layer.
- **Hand-written screens.** Rejected on volume.

## Consequences

- **Adding a screen is one controller plus Hungarian labels.** That is the whole cost, and it is why the back
  office kept up with the ERP's growth.
- **The module is a third of the codebase** (13 871 LOC) and is entirely ours to maintain.
- **Convention is executable.** Renaming a controller renames its rights and revokes access for every
  non-super administrator, silently. Renaming a model changes every label key.
- **A field missing from `getFormLayout()` is not rendered**, including required ones — the save then fails
  validation with no visible cause. The layout is authoritative, not additive.
- Sub-grids are the same screens filtered by `propagates`, a JSON parameter carried through the URLs. It is
  request input applied as a raw `WHERE`.
- `inline-grid` and `toggle` both collapse onto the `_EDIT` right; there is no finer control.
