Skip to content
docsv0.24.0

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): ?string

Why `$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

Parameters of diagnose()
NameTypeDescription
$contentsstring

PluginSurgeon::hasMethod()

public function hasMethod(string $contents, string $method): bool

Whether the class in `$contents` declares a concrete (braced) method named `$method`.

Parameters

Parameters of hasMethod()
NameTypeDescription
$contentsstring
$methodstring

PluginSurgeon::insertIntoMethod()

public function insertIntoMethod(string $contents, string $method, string $snippet): string

Splices `$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

Parameters of insertIntoMethod()
NameTypeDescription
$contentsstring
$methodstring
$snippetstring

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): string

Splices `$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

Parameters of insertIntoReturnArray()
NameTypeDescription
$contentsstring
$methodstring
$snippetstring

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): string

Appends 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

Parameters of appendMethod()
NameTypeDescription
$contentsstring
$methodSourcestring

Throws

\RuntimeException When the file is unparseable or carries no locatable class body.

PluginSurgeon::ensureImplements()

public function ensureImplements(string $contents, string $interfaceFqcn): string

Adds `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

Parameters of ensureImplements()
NameTypeDescription
$contentsstring
$interfaceFqcnstring

Throws

\RuntimeException When the file is unparseable or carries no class declaration.

PluginSurgeon::wrapMethod()

public function wrapMethod(string $signature, string $body): string

Renders 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

Parameters of wrapMethod()
NameTypeDescription
$signaturestring
$bodystring