Skip to content
Merged
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 @@ -33,6 +33,7 @@
import org.slf4j.LoggerFactory;

import java.io.File;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -636,6 +637,48 @@ public String toDefaultValue(Schema p) {
}
}

// OAS 3.x: `default` may appear alongside `$ref` on the same schema (e.g. optional query param whose schema
// references an enum model). That wrapper is often not classified as string/number here, but still carries
// the default OpenAPI value — needed so Mustache can emit `query->get(..., <default>)` for php-symfony.
if (p.getDefault() != null) {
return defaultValueToPhpLiteral(p.getDefault());
}

return null;
}

/**
* Converts a JSON Schema {@code default} value to a PHP expression suitable for templates (e.g. second argument to
* {@code query->get}). Only safe scalar literals are supported; unknown types log a warning and yield {@code null}
* so we do not emit broken PHP from {@code Object#toString()}.
*/
private String defaultValueToPhpLiteral(Object def) {
if (def == null) {
return null;
}
if (def instanceof String) {
return "'" + escapeTextInSingleQuotes((String) def) + "'";
}
if (def instanceof Boolean) {
return Boolean.TRUE.equals(def) ? "true" : "false";
}
if (def instanceof BigDecimal) {
return ((BigDecimal) def).toPlainString();
}
if (def instanceof Number) {
String s = def.toString();
if (s.contains("Infinity") || s.contains("NaN")) {
LOGGER.warn("Unsupported numeric default for PHP literal: {}", def);
return null;
}
return s;
}
if (def instanceof Character) {
return "'" + escapeTextInSingleQuotes(String.valueOf((Character) def)) + "'";
}
LOGGER.warn(
"Cannot convert OpenAPI default of type {} to a PHP literal; omitting defaultValue",
def.getClass().getName());
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,20 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface(
Assert.assertTrue(
apiContent.contains("use Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus;"),
"Expected enum model import");
// This spec sets default: available on the enum $ref; the handler must be non-nullable (no leading "?" /
// "|null") because the controller always supplies a value after applying the OpenAPI default.
Assert.assertTrue(
apiContent.contains("?PetModelPetStatus $status"),
"Expected enum ref query param to use short class in type hint");
Pattern.compile("public function listPets\\(\\s*PetModelPetStatus\\s+\\$status,").matcher(apiContent).find(),
"Expected defaulted enum-ref query param to use short non-nullable class in type hint");
Assert.assertFalse(
Pattern.compile("public function listPets\\(\\s*\\?PetModelPetStatus\\s+\\$status").matcher(apiContent).find(),
"Defaulted enum-ref query param must not use nullable type hint (?PetModelPetStatus)");
Assert.assertTrue(
Pattern.compile("@param\\s+PetModelPetStatus\\s+\\$status\\b").matcher(apiContent).find(),
"PHPDoc @param should use short PetModelPetStatus without |null when OpenAPI default is set");
Assert.assertFalse(
Pattern.compile("@param\\s+PetModelPetStatus\\|null\\s+\\$status\\b").matcher(apiContent).find(),
"PHPDoc @param should use short PetModelPetStatus|null (consistent with use import)");
"PHPDoc must not document |null for enum ref when OpenAPI default is set");
Assert.assertFalse(
apiContent.contains("?\\Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus $status"),
"Signature must not use leading-backslash FQCN when a matching use import exists");
Expand All @@ -234,6 +242,92 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface(
output.deleteOnExit();
}

/**
* Optional {@code in: query} parameter: {@code required: false}, schema is an enum {@code $ref} with a valid
* {@code default} (see OpenAPI 3.x). Omitting the query key must be equivalent to sending that default; the
* generated controller must not reject the request in validation solely because the value was absent.
* <p>
* Spec: {@code src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml}. Product doc:
* {@code php-symfony.md} section &quot;可选 query:带默认值的非必填枚举 {@code $ref} 缺省却仍被拒绝&quot;.
* <p>
* Expected generated behavior (any one is acceptable):
* <ul>
* <li>Pass the OpenAPI default into {@code Request::query->get} for {@code tone}, and/or</li>
* <li>Apply the Elvis default line ({@code $tone = $tone?:...}) after the read (see {@code api_controller.mustache}), and/or</li>
* <li>Wrap enum {@code Assert\\Type} in {@code Assert\\Optional} for non-required enum refs (see {@code api_input_validation.mustache}).</li>
* </ul>
* Also asserts the integer optional {@code limit} parameter still receives {@code get('limit', 10)} as a control.
* <p>
* <b>Note:</b> This test fails on the generator until optional enum-ref query parameters expose
* {@link org.openapitools.codegen.CodegenParameter#defaultValue} (or equivalent) so templates apply the OpenAPI
* default and/or skip strict {@code Assert\\Type} on {@code null}. It is intended to lock the fix described in the
* php-symfony troubleshooting doc.
*/
@Test
public void testOptionalEnumRefQueryParameterWithDefaultAppliesOpenApiSemantics() throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put("invokerPackage", "Org\\OpenAPITools\\FeedHints");
properties.put(AbstractPhpCodegen.SRC_BASE_PATH, "src");

File output = Files.createTempDirectory("test").toFile();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("php-symfony")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final ClientOptInput clientOptInput = configurator.toClientOptInput();
DefaultGenerator generator = new DefaultGenerator();
List<File> files = generator.opts(clientOptInput).generate();

File apiInterfaceFile = files.stream()
.filter(f -> "DefaultApiInterface.php".equals(f.getName()) && f.getPath().contains("Api" + File.separator))
.findFirst()
.orElseThrow(() -> new AssertionError("DefaultApiInterface.php not generated"));
String apiContent = Files.readString(apiInterfaceFile.toPath(), StandardCharsets.UTF_8);
Assert.assertTrue(
Pattern.compile("public function listFeedHints\\(\\s*PetAnnouncementTone\\s+\\$tone,").matcher(apiContent).find(),
"Expected defaulted enum-ref query param to use short non-nullable class in API interface type hint");
Assert.assertFalse(
Pattern.compile("public function listFeedHints\\(\\s*\\?PetAnnouncementTone\\s+\\$tone").matcher(apiContent).find(),
"Defaulted enum-ref query param must not use nullable type hint (?PetAnnouncementTone)");
Assert.assertTrue(
Pattern.compile("@param\\s+PetAnnouncementTone\\s+\\$tone\\b").matcher(apiContent).find(),
"PHPDoc @param should use PetAnnouncementTone without |null when OpenAPI default is set");
Assert.assertFalse(
Pattern.compile("@param\\s+PetAnnouncementTone\\|null\\s+\\$tone\\b").matcher(apiContent).find(),
"PHPDoc must not document |null for enum ref when OpenAPI default is set");
assertGeneratedPhpSyntaxValid(apiInterfaceFile);

File controllerFile = files.stream()
.filter(f -> "DefaultController.php".equals(f.getName()) && f.getPath().contains("Controller" + File.separator))
.findFirst()
.orElseThrow(() -> new AssertionError("DefaultController.php not generated"));

String controller = Files.readString(controllerFile.toPath(), StandardCharsets.UTF_8);

Assert.assertTrue(
controller.contains("$request->query->get('limit', 10)"),
"Integer optional query with default should pass default as second argument to query->get (control case)");

boolean defaultInGet = Pattern.compile("\\$request->query->get\\('tone',\\s*").matcher(controller).find();
boolean elvisDefault = Pattern.compile("\\$tone\\s*=\\s*\\$tone\\?:").matcher(controller).find();
boolean optionalEnumTypeAssert =
controller.contains("new Assert\\Optional(")
&& controller.contains("PetAnnouncementTone");

Assert.assertTrue(
defaultInGet || elvisDefault || optionalEnumTypeAssert,
"Omitted optional enum-ref query with OpenAPI default must apply default (get/Elvis) and/or use "
+ "Assert\\Optional around enum Type so null is valid before default is applied; "
+ "see optional-enum-query-ref-default.yaml and php-symfony troubleshooting doc");

assertGeneratedPhpSyntaxValid(controllerFile);

output.deleteOnExit();
}

/**
* Runs {@code php -l} on the file. Skips if {@code php} is not available (optional toolchain).
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Minimal OpenAPI 3.1 spec: optional query with enum $ref + default (components.parameters).
# Repro pattern: omitting the query key should behave like the declared default; php-symfony
# may still validate null as enum type before business logic. See project troubleshooting doc.
openapi: 3.1.0
info:
title: Optional enum query ref with default (php-symfony repro)
version: '1.0'
paths:
/pets/feed-hints:
get:
operationId: listFeedHints
parameters:
- $ref: '#/components/parameters/ToneQuery'
- name: limit
in: query
required: false
description: Integer optional query with default (often generated correctly).
schema:
type: integer
format: int32
default: 10
minimum: 1
maximum: 50
responses:
'200':
description: OK
components:
parameters:
ToneQuery:
name: tone
in: query
required: false
description: Optional filter; default applies when the query key is omitted.
schema:
$ref: '#/components/schemas/PetAnnouncementTone'
default: friendly
schemas:
PetAnnouncementTone:
type: string
enum:
- friendly
- formal
Loading