Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -566,11 +566,19 @@ export class EndpointSnippetGenerator extends WithGeneration {
const args: ast.Literal[] = [];

this.context.errors.scope(Scope.PathParameters);
const pathParameterFields: ast.ConstructorField[] = [];
const pathParameters = [...(this.context.ir.pathParameters ?? []), ...(request.pathParameters ?? [])];
if (pathParameters.length > 0) {
pathParameterFields.push(...this.getPathParameters({ namedParameters: pathParameters, snippet }));
}
const endpointPathParameters = request.pathParameters ?? [];
const rootPathParameters = this.context.ir.pathParameters ?? [];
// Process all path parameters together so associateByWireValue sees the
// full set and doesn't flag endpoint params as unrecognized.
const allNamedParameters = [...rootPathParameters, ...endpointPathParameters];
const allPathParameterFields = this.getPathParameters({ namedParameters: allNamedParameters, snippet });
// When there are no endpoint-specific path parameters, root-level path
// parameters may have default values (e.g. from x-fern-base-path). Place
// them after the request argument to match AbstractEndpointGenerator parameter ordering.
const moveRootAfterRequest = endpointPathParameters.length === 0 && rootPathParameters.length > 0;
// Split the fields back into root vs endpoint groups based on count.
const rootPathParameterFields = allPathParameterFields.slice(0, rootPathParameters.length);
const endpointPathParameterFields = allPathParameterFields.slice(rootPathParameters.length);
this.context.errors.unscope();

// TODO: Add support for file properties.
Expand All @@ -584,7 +592,11 @@ export class EndpointSnippetGenerator extends WithGeneration {
inlinePathParameters: this.settings.shouldInlinePathParameters
})
) {
args.push(...pathParameterFields.map((field) => field.value));
if (moveRootAfterRequest) {
args.push(...endpointPathParameterFields.map((field) => field.value));
} else {
args.push(...allPathParameterFields.map((field) => field.value));
}
}
// For now, the C# SDK always requires the inlined request parameter.
args.push(
Expand All @@ -595,11 +607,22 @@ export class EndpointSnippetGenerator extends WithGeneration {
request,
inlinePathParameters: this.settings.shouldInlinePathParameters
})
? pathParameterFields
? allPathParameterFields
: [],
filePropertyInfo
})
);

if (
moveRootAfterRequest &&
!this.context.includePathParametersInWrappedRequest({
request,
inlinePathParameters: this.settings.shouldInlinePathParameters
})
) {
args.push(...rootPathParameterFields.map((field) => field.value));
}

return args;
}

Expand Down Expand Up @@ -789,21 +812,36 @@ export class EndpointSnippetGenerator extends WithGeneration {
snippet: FernIr.dynamic.EndpointSnippetRequest;
}): ast.Literal[] {
const args: ast.Literal[] = [];

this.context.errors.scope(Scope.PathParameters);
const pathParameters = [...(this.context.ir.pathParameters ?? []), ...(request.pathParameters ?? [])];
if (pathParameters.length > 0) {
args.push(
...this.getPathParameters({ namedParameters: pathParameters, snippet }).map((field) => field.value)
);
}
const endpointPathParameters = request.pathParameters ?? [];
const rootPathParameters = this.context.ir.pathParameters ?? [];
// Process all path parameters together so associateByWireValue sees the
// full set and doesn't flag endpoint params as unrecognized.
const allNamedParameters = [...rootPathParameters, ...endpointPathParameters];
const allPathParamFields = this.getPathParameters({ namedParameters: allNamedParameters, snippet });
this.context.errors.unscope();

// When there are no endpoint-specific path parameters, root-level path
// parameters may have default values (e.g. from x-fern-base-path). Place
// them after the body argument to match AbstractEndpointGenerator parameter ordering.
const moveRootAfterBody = endpointPathParameters.length === 0 && rootPathParameters.length > 0;
if (moveRootAfterBody) {
// No endpoint params, so allPathParamFields are all root — skip them for now
} else {
args.push(...allPathParamFields.map((field) => field.value));
}

this.context.errors.scope(Scope.RequestBody);
if (request.body != null) {
args.push(this.getBodyRequestArg({ body: request.body, value: snippet.requestBody }));
}
this.context.errors.unscope();

if (moveRootAfterBody) {
args.push(...allPathParamFields.map((field) => field.value));
}

return args;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- summary: |
Fix dynamic snippet argument ordering to place body arguments before
defaulted path parameters, matching the generated method signature ordering.
type: fix
11 changes: 8 additions & 3 deletions generators/php/codegen/src/ast/Method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,14 @@ export class Method extends AstNode {
writer.write(`${this.access}${this.static_ ? " static" : ""} function ${this.name}(`);

// NOTE: Put all required parameters before all optional parameters
// since this is required by PHPStan
const requiredParameters = this.parameters.filter((param) => !param.type.isOptional());
const optionalParameters = this.parameters.filter((param) => param.type.isOptional());
// since this is required by PHPStan. Parameters with an initializer
// (default value) are also considered optional in PHP.
const requiredParameters = this.parameters.filter(
(param) => !param.type.isOptional() && param.initializer == null
);
const optionalParameters = this.parameters.filter(
(param) => param.type.isOptional() || param.initializer != null
);

const orderedParameters = [...requiredParameters, ...optionalParameters];

Expand Down
63 changes: 50 additions & 13 deletions generators/php/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,20 +818,35 @@ export class EndpointSnippetGenerator {
const args: php.TypeLiteral[] = [];

this.context.errors.scope(Scope.PathParameters);
const pathParameters = [...(this.context.ir.pathParameters ?? []), ...(request.pathParameters ?? [])];
if (pathParameters.length > 0) {
args.push(
...this.getPathParameters({ namedParameters: pathParameters, snippet }).map((field) => field.value)
);
}
const endpointPathParameters = request.pathParameters ?? [];
const rootPathParameters = this.context.ir.pathParameters ?? [];
// Process all path parameters together so associateByWireValue sees the
// full set and doesn't flag endpoint params as unrecognized.
const allNamedParameters = [...rootPathParameters, ...endpointPathParameters];
const allPathParamFields = this.getPathParameters({ namedParameters: allNamedParameters, snippet });
this.context.errors.unscope();

// When there are no endpoint-specific path parameters, root-level path
// parameters may have default values (e.g. from x-fern-base-path). Place
// them after the body argument to match Method.ts parameter ordering which
// moves parameters with initializers after required parameters.
const moveRootAfterBody = endpointPathParameters.length === 0 && rootPathParameters.length > 0;
if (moveRootAfterBody) {
// No endpoint params, so allPathParamFields are all root — skip them for now
} else {
args.push(...allPathParamFields.map((field) => field.value));
}

this.context.errors.scope(Scope.RequestBody);
if (request.body != null) {
args.push(this.getBodyRequestArg({ body: request.body, value: snippet.requestBody }));
}
this.context.errors.unscope();

if (moveRootAfterBody) {
args.push(...allPathParamFields.map((field) => field.value));
}

return args;
}

Expand Down Expand Up @@ -873,19 +888,33 @@ export class EndpointSnippetGenerator {
const inlinePathParameters = this.context.customConfig?.inlinePathParameters ?? false;

this.context.errors.scope(Scope.PathParameters);
const pathParameterFields: php.ConstructorField[] = [];
const pathParameters = [...(this.context.ir.pathParameters ?? []), ...(request.pathParameters ?? [])];
if (pathParameters.length > 0) {
pathParameterFields.push(...this.getPathParameters({ namedParameters: pathParameters, snippet }));
}
const endpointPathParameters = request.pathParameters ?? [];
const rootPathParameters = this.context.ir.pathParameters ?? [];
// Process all path parameters together so associateByWireValue sees the
// full set and doesn't flag endpoint params as unrecognized.
const allNamedParameters = [...rootPathParameters, ...endpointPathParameters];
const allPathParameterFields = this.getPathParameters({ namedParameters: allNamedParameters, snippet });
// When there are no endpoint-specific path parameters, root-level path
// parameters may have default values (e.g. from x-fern-base-path). Place
// them after the request argument to match Method.ts parameter ordering.
// When endpoint-specific path parameters exist, keep the original combined
// order (root first, then endpoint) before the request.
const moveRootAfterRequest = endpointPathParameters.length === 0 && rootPathParameters.length > 0;
// Split the fields back into root vs endpoint groups based on count.
const rootPathParameterFields = allPathParameterFields.slice(0, rootPathParameters.length);
const endpointPathParameterFields = allPathParameterFields.slice(rootPathParameters.length);
this.context.errors.unscope();

this.context.errors.scope(Scope.RequestBody);
const filePropertyInfo = this.getFilePropertyInfo({ request, snippet });
this.context.errors.unscope();

if (!this.context.includePathParametersInWrappedRequest({ request, inlinePathParameters })) {
args.push(...pathParameterFields.map((field) => field.value));
if (moveRootAfterRequest) {
args.push(...endpointPathParameterFields.map((field) => field.value));
} else {
args.push(...allPathParameterFields.map((field) => field.value));
}
}

if (
Expand All @@ -903,12 +932,20 @@ export class EndpointSnippetGenerator {
request,
inlinePathParameters
})
? pathParameterFields
? allPathParameterFields
: [],
filePropertyInfo
})
);
}

if (
moveRootAfterRequest &&
!this.context.includePathParametersInWrappedRequest({ request, inlinePathParameters })
) {
args.push(...rootPathParameterFields.map((field) => field.value));
}

return args;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- summary: |
Fix method parameter ordering to place parameters with default values after
required parameters, resolving PHPStan deprecation errors in PHP 8.0+.
type: fix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading