fix(php): rename spec parameters only where they collide with generated local variables
The php client generator emits each spec parameter as a PHP variable in the same function scope as the variables its templates declare ($resourcePath, $multipart, $headers, …). In some situations a parameter with one of those names produces a file PHP refuses to compile, or the caller's value is replaced before it is used. This PR renames a parameter to param_<name> only in the situations where the generated code breaks, and fixes one psr-18 case in the template instead.
Terms used below:
- the default library is
php's default HTTP layer (Guzzle;library=guzzlein the tests); xxxRequest()is the generated method that builds the HTTP request for operationxxx, andxxxAsyncWithHttpInfo()is the asynchronous variant that forwards its arguments to it;- with the
x-group-parametersvendor extension, an operation takes one$associative_arrayargument whose entries are unpacked into local variables; - an exploded query array is a
style: form,explode: truequery parameter whose entries are sent as separate keys.
Reproducer
modules/openapi-generator/src/test/resources/3_0/php/php-internal-var-collision.yaml and php-psr18-internal-var-collision.yaml (added by this PR) contain one operation per case. On master, regardless of variableNamingConvention:
| Spec parameter | When it breaks | What master generates |
|---|---|---|
this | always | function useThis($this, …) — $this cannot be used as a parameter; PHP rejects the file |
multipart | always | $multipart = false; runs at the top of xxxRequest(), before the caller's value is read |
headers | as the request body | xxxRequest() sets $headers = $this->headerSelector->selectHeaders(…) and then uses isset($headers) to pick the body, so the header array is sent instead of the caller's body |
variables | default library, operation with servers | without x-group-parameters, it duplicates the appended array $variables parameter and PHP rejects the file; with x-group-parameters, the template reads the same $associative_array['variables'] entry for the server variables, so one value is used for two purposes |
key, value | psr-18, after an exploded query array | the expansion loop foreach (… as $key => $value) overwrites them |
Other names collide only when the converted parameter name equals the template variable, which depends on variableNamingConvention. Under the default snake_case, a spec parameter resourcePath becomes $resource_path and does not collide; under camelCase or original it does. The reverse holds for associative_array, which keeps its name under snake_case and original but becomes $associativeArray under camelCase:
| Converted name | When it breaks |
|---|---|
resourcePath, formParams, queryParams, headerParams, httpBody | always (initialized at the top of xxxRequest()) |
returnType | without x-group-parameters (assigned in xxxAsyncWithHttpInfo() before the arguments are forwarded) |
contentType | default library: without x-group-parameters it duplicates the string $contentType parameter appended to every operation; with it, the template reads the same $associative_array['contentType'] entry as the request's content type |
hostIndex | default library, operation with servers (same two forms as variables above) |
formDataProcessor | as a form field ($formDataProcessor = new FormDataProcessor() runs before the field is read) |
associative_array | with x-group-parameters: in the default library the template reads $associative_array again after unpacking; in psr-18 only when it is not the last unpacked parameter |
Fix
PhpClientCodegenrenames a parameter only in the situations listed above. It decides per operation, after the operation has been built, so it can check the parameter's position, the operation'sservers, whetherx-group-parametersapplies, and the parameter order. The new name isparam_<original name>passed through the naming convention again ($param_thisundersnake_case,$paramThisundercamelCase). The same name is applied to every copy of the parameter in the operation's parameter lists, so the signature and the method body agree. If the new name is already used by another parameter of the operation, a number is appended.- A name set explicitly through
parameterNameMappingsis kept as the user wrote it. php/libraries/psr-18/api.mustache: the variables of the exploded-query loop are renamed to$localVariableKey/$localVariableValue, so spec parameters namedkeyorvalueno longer need renaming. A spec parameter whose converted name islocalVariableKeyorlocalVariableValueis renamed inpsr-18.- Not renamed, because the templates assign them only after the caller's value has been used:
query,request,options,response,data,content,apiKey, and the names above in positions not listed (for example a query parameterheaders, orvariablesin an operation withoutservers). This follows #25581 (javaokhttp-gson), which also renamed only names that collide. - Out of scope: the
php-nextgengenerator and the other PHP generators, which have their own templates.
Behavior change
Only parameters in the situations above are renamed. For those parameters, the PHP argument name (used by PHP 8 named arguments) and the x-group-parameters array key become the new name. In every case except the x-group-parameters forms of contentType, hostIndex and variables, master produced a client that does not compile or loses the caller's value. In those three forms the old key was read twice, once for the parameter and once by the template, so one value served two purposes. After this PR the two get separate keys.
Tests
Each test generates the fixtures and compares the full argument list of the generated methods as whole strings.
| Test (new) | Checks |
|---|---|
testInternalVariableNamesAreRenamed | default library, snake_case: the breaking cases are renamed; the same names in harmless positions keep their names |
testInternalVariableNamesAreRenamedCamelCase | the same under camelCase, including contentType, hostIndex/variables (only with servers), formDataProcessor (only as a form field) and returnType (checked on xxxAsyncWithHttpInfo()) |
testInternalVariableNamesAreRenamedOriginal | the same under original |
testInternalVariableParameterNameMappingsWin | a name set through parameterNameMappings is not renamed |
testPsr18InternalVariableNamesAreRenamed | psr-18, snake_case: the shared cases are renamed; key/value keep their names next to an exploded query array; associative_array is renamed when it is not the last unpacked parameter and kept when it is |
testPsr18InternalVariableNamesAreRenamedCamelCase | psr-18, camelCase: localVariableKey/localVariableValue after an exploded query array are renamed; the default-library-only names (contentType, hostIndex, variables) keep their names |
./mvnw -nsu -Denforcer.skip=true -pl modules/openapi-generator -am -Dtest=PhpClientCodegenTest -Dsurefire.failIfNoSpecifiedTests=false test
| Result | |
|---|---|
| master + this PR | 13 tests, 0 failures, 0 skipped |
this PR with the PhpClientCodegen.java and psr-18/api.mustache changes reverted (tests kept) | 5 failures — all new tests except testInternalVariableParameterNameMappingsWin, which also passes on master and guards against renaming names the user mapped explicitly |
Samples
php-psr-18:lib/Api/FakeApi.phpandlib/Api/UserApi.phpchange; each change only renames the exploded-query loop variables to$localVariableKey/$localVariableValue.php-OpenAPIClient: no change; the petstore spec has none of the cases above.
These are the only two configs with generatorName: php.
cc @wing328 and the PHP technical committee: @jebentier @dkarlovi @mandrean @jfastnacht @ybelenko @renepardon
PR checklist
- Read the contribution guidelines.
- Run the following to build the project and update samples — scoped to this change: built the CLI (
./mvnw -pl modules/openapi-generator-cli -am package -DskipTests) and regeneratedphp-OpenAPIClientandphp-psr-18with./bin/generate-samples.sh; the resulting diff is committed.export_docs_generators.shnot run: no generator option or metadata changes.
(For Windows users, please run the script in WSL)./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example./bin/generate-samples.sh bin/configs/java*.
IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed. - If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
🤖 Generated with Claude Code
Summary by cubic
Fixes the PHP client generator emitting invalid or broken clients when a spec parameter's name collides with a local variable in the generated request methods. Colliding parameters are now renamed to param_<name> (converted through the configured naming convention, numbered when taken) only in the positions where the generated code would fail to compile or overwrite the caller's value; harmless names keep theirs. The psr-18 template's exploded-query loop now uses $localVariableKey/$localVariableValue, so spec params named key/value keep their names, and psr-18's own loop-variable collisions are handled in the generator.
Behavior change
- Renamed parameters change the PHP argument name (relevant for PHP 8 named arguments) and the
x-group-parametersarray key; wire names are unchanged. - Names set via
parameterNameMappingsalways win and are never renamed.