Skip to content
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.62.0
changelogEntry:
- summary: |
Use auth scheme placeholder values in snippets when configured via
`placeholder` field on auth schemes.
type: feat
createdAt: "2026-04-25"
irVersion: 66
- version: 2.61.2
changelogEntry:
- summary: |
Expand Down
2 changes: 1 addition & 1 deletion generators/php/dynamic-snippets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
"@fern-api/dynamic-ir-sdk": "66.1.0",
"@fern-api/dynamic-ir-sdk": "66.3.0",
"@fern-api/path-utils": "workspace:*",
"@fern-api/php-codegen": "workspace:*",
"@types/lodash-es": "catalog:",
Expand Down
62 changes: 38 additions & 24 deletions generators/php/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,24 +211,7 @@ export class EndpointSnippetGenerator {
if (snippet.auth != null) {
authArgs.push(...this.getConstructorAuthArgs({ auth: endpoint.auth, values: snippet.auth }));
} else {
// Provide default auth values for endpoints that require authentication
if (endpoint.auth.type === "inferred") {
// For inferred auth, provide default test values
const defaultInferredAuthValues: FernIr.dynamic.InferredAuthValues = {
values: undefined
};
authArgs.push(
...this.getConstructorInferredAuthArgs({
auth: endpoint.auth,
values: defaultInferredAuthValues
})
);
} else {
this.context.errors.add({
severity: Severity.Warning,
message: `Auth with ${endpoint.auth.type} configuration is required for this endpoint`
});
}
authArgs.push(...this.getDefaultAuthArgs({ auth: endpoint.auth }));
}
}

Expand Down Expand Up @@ -306,6 +289,41 @@ export class EndpointSnippetGenerator {
}
}

private getDefaultAuthArgs({ auth }: { auth: FernIr.dynamic.Auth }): NamedArgument[] {
switch (auth.type) {
case "bearer":
return this.getConstructorBearerAuthArgs({
auth,
values: { token: "YOUR_TOKEN" }
});
case "oauth":
return this.getConstructorOAuthArgs({
auth,
values: { clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET" }
});
case "basic":
return this.getConstructorBasicAuthArgs({
auth,
values: {
username: "YOUR_USERNAME",
password: "YOUR_PASSWORD"
}
});
case "header":
return this.getConstructorHeaderAuthArgs({
auth,
values: { value: "YOUR_AUTH_TOKEN" }
});
case "inferred":
return this.getConstructorInferredAuthArgs({
auth,
values: { values: undefined }
});
default:
assertNever(auth);
}
}

private addError(message: string): void {
this.context.errors.add({ severity: Severity.Critical, message });
}
Expand All @@ -321,18 +339,14 @@ export class EndpointSnippetGenerator {
auth: FernIr.dynamic.BasicAuth;
values: FernIr.dynamic.BasicAuthValues;
}): NamedArgument[] {
// usernameOmit/passwordOmit may exist in newer IR versions
const authRecord = auth as unknown as Record<string, unknown>;
const usernameOmitted = !!authRecord.usernameOmit;
const passwordOmitted = !!authRecord.passwordOmit;
const args: NamedArgument[] = [];
if (!usernameOmitted) {
if (!auth.usernameOmit) {
args.push({
name: this.context.getPropertyName(auth.username),
assignment: php.TypeLiteral.string(values.username)
});
}
if (!passwordOmitted) {
if (!auth.passwordOmit) {
args.push({
name: this.context.getPropertyName(auth.password),
assignment: php.TypeLiteral.string(values.password)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,139 @@
import { DynamicSnippetsTestRunner } from "@fern-api/browser-compatible-base-generator";
import { AbsoluteFilePath } from "@fern-api/path-utils";

import { buildDynamicSnippetsGenerator } from "./utils/buildDynamicSnippetsGenerator.js";
import { buildGeneratorConfig } from "./utils/buildGeneratorConfig.js";

const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of(
`${__dirname}/../../../../../packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions`
);

describe("snippets (default)", () => {
const runner = new DynamicSnippetsTestRunner();
runner.runTests({
buildGenerator: ({ irFilepath }) =>
buildDynamicSnippetsGenerator({ irFilepath, config: buildGeneratorConfig() })
});
});

describe("snippets (missing auth placeholders)", () => {
it("bearer auth with no auth in request", async () => {
const generator = buildDynamicSnippetsGenerator({
irFilepath: AbsoluteFilePath.of(`${DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY}/examples.json`),
config: buildGeneratorConfig()
});
const response = await generator.generate({
endpoint: {
method: "GET",
path: "/metadata"
},
baseURL: undefined,
environment: undefined,
auth: undefined,
pathParameters: undefined,
queryParameters: {
shallow: false,
tag: "development"
},
headers: {
"X-API-Version": "0.0.1"
},
requestBody: undefined
});
expect(response.snippet).toContain("YOUR_TOKEN");
expect(response.snippet).toMatchSnapshot();
});

it("oauth auth with no auth in request", async () => {
const generator = buildDynamicSnippetsGenerator({
irFilepath: AbsoluteFilePath.of(
`${DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY}/oauth-client-credentials-default.json`
),
config: buildGeneratorConfig({ customConfig: { namespace: "Seed" } })
});
const response = await generator.generate({
endpoint: {
method: "GET",
path: "/get-something"
},
baseURL: "https://api.fern.com",
environment: undefined,
auth: undefined,
pathParameters: undefined,
queryParameters: undefined,
headers: undefined,
requestBody: undefined
});
expect(response.snippet).toContain("YOUR_CLIENT_ID");
expect(response.snippet).toContain("YOUR_CLIENT_SECRET");
expect(response.snippet).toMatchSnapshot();
});

it("basic auth with no auth in request", async () => {
const generator = buildDynamicSnippetsGenerator({
irFilepath: AbsoluteFilePath.of(`${DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY}/basic-auth.json`),
config: buildGeneratorConfig()
});
const response = await generator.generate({
endpoint: {
method: "GET",
path: "/basic-auth"
},
baseURL: undefined,
environment: undefined,
auth: undefined,
pathParameters: undefined,
queryParameters: undefined,
headers: undefined,
requestBody: undefined
});
expect(response.snippet).toContain("YOUR_USERNAME");
expect(response.snippet).toContain("YOUR_PASSWORD");
expect(response.snippet).toMatchSnapshot();
});

it("basic auth with passwordOmit flag", async () => {
const generator = buildDynamicSnippetsGenerator({
irFilepath: AbsoluteFilePath.of(`${DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY}/basic-auth-pw-omitted.json`),
config: buildGeneratorConfig()
});
const response = await generator.generate({
endpoint: {
method: "GET",
path: "/basic-auth"
},
baseURL: undefined,
environment: undefined,
auth: undefined,
pathParameters: undefined,
queryParameters: undefined,
headers: undefined,
requestBody: undefined
});
expect(response.snippet).toContain("YOUR_USERNAME");
expect(response.snippet).not.toContain("YOUR_PASSWORD");
expect(response.snippet).toMatchSnapshot();
});

it("header auth with no auth in request", async () => {
const generator = buildDynamicSnippetsGenerator({
irFilepath: AbsoluteFilePath.of(`${DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY}/header-auth.json`),
config: buildGeneratorConfig()
});
const response = await generator.generate({
endpoint: {
method: "GET",
path: "/apiKey"
},
baseURL: undefined,
environment: undefined,
auth: undefined,
pathParameters: undefined,
queryParameters: undefined,
headers: undefined,
requestBody: undefined
});
expect(response.snippet).toContain("YOUR_AUTH_TOKEN");
expect(response.snippet).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -739,3 +739,87 @@ exports[`snippets (default) > single-url-environment-default > 'invalid environm
}
]"
`;

exports[`snippets (missing auth placeholders) > basic auth with no auth in request 1`] = `
"<?php

namespace Example;

use Acme\\AcmeClient;

$client = new AcmeClient(
username: 'YOUR_USERNAME',
password: 'YOUR_PASSWORD',
);
$client->basicAuth->getWithBasicAuth();
"
`;

exports[`snippets (missing auth placeholders) > basic auth with passwordOmit flag 1`] = `
"<?php

namespace Example;

use Acme\\AcmeClient;

$client = new AcmeClient(
username: 'YOUR_USERNAME',
);
$client->basicAuth->getWithBasicAuth();
"
`;

exports[`snippets (missing auth placeholders) > bearer auth with no auth in request 1`] = `
"<?php

namespace Example;

use Acme\\AcmeClient;
use Acme\\Service\\Requests\\GetMetadataRequest;

$client = new AcmeClient(
token: 'YOUR_TOKEN',
);
$client->service->getMetadata(
new GetMetadataRequest([
'shallow' => false,
'tag' => [
'development',
],
'xAPIVersion' => '0.0.1',
]),
);
"
`;

exports[`snippets (missing auth placeholders) > header auth with no auth in request 1`] = `
"<?php

namespace Example;

use Acme\\AcmeClient;

$client = new AcmeClient(
headerTokenAuth: 'YOUR_AUTH_TOKEN',
);
$client->service->getWithBearerToken();
"
`;

exports[`snippets (missing auth placeholders) > oauth auth with no auth in request 1`] = `
"<?php

namespace Example;

use Seed\\AcmeClient;

$client = new AcmeClient(
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
options: [
'baseUrl' => 'https://api.fern.com',
],
);
$client->simple->getSomething();
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Fix dynamic snippet generator to emit placeholder auth values when snippet
requests omit auth for endpoints that require bearer, OAuth, basic, or header
authentication. Previously the generated constructor call was missing the
required auth parameter, causing phpstan failures.
type: fix
10 changes: 10 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.5.2
changelogEntry:
- summary: |
Fix dynamic snippet generator to emit placeholder auth values when snippet
requests omit auth for endpoints that require bearer, OAuth, basic, or header
authentication. Previously the generated constructor call was missing the
required auth parameter, causing phpstan failures.
type: fix
createdAt: "2026-04-25"
irVersion: 66
- version: 2.5.1
changelogEntry:
- summary: |
Expand Down
Loading
Loading