Skip to content

feat(php): invocation-only dynamic snippets + clientImport - #17452

Open
cadesark wants to merge 2 commits into
cade/clientimport-invocation-snippetfrom
cade/php-clientimport
Open

feat(php): invocation-only dynamic snippets + clientImport#17452
cadesark wants to merge 2 commits into
cade/clientimport-invocation-snippetfrom
cade/php-clientimport

Conversation

@cadesark

@cadesark cadesark commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Populates the optional clientImport field on InvocationSnippetResponse for the PHP dynamic-snippets invocation generator, mirroring the TypeScript port on the base branch.

The PHP generator now renders a bare reference to the generated root client class and captures the use ...; statement it requires via AstNode.toStringWithoutImports — the same helper that already backs the existing imports field. This is distinct from imports, which only carries imports the bare call itself references; clientImport is the client's own import. Empty string when the client needs no import. This lets docs render the client import (e.g. use Acme\AcmeClient;) without hand-authoring it.

Changes

  • generators/php/dynamic-snippets/src/EndpointSnippetGenerator.ts: build a bare root-client-class reference and set clientImport from its rendered use ...; block.
  • generators/php/dynamic-snippets/src/__test__/InvocationSnippet.test.ts: assert clientImport === "use Acme\\AcmeClient;".

Testing

  • pnpm turbo run compile --filter @fern-api/php-dynamic-snippets — passes.
  • Package vitest suite — 44/44 pass (including the new clientImport assertion). No snapshot churn.

Generated with Claude Code


Open in Devin Review

@cadesark cadesark self-assigned this Aug 17, 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

Adds clientImport to the PHP invocation snippet response by rendering a throwaway reference to the root client class and harvesting its use statement. The mechanism works, but the clientVariableName option is now language-sigil-sensitive in PHP (callers must pass $foo, unlike C#'s foo), and the client-reference rendering is more indirect than it needs to be.

  • 🟡 2 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

clientVariableName is passed through verbatim, so a caller must remember to include the $ for PHP (the test uses "$mailchimp") while the C# generator in this same PR takes a bare mailchimp. A shared Options field with per-language sigil rules is a footgun — passing "mailchimp" here silently emits invalid PHP. Normalize instead:

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

Comment on lines +113 to +125
const clientReference = php.codeblock((writer) => {
writer.writeNode(
php.classReference({
name: this.context.getRootClientClassName(),
namespace: this.context.rootNamespace
})
);
});
const { imports: clientImport } = clientReference.toStringWithoutImports({
namespace: SNIPPET_NAMESPACE,
rootNamespace: SNIPPET_NAMESPACE,
customConfig: this.context.customConfig ?? {}
});

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

php.classReference(...) is itself an AstNode, so the codeblock wrapper (and the discarded code) is unnecessary indirection. Also worth hoisting getRootClientClassName() since it's called again on line 129.

Suggested change
const clientReference = php.codeblock((writer) => {
writer.writeNode(
php.classReference({
name: this.context.getRootClientClassName(),
namespace: this.context.rootNamespace
})
);
});
const { imports: clientImport } = clientReference.toStringWithoutImports({
namespace: SNIPPET_NAMESPACE,
rootNamespace: SNIPPET_NAMESPACE,
customConfig: this.context.customConfig ?? {}
});
const clientImportOnly = php.classReference({
name: this.context.getRootClientClassName(),
namespace: this.context.rootNamespace
});
const { imports: clientImport } = clientImportOnly.toStringWithoutImports({
namespace: SNIPPET_NAMESPACE,
rootNamespace: SNIPPET_NAMESPACE,
customConfig: this.context.customConfig ?? {}
});

const body = writer.toString(true);
const imports = writer.importsToString() ?? "";
return {
code: formatter != null ? formatter.formatSync(body) : body,

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

formatter.formatSync(body) — does AbstractFormatter expose a sync variant? Every other call site in this file uses the async format(...). If formatSync doesn't exist this won't compile; if it does, note that imports is returned unformatted while code is formatted, which can produce inconsistent indentation when the caller concatenates them.

cadesark and others added 2 commits August 17, 2026 17:23
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>
Populates the optional clientImport field on InvocationSnippetResponse for
the PHP dynamic-snippets invocation generator, mirroring the TypeScript port.

Renders a bare reference to the generated root client class and captures the
`use ...;` statement it requires via AstNode.toStringWithoutImports (the same
helper used for the existing `imports` field). Distinct from `imports`, which
only carries the imports the bare call itself references. Empty string when the
client needs no import. Lets docs render the client import (e.g.
`use Acme\AcmeClient;`) without hand-authoring it.

Co-Authored-By: Claude <noreply@anthropic.com>

@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: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@cadesark
cadesark force-pushed the cade/php-clientimport branch from 1531d98 to 907806e Compare August 17, 2026 21:24
@cadesark cadesark changed the title feat(php): expose clientImport in invocation snippets feat(php): invocation-only dynamic snippets + clientImport Aug 18, 2026
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