Skip to content

Commit 1680d28

Browse files
authored
fix(php): honor OpenAPI default beside $ref in toDefaultValue (#23541)
* fix(php): honor OpenAPI default beside $ref in toDefaultValue Parameter/model schemas that combine $ref with a sibling default were not handled by the boolean/number/string branches, so CodegenParameter.defaultValue stayed empty and php-symfony emitted query->get without the second argument. Add a fallback: when getDefault() is set, emit a PHP literal (quoted strings via escapeTextInSingleQuotes, other types via toString()). Update PhpSymfonyServerCodegenTest: assert optional enum-ref query defaults, and relax petstore interface assertions when a default is present (non-nullable signature per api.mustache). * fix(php): safe OpenAPI default literals for $ref schemas; tighten php-symfony tests AbstractPhpCodegen maps getDefault() to valid PHP via defaultValueToPhpLiteral instead of blind toString(). PhpSymfonyServerCodegenTest asserts non-nullable handler params when an enum $ref has an OpenAPI default (petstore + optional spec).
1 parent c07f3a0 commit 1680d28

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPhpCodegen.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.slf4j.LoggerFactory;
3434

3535
import java.io.File;
36+
import java.math.BigDecimal;
3637
import java.util.*;
3738
import java.util.regex.Matcher;
3839
import java.util.regex.Pattern;
@@ -636,6 +637,48 @@ public String toDefaultValue(Schema p) {
636637
}
637638
}
638639

640+
// OAS 3.x: `default` may appear alongside `$ref` on the same schema (e.g. optional query param whose schema
641+
// references an enum model). That wrapper is often not classified as string/number here, but still carries
642+
// the default OpenAPI value — needed so Mustache can emit `query->get(..., <default>)` for php-symfony.
643+
if (p.getDefault() != null) {
644+
return defaultValueToPhpLiteral(p.getDefault());
645+
}
646+
647+
return null;
648+
}
649+
650+
/**
651+
* Converts a JSON Schema {@code default} value to a PHP expression suitable for templates (e.g. second argument to
652+
* {@code query->get}). Only safe scalar literals are supported; unknown types log a warning and yield {@code null}
653+
* so we do not emit broken PHP from {@code Object#toString()}.
654+
*/
655+
private String defaultValueToPhpLiteral(Object def) {
656+
if (def == null) {
657+
return null;
658+
}
659+
if (def instanceof String) {
660+
return "'" + escapeTextInSingleQuotes((String) def) + "'";
661+
}
662+
if (def instanceof Boolean) {
663+
return Boolean.TRUE.equals(def) ? "true" : "false";
664+
}
665+
if (def instanceof BigDecimal) {
666+
return ((BigDecimal) def).toPlainString();
667+
}
668+
if (def instanceof Number) {
669+
String s = def.toString();
670+
if (s.contains("Infinity") || s.contains("NaN")) {
671+
LOGGER.warn("Unsupported numeric default for PHP literal: {}", def);
672+
return null;
673+
}
674+
return s;
675+
}
676+
if (def instanceof Character) {
677+
return "'" + escapeTextInSingleQuotes(String.valueOf((Character) def)) + "'";
678+
}
679+
LOGGER.warn(
680+
"Cannot convert OpenAPI default of type {} to a PHP literal; omitting defaultValue",
681+
def.getClass().getName());
639682
return null;
640683
}
641684

modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpSymfonyServerCodegenTest.java

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,20 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface(
216216
Assert.assertTrue(
217217
apiContent.contains("use Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus;"),
218218
"Expected enum model import");
219+
// This spec sets default: available on the enum $ref; the handler must be non-nullable (no leading "?" /
220+
// "|null") because the controller always supplies a value after applying the OpenAPI default.
219221
Assert.assertTrue(
220-
apiContent.contains("?PetModelPetStatus $status"),
221-
"Expected enum ref query param to use short class in type hint");
222+
Pattern.compile("public function listPets\\(\\s*PetModelPetStatus\\s+\\$status,").matcher(apiContent).find(),
223+
"Expected defaulted enum-ref query param to use short non-nullable class in type hint");
224+
Assert.assertFalse(
225+
Pattern.compile("public function listPets\\(\\s*\\?PetModelPetStatus\\s+\\$status").matcher(apiContent).find(),
226+
"Defaulted enum-ref query param must not use nullable type hint (?PetModelPetStatus)");
222227
Assert.assertTrue(
228+
Pattern.compile("@param\\s+PetModelPetStatus\\s+\\$status\\b").matcher(apiContent).find(),
229+
"PHPDoc @param should use short PetModelPetStatus without |null when OpenAPI default is set");
230+
Assert.assertFalse(
223231
Pattern.compile("@param\\s+PetModelPetStatus\\|null\\s+\\$status\\b").matcher(apiContent).find(),
224-
"PHPDoc @param should use short PetModelPetStatus|null (consistent with use import)");
232+
"PHPDoc must not document |null for enum ref when OpenAPI default is set");
225233
Assert.assertFalse(
226234
apiContent.contains("?\\Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus $status"),
227235
"Signature must not use leading-backslash FQCN when a matching use import exists");
@@ -234,6 +242,92 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface(
234242
output.deleteOnExit();
235243
}
236244

245+
/**
246+
* Optional {@code in: query} parameter: {@code required: false}, schema is an enum {@code $ref} with a valid
247+
* {@code default} (see OpenAPI 3.x). Omitting the query key must be equivalent to sending that default; the
248+
* generated controller must not reject the request in validation solely because the value was absent.
249+
* <p>
250+
* Spec: {@code src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml}. Product doc:
251+
* {@code php-symfony.md} section &quot;可选 query:带默认值的非必填枚举 {@code $ref} 缺省却仍被拒绝&quot;.
252+
* <p>
253+
* Expected generated behavior (any one is acceptable):
254+
* <ul>
255+
* <li>Pass the OpenAPI default into {@code Request::query->get} for {@code tone}, and/or</li>
256+
* <li>Apply the Elvis default line ({@code $tone = $tone?:...}) after the read (see {@code api_controller.mustache}), and/or</li>
257+
* <li>Wrap enum {@code Assert\\Type} in {@code Assert\\Optional} for non-required enum refs (see {@code api_input_validation.mustache}).</li>
258+
* </ul>
259+
* Also asserts the integer optional {@code limit} parameter still receives {@code get('limit', 10)} as a control.
260+
* <p>
261+
* <b>Note:</b> This test fails on the generator until optional enum-ref query parameters expose
262+
* {@link org.openapitools.codegen.CodegenParameter#defaultValue} (or equivalent) so templates apply the OpenAPI
263+
* default and/or skip strict {@code Assert\\Type} on {@code null}. It is intended to lock the fix described in the
264+
* php-symfony troubleshooting doc.
265+
*/
266+
@Test
267+
public void testOptionalEnumRefQueryParameterWithDefaultAppliesOpenApiSemantics() throws Exception {
268+
Map<String, Object> properties = new HashMap<>();
269+
properties.put("invokerPackage", "Org\\OpenAPITools\\FeedHints");
270+
properties.put(AbstractPhpCodegen.SRC_BASE_PATH, "src");
271+
272+
File output = Files.createTempDirectory("test").toFile();
273+
274+
final CodegenConfigurator configurator = new CodegenConfigurator()
275+
.setGeneratorName("php-symfony")
276+
.setAdditionalProperties(properties)
277+
.setInputSpec("src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml")
278+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
279+
280+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
281+
DefaultGenerator generator = new DefaultGenerator();
282+
List<File> files = generator.opts(clientOptInput).generate();
283+
284+
File apiInterfaceFile = files.stream()
285+
.filter(f -> "DefaultApiInterface.php".equals(f.getName()) && f.getPath().contains("Api" + File.separator))
286+
.findFirst()
287+
.orElseThrow(() -> new AssertionError("DefaultApiInterface.php not generated"));
288+
String apiContent = Files.readString(apiInterfaceFile.toPath(), StandardCharsets.UTF_8);
289+
Assert.assertTrue(
290+
Pattern.compile("public function listFeedHints\\(\\s*PetAnnouncementTone\\s+\\$tone,").matcher(apiContent).find(),
291+
"Expected defaulted enum-ref query param to use short non-nullable class in API interface type hint");
292+
Assert.assertFalse(
293+
Pattern.compile("public function listFeedHints\\(\\s*\\?PetAnnouncementTone\\s+\\$tone").matcher(apiContent).find(),
294+
"Defaulted enum-ref query param must not use nullable type hint (?PetAnnouncementTone)");
295+
Assert.assertTrue(
296+
Pattern.compile("@param\\s+PetAnnouncementTone\\s+\\$tone\\b").matcher(apiContent).find(),
297+
"PHPDoc @param should use PetAnnouncementTone without |null when OpenAPI default is set");
298+
Assert.assertFalse(
299+
Pattern.compile("@param\\s+PetAnnouncementTone\\|null\\s+\\$tone\\b").matcher(apiContent).find(),
300+
"PHPDoc must not document |null for enum ref when OpenAPI default is set");
301+
assertGeneratedPhpSyntaxValid(apiInterfaceFile);
302+
303+
File controllerFile = files.stream()
304+
.filter(f -> "DefaultController.php".equals(f.getName()) && f.getPath().contains("Controller" + File.separator))
305+
.findFirst()
306+
.orElseThrow(() -> new AssertionError("DefaultController.php not generated"));
307+
308+
String controller = Files.readString(controllerFile.toPath(), StandardCharsets.UTF_8);
309+
310+
Assert.assertTrue(
311+
controller.contains("$request->query->get('limit', 10)"),
312+
"Integer optional query with default should pass default as second argument to query->get (control case)");
313+
314+
boolean defaultInGet = Pattern.compile("\\$request->query->get\\('tone',\\s*").matcher(controller).find();
315+
boolean elvisDefault = Pattern.compile("\\$tone\\s*=\\s*\\$tone\\?:").matcher(controller).find();
316+
boolean optionalEnumTypeAssert =
317+
controller.contains("new Assert\\Optional(")
318+
&& controller.contains("PetAnnouncementTone");
319+
320+
Assert.assertTrue(
321+
defaultInGet || elvisDefault || optionalEnumTypeAssert,
322+
"Omitted optional enum-ref query with OpenAPI default must apply default (get/Elvis) and/or use "
323+
+ "Assert\\Optional around enum Type so null is valid before default is applied; "
324+
+ "see optional-enum-query-ref-default.yaml and php-symfony troubleshooting doc");
325+
326+
assertGeneratedPhpSyntaxValid(controllerFile);
327+
328+
output.deleteOnExit();
329+
}
330+
237331
/**
238332
* Runs {@code php -l} on the file. Skips if {@code php} is not available (optional toolchain).
239333
*/
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Minimal OpenAPI 3.1 spec: optional query with enum $ref + default (components.parameters).
2+
# Repro pattern: omitting the query key should behave like the declared default; php-symfony
3+
# may still validate null as enum type before business logic. See project troubleshooting doc.
4+
openapi: 3.1.0
5+
info:
6+
title: Optional enum query ref with default (php-symfony repro)
7+
version: '1.0'
8+
paths:
9+
/pets/feed-hints:
10+
get:
11+
operationId: listFeedHints
12+
parameters:
13+
- $ref: '#/components/parameters/ToneQuery'
14+
- name: limit
15+
in: query
16+
required: false
17+
description: Integer optional query with default (often generated correctly).
18+
schema:
19+
type: integer
20+
format: int32
21+
default: 10
22+
minimum: 1
23+
maximum: 50
24+
responses:
25+
'200':
26+
description: OK
27+
components:
28+
parameters:
29+
ToneQuery:
30+
name: tone
31+
in: query
32+
required: false
33+
description: Optional filter; default applies when the query key is omitted.
34+
schema:
35+
$ref: '#/components/schemas/PetAnnouncementTone'
36+
default: friendly
37+
schemas:
38+
PetAnnouncementTone:
39+
type: string
40+
enum:
41+
- friendly
42+
- formal

0 commit comments

Comments
 (0)