PluginSurgeon
Deterministic, token-based STRUCTURAL insertion into an existing plugin file that carries no `// {marker}` anchor — the closure of the last guidance-prose fallback. When a generator knows the exact registration a postcondition requires, handing it back as text for someone else to paste is an incomplete run, not a deliverable: a measured cattle run had `make entity` create the file, verify it, then return `ok:false` because the repository registration had landed as prose — and the fix was handed to a language model as instructions. {@see MarkerInserter} stays the first choice (an anchor is cheaper and survives any file shape); this class is the second, for files with no anchor but a locatable structure. This is still NOT free-form AST rewriting. Every operation locates ONE unambiguous structural anchor — a method's closing brace, a `return [` list's closing bracket, the class declaration — via `token_get_all()`, whose tokenizer atomizes comments and strings so brace counting cannot be fooled by a `}` inside either, and splices at it, byte-preserving everything else. Anything it cannot locate it REFUSES with a named reason (see {@see diagnose()}): failing closed into guidance is the only remaining prose path, and the postcondition report names the file and the reason.
PluginSurgeon::diagnose()
public function diagnose(string $contents): ?stringWhy `$contents` cannot be structurally edited — `null` when it can (parseable PHP with one locatable class body). The non-null string is the reason a generator's fallback guidance and the postcondition report must NAME, e.g. `PHP parse error: …`, `no class declaration found`.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string |
PluginSurgeon::hasMethod()
public function hasMethod(string $contents, string $method): boolWhether the class in `$contents` declares a concrete (braced) method named `$method`.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string | |
| $method | string |
PluginSurgeon::insertIntoMethod()
public function insertIntoMethod(string $contents, string $method, string $snippet): stringSplices `$snippet` at the END of `$method`'s body — immediately before its closing brace, each line re-indented one level deeper than that brace's own line, so the insertion reads as hand-written code wherever the file put the method.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string | |
| $method | string | |
| $snippet | string |
Throws
\RuntimeException When the file is unparseable, has no class, or has no `$method`.
PluginSurgeon::insertIntoReturnArray()
public function insertIntoReturnArray(string $contents, string $method, string $snippet): stringSplices `$snippet` (one array entry per line, each ending in `,`) at the END of the literal `return [ … ]` list inside `$method` — before its closing `]`, adding the missing trailing comma to the previous entry when the hand-written list lacks one.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string | |
| $method | string | |
| $snippet | string |
Throws
\RuntimeException When the method is missing, or returns anything other than a
literal `[…]` array — the fail-closed case the caller reports.
PluginSurgeon::appendMethod()
public function appendMethod(string $contents, string $methodSource): stringAppends a complete `$methodSource` (as {@see wrapMethod()} renders it) at the END of the class body — before the class's closing brace, indented one level in, separated by a blank line from the previous member.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string | |
| $methodSource | string |
Throws
\RuntimeException When the file is unparseable or carries no locatable class body.
PluginSurgeon::ensureImplements()
public function ensureImplements(string $contents, string $interfaceFqcn): stringAdds `implements \$interfaceFqcn` to the class declaration (or appends it to an existing `implements` list) so an appended method is actually reached by the kernel — a `routes()` on a class that never declares `RouteProviderInterface` would be dead code wearing a wiring. Idempotent: when the declaration already mentions the interface (short name or FQCN), the contents come back unchanged.
Parameters
| Name | Type | Description |
|---|---|---|
| $contents | string | |
| $interfaceFqcn | string |
Throws
\RuntimeException When the file is unparseable or carries no class declaration.
PluginSurgeon::wrapMethod()
public function wrapMethod(string $signature, string $body): stringRenders a complete method source — `$signature` plus a braced `$body` indented one level — in the shape {@see appendMethod()} expects (no outer indentation; that is applied at splice time).
Parameters
| Name | Type | Description |
|---|---|---|
| $signature | string | |
| $body | string |