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
61 changes: 37 additions & 24 deletions packages/quicktype-core/src/language/Php/PhpRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "))"], () =>
Expand All @@ -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(
Expand Down Expand Up @@ -1110,6 +1119,7 @@ export class PhpRenderer extends ConvenienceRenderer {
nullable,
attrName,
scopeAttrName,
skipPrimitiveTypeCheck,
);
},
);
Expand Down Expand Up @@ -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}$/', ",
Expand Down Expand Up @@ -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;");
},
);
Expand Down
7 changes: 7 additions & 0 deletions test/inputs/json/priority/php-validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"boolean": true,
"integer": 1,
"number": 1.5,
"string": "value",
"integers": [1, 2, 3]
}
1 change: 1 addition & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
82 changes: 82 additions & 0 deletions test/unit/php-validation.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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))",
);
});
});
Loading