fix(php): rename spec parameters only where they collide with generated local variables

KH
4d ago
+1282-44

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=guzzle in the tests);
  • xxxRequest() is the generated method that builds the HTTP request for operation xxx, and xxxAsyncWithHttpInfo() is the asynchronous variant that forwards its arguments to it;
  • with the x-group-parameters vendor extension, an operation takes one $associative_array argument whose entries are unpacked into local variables;
  • an exploded query array is a style: form, explode: true query 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 parameterWhen it breaksWhat master generates
thisalwaysfunction useThis($this, …) — $this cannot be used as a parameter; PHP rejects the file
multipartalways$multipart = false; runs at the top of xxxRequest(), before the caller's value is read
headersas the request bodyxxxRequest() 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
variablesdefault library, operation with serverswithout 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, valuepsr-18, after an exploded query arraythe 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 nameWhen it breaks
resourcePath, formParams, queryParams, headerParams, httpBodyalways (initialized at the top of xxxRequest())
returnTypewithout x-group-parameters (assigned in xxxAsyncWithHttpInfo() before the arguments are forwarded)
contentTypedefault 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
hostIndexdefault library, operation with servers (same two forms as variables above)
formDataProcessoras a form field ($formDataProcessor = new FormDataProcessor() runs before the field is read)
associative_arraywith 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

  • PhpClientCodegen renames 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's servers, whether x-group-parameters applies, and the parameter order. The new name is param_<original name> passed through the naming convention again ($param_this under snake_case, $paramThis under camelCase). 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 parameterNameMappings is 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 named key or value no longer need renaming. A spec parameter whose converted name is localVariableKey or localVariableValue is renamed in psr-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 parameter headers, or variables in an operation without servers). This follows #25581 (java okhttp-gson), which also renamed only names that collide.
  • Out of scope: the php-nextgen generator 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
testInternalVariableNamesAreRenameddefault library, snake_case: the breaking cases are renamed; the same names in harmless positions keep their names
testInternalVariableNamesAreRenamedCamelCasethe same under camelCase, including contentType, hostIndex/variables (only with servers), formDataProcessor (only as a form field) and returnType (checked on xxxAsyncWithHttpInfo())
testInternalVariableNamesAreRenamedOriginalthe same under original
testInternalVariableParameterNameMappingsWina name set through parameterNameMappings is not renamed
testPsr18InternalVariableNamesAreRenamedpsr-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
testPsr18InternalVariableNamesAreRenamedCamelCasepsr-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 PR13 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.php and lib/Api/UserApi.php change; 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 regenerated php-OpenAPIClient and php-psr-18 with ./bin/generate-samples.sh; the resulting diff is committed. export_docs_generators.sh not run: no generator option or metadata changes.
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    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-parameters array key; wire names are unchanged.
  • Names set via parameterNameMappings always win and are never renamed.