diff --git a/packages/quicktype-core/src/language/Php/PhpRenderer.ts b/packages/quicktype-core/src/language/Php/PhpRenderer.ts index 85d4ea4ee0..efd688a61b 100644 --- a/packages/quicktype-core/src/language/Php/PhpRenderer.ts +++ b/packages/quicktype-core/src/language/Php/PhpRenderer.ts @@ -1016,6 +1016,7 @@ export class PhpRenderer extends ConvenienceRenderer { t: Type, attrName: Sourcelike, scopeAttrName: string, + skipPrimitiveTypeCheck = false, ): void { const is = (isfn: string, myT: Name = className): void => { this.emitBlock(["if (!", isfn, "(", scopeAttrName, "))"], () => @@ -1038,30 +1039,38 @@ export class PhpRenderer extends ConvenienceRenderer { // non-string arguments.) }, (_nullType) => is("is_null"), - (_boolType) => is("is_bool"), - (_integerType) => is("is_integer"), + (_boolType) => { + if (!skipPrimitiveTypeCheck) is("is_bool"); + }, + (_integerType) => { + if (!skipPrimitiveTypeCheck) is("is_integer"); + }, (_doubleType) => { - // PHP integers are acceptable wherever floats are, and - // json_decode gives an int for a whole JSON number. - this.emitBlock( - [ - "if (!is_float(", - scopeAttrName, - ") && !is_int(", - scopeAttrName, - "))", - ], - () => - this.emitLine( - 'throw new Exception("Attribute Error:', - className, - "::", - attrName, - '");', - ), - ); + if (!skipPrimitiveTypeCheck) { + // PHP integers are acceptable wherever floats are, and + // json_decode gives an int for a whole JSON number. + this.emitBlock( + [ + "if (!is_float(", + scopeAttrName, + ") && !is_int(", + scopeAttrName, + "))", + ], + () => + this.emitLine( + 'throw new Exception("Attribute Error:', + className, + "::", + attrName, + '");', + ), + ); + } + }, + (_stringType) => { + if (!skipPrimitiveTypeCheck) is("is_string"); }, - (_stringType) => is("is_string"), (arrayType) => { is("is_array"); this.emitLine( @@ -1110,6 +1119,7 @@ export class PhpRenderer extends ConvenienceRenderer { nullable, attrName, scopeAttrName, + skipPrimitiveTypeCheck, ); }, ); @@ -1146,7 +1156,7 @@ export class PhpRenderer extends ConvenienceRenderer { } if (transformedStringType.kind === "uuid") { - is("is_string"); + if (!skipPrimitiveTypeCheck) is("is_string"); this.emitBlock( [ "if (!preg_match('/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/', ", @@ -1286,7 +1296,10 @@ export class PhpRenderer extends ConvenienceRenderer { " $value): bool", ], () => { - this.phpValidate(className, p.type, name, "$value"); + // PHP has already enforced scalar parameter type hints before + // entering this method. Recursive collection and union + // validation still performs its own runtime checks. + this.phpValidate(className, p.type, name, "$value", true); this.emitLine("return true;"); }, ); diff --git a/test/inputs/json/priority/php-validation.json b/test/inputs/json/priority/php-validation.json new file mode 100644 index 0000000000..f6fd54c062 --- /dev/null +++ b/test/inputs/json/priority/php-validation.json @@ -0,0 +1,7 @@ +{ + "boolean": true, + "integer": 1, + "number": 1.5, + "string": "value", + "integers": [1, 2, 3] +} diff --git a/test/languages.ts b/test/languages.ts index 6bcfd43611..fde046c5af 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1809,6 +1809,7 @@ export const PHPLanguage: Language = { // The motivating repro for non-nullable union support: a // heterogeneous array under a PHP-reserved-word property name. "php-mixed-union.json", + "php-validation.json", ], skipMiscJSON: true, skipSchema: [ diff --git a/test/unit/php-validation.test.ts b/test/unit/php-validation.test.ts new file mode 100644 index 0000000000..2d26be8ba1 --- /dev/null +++ b/test/unit/php-validation.test.ts @@ -0,0 +1,82 @@ +import { + InputData, + JSONSchemaInput, + quicktype, +} from "../../packages/quicktype-core/src/index.js"; +import { describe, expect, test } from "vitest"; + +async function phpForSchema(schema: object): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "ScalarValidation", + schema: JSON.stringify(schema), + }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + const result = await quicktype({ inputData, lang: "php" }); + return result.lines.join("\n"); +} + +function validationMethod(php: string, propertyName: string): string { + const start = php.indexOf( + `public static function validate${propertyName}(`, + ); + if (start < 0) { + throw new Error( + `No validate${propertyName} method found in generated PHP`, + ); + } + const end = php.indexOf("\n /**", start); + return php.slice(start, end < 0 ? undefined : end); +} + +describe("PHP property validation", () => { + test("does not recheck scalar parameter types", async () => { + const php = await phpForSchema({ + type: "object", + properties: { + boolean: { type: "boolean" }, + integer: { type: "integer" }, + number: { type: "number" }, + nullableNumber: { type: ["number", "null"] }, + string: { type: "string" }, + integers: { type: "array", items: { type: "integer" } }, + scalarUnion: { + oneOf: [{ type: "integer" }, { type: "string" }], + }, + }, + required: [ + "boolean", + "integer", + "number", + "nullableNumber", + "string", + "integers", + "scalarUnion", + ], + }); + + for (const property of [ + "Boolean", + "Integer", + "Number", + "NullableNumber", + "String", + ]) { + expect(validationMethod(php, property)).not.toMatch( + /is_(?:bool|float|int|integer|string)\(/, + ); + } + + expect(validationMethod(php, "Integers")).toContain( + "if (!is_integer($value_v))", + ); + expect(validationMethod(php, "ScalarUnion")).toContain( + "if (is_int($value))", + ); + expect(validationMethod(php, "ScalarUnion")).toContain( + "elseif (is_string($value))", + ); + }); +});