Skip to content

feat(php): generate invocation-only dynamic snippets - #17407

Open
cadesark wants to merge 4 commits into
devin/1786644600-invocation-only-dynamic-snippetsfrom
cade/php-invocation-snippets
Open

feat(php): generate invocation-only dynamic snippets#17407
cadesark wants to merge 4 commits into
devin/1786644600-invocation-only-dynamic-snippetsfrom
cade/php-invocation-snippets

Conversation

@cadesark

@cadesark cadesark commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Ports the invocation-only dynamic-snippet hook to the PHP generator, mirroring the finalized TypeScript contract (#17393) and the Python (#17402) / Go (#17403) / Java (#17404) / C# (#17405) ports. Stacked on devin/1786644600-invocation-only-dynamic-snippets.

Structured contract

Alongside the full snippet, the generator now returns InvocationSnippetResponse = { snippet, imports, clientName, errors }:

  • snippet — the bare call, honoring options.clientVariableName (default $client). No $client = new ...Client(...) construction, no <?php prefix or namespace ...; header, no trailing ; (stripped). Example: $client->endpoints->httpMethods->testGet(\n 'id',\n).
  • imports — the PHP use ...; block the call references, rendered as a string ("" when none).
  • clientName — the generated PHP client class name (context.getRootClientClassName(), e.g. AcmeClient).
  • errors — preserved from the existing ErrorReporter.

How use imports are captured (AST change)

PHP's Writer already tracks references and prepends a namespace ...; + use ...; block in toString(skipImports=false). Mirroring the C#/Java precedent, I split the render:

  • Writer.importsToString() (new, generators/php/codegen/src/ast/core/Writer.ts) — exposes the rendered use ...; block on its own (empty string when none).
  • AstNode.toStringWithoutImports() (new, generators/php/codegen/src/ast/core/AstNode.ts) — a single write pass yields { code, imports }: code is the node body with no namespace/use header (writer.toString(true)), imports is the use ...; block. This is the PHP analogue of the TS AST's toStringWithoutImports.

EndpointSnippetGenerator.callMethod is parameterized with clientVariableName, and the new generateInvocationSnippetSync renders only the invocation node through toStringWithoutImports. Invocations that construct types from another namespace inline (an inlined request class, or a DateTime body value) surface those use statements rather than bailing to the full snippet; a plain call returns imports === "".

Tests

generators/php/dynamic-snippets/src/__test__/InvocationSnippet.test.ts (5 tests):

  • bare call — exact string, no client construction / <?php / namespace / use, no trailing ;
  • imports === "" for a bare call
  • clientName = AcmeClient
  • custom clientVariableName ($mailchimp)
  • import-referencing invocation — asserts use Acme\Types\Object\Types\ObjectWithOptionalField; and use DateTime; are surfaced

Verification

  • pnpm turbo run compile --filter @fern-api/php-dynamic-snippets --filter @fern-api/php-codegen — green
  • pnpm turbo run test --filter @fern-api/php-dynamic-snippets — 43/43 green (5 new + 38 existing)
  • npx biome check --write on the 4 changed .ts files — clean, no fixes
  • Changelog: generators/php/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml (type: feat); versions.yml untouched

Generated with Claude Code


Open in Devin Review

cadesark and others added 4 commits August 13, 2026 18:10
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…snippets

The invocation-only dynamic-snippets hook now returns a structured
InvocationSnippetResponse (snippet + imports + clientName + errors) instead of
a bare call string. Callers such as documentation code templates can now
regenerate the imports and client instantiation alongside the call and keep
them in sync across SDK renames.

The branded-string-alias case, which previously returned undefined (dropping
the call because it referenced an SDK import the caller couldn't supply), now
returns the call plus the required import block. generateInvocationSync still
returns undefined only for the "generator doesn't implement the hook"
capability check the multi-language fan-out relies on.

Co-Authored-By: Claude <noreply@anthropic.com>
Mirrors the finalized TypeScript structured contract (PR #17393) and the
Python (#17402) / Go (#17403) / Java (#17404) / C# (#17405) ports: alongside
the full snippet, the generator now returns InvocationSnippetResponse =
{ snippet, imports, clientName, errors } for callers (e.g. docs code templates)
that render the invocation inside code they already own.

- snippet: the bare call (honoring options.clientVariableName), no client
  construction, no `<?php` prefix or `namespace ...;` header, and no trailing `;`.
- imports: the PHP `use ...;` block the call references, captured separately via
  a new AstNode.toStringWithoutImports helper (backed by Writer.importsToString) —
  the PHP analogue of the TS AST's toStringWithoutImports and the C#/Java AST
  helpers. Empty string when the call needs no imports; populated when the
  invocation constructs types from another namespace inline (e.g. an inlined
  request class or a DateTime body value).
- clientName: the generated client class name (context.getRootClientClassName()).
- errors: preserved from the existing error reporter.

Co-Authored-By: Claude <noreply@anthropic.com>
@cadesark cadesark self-assigned this Aug 13, 2026

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Clean port of the invocation-only snippet hook to PHP. Main concern: clientVariableName is passed straight into php.codeblock without normalizing the $ sigil, so a caller passing the language-agnostic client (as TS/Python/Go callers do) yields invalid PHP. Minor nit on the redundant importsToString wrapper.

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

}): php.MethodInvocation {
return php.invokeMethod({
on: php.codeblock(CLIENT_VAR_NAME),
on: php.codeblock(clientVariableName ?? CLIENT_VAR_NAME),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

options.clientVariableName comes from the shared, language-agnostic Options contract, so callers will typically pass client / mailchimp (no sigil) — the same value handed to the TS/Python/Go generators. Interpolating it raw produces invalid PHP (mailchimp->endpoints->...). The test only exercises the pre-sigiled "$mailchimp", so this slips through.

Normalize the sigil:

Suggested change
on: php.codeblock(clientVariableName ?? CLIENT_VAR_NAME),
on: php.codeblock(
clientVariableName != null
? clientVariableName.startsWith("$")
? clientVariableName
: `$${clientVariableName}`
: CLIENT_VAR_NAME
),

And add a test case passing clientVariableName: "mailchimp".

*/
public importsToString(): string {
return this.stringifyImports();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

importsToString is a pure pass-through to stringifyImports. Just widen stringifyImports to public (or rename it) rather than maintaining two names for the same thing.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Custom client variable name produces invalid PHP when passed without a dollar sign

The requested client variable name is inserted verbatim into the generated call (php.codeblock(clientVariableName ?? CLIENT_VAR_NAME) at generators/php/dynamic-snippets/src/EndpointSnippetGenerator.ts:177) without adding PHP's required $ prefix, so a caller that passes a plain name gets a code sample that is not valid PHP.
Impact: Documentation samples rendered with a custom client name can be syntactically broken and won't run for users.

Cross-language option is language-agnostic while PHP variables require a leading `$`

clientVariableName is a shared, language-agnostic option ("The name of the variable the endpoint is invoked on", generators/browser-compatible-base/src/dynamic-snippets/Options.ts). The TypeScript port's test passes it as a bare identifier (clientVariableName: "mailchimp", generators/typescript-v2/dynamic-snippets/src/__test__/InvocationSnippet.test.ts:52), whereas the PHP test only exercises the pre-prefixed form ("$mailchimp", generators/php/dynamic-snippets/src/__test__/InvocationSnippet.test.ts:68). With a bare name the PHP generator emits mailchimp->endpoints->httpMethods->testGet(...), which is invalid PHP; the default path is unaffected because CLIENT_VAR_NAME is "$client". Normalizing (prepending $ when absent) would make the PHP generator robust to the shared contract; note PHP's own SDK context also returns the $-prefixed form (generators/php/sdk/src/readme/ReadmeSnippetBuilder.ts:152).

(Refers to lines 176-178)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant