Skip to content
docsv0.30.2

DeclaredOperation

Turns a declared class into the SAME `Operation` value object every projector already reads. This is the derivation half of greenhouse decisions/0212. The rule it implements, in one line: **What changes authority is DECLARED; what a type already says is DERIVED.** So the attributes carry intent — the name, the purpose, what it mutates, whose authority it spends, which argument the human must name — and this class reads the CONSTRUCTOR to derive the input schema and the RETURN TYPE to derive the output one. A `string $name` needs no `['type' => 'string']` written beside it; a PHP enum needs no second list of its own values. ── WHY IT PRODUCES THE OLD VALUE OBJECT AND NOT A NEW ONE ────────────────────────────────────── Because nothing downstream may change. CLI flags, MCP tools, HTTP routes and methods, consent, signatures, the agent's catalogue and `plugins:architecture` all read `Operation` today, and 82 declaration sites across the platform build it by hand. Emitting a different type would fork the ecosystem on the day it landed. Emitting the same one makes the two styles peers: the attribute wins where it exists, and the hand-written form dies by attrition rather than by decree. ── WHY IT RUNS ONCE, AT DECLARATION ──────────────────────────────────────────────────────────── Reflection is not free, and an operation is declared once per process and invoked many times. So every reflective question — the schema, the target, the collaborators `run()` needs, whether they can even be resolved — is asked HERE, and the handler that comes out closes over plain arrays. A request pays nothing for having been declared this way. ── WHAT IT REFUSES TO GUESS ──────────────────────────────────────────────────────────────────── Every refusal below names the class and says what to declare, and every one of them exists because the alternative is a silent lie: - Neither `#[Mutates]` nor `#[Reads]`. Silence is not «harmless»; an author who forgets would ship `mutating: false` on an operation that writes. - A constructor parameter whose type cannot become a schema. Emitting an empty `{}` would tell every surface «anything goes». - Two `#[Target]`s. Two named targets is not a stricter contract, it is an ambiguous one. - A `run()` that needs collaborators with nobody to resolve them — caught at declaration, not on the first request in production.

DeclaredOperation::isDeclared()

public static function isDeclared(string $class): bool

Does this class declare itself an operation? Lets a provider mix both styles in one list.

Parameters

Parameters of isDeclared()
NameTypeDescription
$classstring

DeclaredOperation::from()

public static function from(string $class, ?Closure $resolve = null): Milpa\Command\Operation

Derives the operation a declared class describes — once, at declaration.

Parameters

Parameters of from()
NameTypeDescription
$classclass-stringThe declaring class.
$resolve(\Closure | null)`fn (string $type): object` — how `run()`'s collaborators are found.

Throws

DeclarationException when the declaration cannot be derived without guessing