AdminSettings
What the app declared about its admin panel, read once from the `admin` key of its config bag — and, per key, whether it declared it, the panel is running on a default, or the panel refused it. Every knob has a default that keeps a fresh app both safe and served: the panel lives at `/milpa/admin`, speaks English, answers only to loopback, and signs component state with a secret derived from this package unless the app declares one (`admin.secret`, else `live.secret`). Declaring is the whole interface — nothing here is read from the environment. The gate is the one knob the panel judges instead of copying. The rule: **only a literally empty list `[]` opens the panel**. Anything else that is not a list of strings naming a PSR-15 middleware class — a non-string entry, an associative map, a value that is not a list at all, an empty string, a class that does not exist, a class that exists but is not a middleware — is a misdeclaration, and the effective stack is the STRICT gate ({@see LoopbackOnlyMiddleware}) and nothing else: never «open», never the half that loads. The panel says so in Settings and in the topbar (greenhouse decisions/0204). Falling to strict keeps the panel safe; dying with a 500 would hide the cause.
AdminSettings::__construct()
public function __construct(string $route = self::DEFAULT_ROUTE, string $locale = self::DEFAULT_LOCALE, array $middleware = [], string $secret = '', string $title = self::DEFAULT_TITLE, array $sources = [], bool $declared = false, ?string $secretSource = null, array $rejected = []):Parameters
| Name | Type | Description |
|---|---|---|
| $route | string | the panel's mount point, an absolute local path |
| $locale | string | the language of the panel's own copy — one the {@see Catalog} carries |
| $middleware | array<mixed> | what the app DECLARED under `admin.middleware` when it declared an array: every entry as written, non-strings included, keys included when it was a map; `[]` when it declared no array at all — {@see self::malformed()} tells that apart from an empty list. See {@see self::effectiveMiddleware()} for what the routes get |
| $secret | string | the HMAC secret that signs component state envelopes; empty derives one |
| $title | string | the brand shown in the sidebar and the document title |
| $sources | array<string, string> | per key, `config` when the app declared the value the panel is using; anything else, or a key left out, is `default` |
| $declared | bool | whether the `admin` key exists in the app's config at all |
| $secretSource | (string | null) | where the secret came from (`declared:admin.secret`, `declared:live.secret`); null reads it from `$secret` — a declared secret is `admin.secret`'s, an empty one is derived |
| $rejected | array<string, string> | per key, what the app declared and the panel refused, described (the value for a string, the type otherwise) — a key listed here is `rejected` whatever `$sources` says |
AdminSettings::fromConfig()
public static function fromConfig(?Milpa\Runtime\Config $config): selfReads the `admin.*` keys, falling back to the defaults for anything the app did not declare — and remembering, per key, which of three things happened: declared and used (`config`), left out (`default`), or declared and refused (`rejected` — the default runs, and the row says what was written). A missing config bag (the plugin booted without a kernel, as in unit tests) yields the defaults. A key set to null is not a declaration. A route that is a bare slash, a locale the catalog lacks, an empty title, a value of the wrong type — those the app DID write, so they are `rejected`, never painted `default`. The middleware is kept exactly as declared; judging it is {@see self::effectiveMiddleware()}'s.
Parameters
| Name | Type | Description |
|---|---|---|
| $config | ?Milpa\Runtime\Config |
AdminSettings::declared()
public function declared(): boolTrue when the `admin` key exists in the app's config — false means the panel read nothing under it.
AdminSettings::sources()
public function sources(): arrayPer key, `config` when the app declared the value the panel is using, `default` when it did not, `rejected` when it declared one the panel refused — the default runs, and {@see self::rejected()} says what was written.
Returns
`route`, `locale`, `middleware`, `secret`, `title` → `default` | `config` | `rejected`
AdminSettings::rejected()
public function rejected(): arrayWhat the app declared and the panel refused, per key, described: the value for a string (`fr`, `/`, {@see self::EMPTY}), the type for anything else (`int`, `bool`, `array`).
Returns
only the rejected keys
AdminSettings::secretSource()
public function secretSource(): stringWhere the signing secret came from — never the secret itself, not even a fragment.
Returns
`declared:admin.secret` | `declared:live.secret` | `derived`
AdminSettings::malformed()
public function malformed(): boolTrue when `admin.middleware` was declared as something other than a list: a string, a bool, an int, an associative map. The declaration cannot be read entry by entry, so it is refused whole.
AdminSettings::unresolvedMiddleware()
public function unresolvedMiddleware(): arrayEvery reason the declared gate cannot be carried, for a human — empty when it can. One entry per declared entry that fails, described: `Acme\Nope (class does not exist)`, `stdClass (not a PSR-15 middleware)`, `int (not a class name)`, `(empty)`. A declaration that is not a list at all yields one entry naming what was received: `string (not a list)`.
AdminSettings::effectiveMiddleware()
public function effectiveMiddleware(): arrayThe middleware the panel's routes actually carry. The declared stack only when every entry is a string naming a class that exists and implements {@see MiddlewareInterface} — a literally empty list included: the app opened the panel on purpose. Anything else replaces the WHOLE stack with the strict gate: a gate with a hole in it is not a gate, and mixing the half that loads with a silent fallback would hide which half is running.
AdminSettings::gateKind()
public function gateKind(): stringWhat kind of gate the panel is behind. The topbar chip says {@see self::gateLabel()}, which is this except for the one custom stack it knows by name.
Returns
`loopback` (the strict default, declared or not) | `custom` (the app's own stack) | `open` (a literally empty list, on purpose) | `fallback` (a misdeclared stack fell to loopback-only)
AdminSettings::gateLabel()
public function gateLabel(): stringThe gate as the panel names it — {@see self::gateKind()}, except that a stack that is exactly app-runtime's passkey gate (that one class, loadable, alone) is named `passkey`, not `custom`. A presentation rule over the kind, nothing more: the kind stays `custom` and the routes carry the class as declared; a passkey gate that cannot be loaded is a `fallback` like any other.
Returns
`loopback` | `custom` | `passkey` | `open` | `fallback`
AdminSettings::middlewareDefect()
public static function middlewareDefect(?mixed $entry): ?stringWhy one declared middleware entry cannot be a panel gate, described for a human — or null when the runtime can load it: a non-empty string naming a class that exists and is a PSR-15 middleware.
Parameters
| Name | Type | Description |
|---|---|---|
| $entry | ?mixed |
AdminSettings::sectionUrl()
public function sectionUrl(string $id): stringThe URL of one section of the panel.
Parameters
| Name | Type | Description |
|---|---|---|
| $id | string |
AdminSettings::assetUrl()
public function assetUrl(string $file): stringThe URL of one of the panel's own assets (CSS, client runtime).
Parameters
| Name | Type | Description |
|---|---|---|
| $file | string |
AdminSettings::liveUrl()
public function liveUrl(): stringThe URL of the panel's live wire — `POST {route}/live`, the one endpoint every component of the page takes its actions through, host's and guests' alike (greenhouse decisions/0211). It carries the same middleware stack as every other panel route: a wire outside the gate would be a hole.
AdminSettings::composeUrl()
public function composeUrl(): stringThe URL of the compose file the Stack section projects — every declared service, `text/yaml`.
AdminSettings::signingSecret()
public function signingSecret(): stringThe signing secret, derived when the app declared none — stable per install, never empty.