diff --git a/.claude/settings.json b/.claude/settings.json index 43955c7e104..a1a19b11870 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -58,6 +58,6 @@ "NODE_ENV": "development", "PNPM_VERSION": "9.4.0" }, - "includeCoAuthoredBy": false, + "includeCoAuthoredBy": true, "model": "claude-sonnet-4-20250514" } diff --git a/generators/php/base/src/context/AbstractPhpGeneratorContext.ts b/generators/php/base/src/context/AbstractPhpGeneratorContext.ts index d448d1bc675..250ca767c19 100644 --- a/generators/php/base/src/context/AbstractPhpGeneratorContext.ts +++ b/generators/php/base/src/context/AbstractPhpGeneratorContext.ts @@ -1,7 +1,7 @@ import { AbstractGeneratorContext, FernGeneratorExec, GeneratorNotificationService } from "@fern-api/base-generator"; import { assertNever } from "@fern-api/core-utils"; import { RelativeFilePath } from "@fern-api/fs-utils"; -import { BasePhpCustomConfigSchema, GLOBAL_NAMESPACE, php, SELF } from "@fern-api/php-codegen"; +import { BasePhpCustomConfigSchema, GLOBAL_NAMESPACE, getSafeClassName, php, SELF } from "@fern-api/php-codegen"; import { FernFilepath, IntermediateRepresentation, @@ -82,7 +82,7 @@ export abstract class AbstractPhpGeneratorContext< } public getClassName(name: Name): string { - return name.pascalCase.safeName; + return getSafeClassName(name.pascalCase.safeName); } public getGlobalNamespace(): string { diff --git a/generators/php/codegen/src/ast/Enum.ts b/generators/php/codegen/src/ast/Enum.ts index d981c2f5b4c..e52cbc67e32 100644 --- a/generators/php/codegen/src/ast/Enum.ts +++ b/generators/php/codegen/src/ast/Enum.ts @@ -71,7 +71,9 @@ export class Enum extends AstNode { writer.write(`case ${member.name}`); if (member.value != null) { if (typeof member.value === "string") { - writer.write(` = "${member.value}"`); + // Escape backslashes and double quotes in the string value + const escapedValue = member.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + writer.write(` = "${escapedValue}"`); } else { writer.write(` = ${member.value}`); } diff --git a/generators/php/codegen/src/constants.ts b/generators/php/codegen/src/constants.ts new file mode 100644 index 00000000000..7c161970027 --- /dev/null +++ b/generators/php/codegen/src/constants.ts @@ -0,0 +1,75 @@ +// PHP reserved keywords (case-insensitive) that cannot be used as class names +// Source: https://www.php.net/manual/en/reserved.keywords.php +export const PHP_RESERVED_KEYWORDS = new Set([ + "abstract", + "and", + "array", + "as", + "break", + "callable", + "case", + "catch", + "class", + "clone", + "const", + "continue", + "declare", + "default", + "die", + "do", + "echo", + "else", + "elseif", + "empty", + "enddeclare", + "endfor", + "endforeach", + "endif", + "endswitch", + "endwhile", + "eval", + "exit", + "extends", + "final", + "finally", + "fn", + "for", + "foreach", + "function", + "global", + "goto", + "if", + "implements", + "include", + "include_once", + "instanceof", + "insteadof", + "interface", + "isset", + "list", + "match", + "namespace", + "new", + "or", + "print", + "private", + "protected", + "public", + "readonly", + "require", + "require_once", + "return", + "static", + "switch", + "throw", + "trait", + "try", + "unset", + "use", + "var", + "while", + "xor", + "yield", + "yield_from", + "__halt_compiler" +]); diff --git a/generators/php/codegen/src/index.ts b/generators/php/codegen/src/index.ts index 26f7b1f393a..090253a24f6 100644 --- a/generators/php/codegen/src/index.ts +++ b/generators/php/codegen/src/index.ts @@ -1,3 +1,5 @@ export { GLOBAL_NAMESPACE, SELF, STATIC } from "./ast/core/Constant"; +export { PHP_RESERVED_KEYWORDS } from "./constants"; export { BasePhpCustomConfigSchema } from "./custom-config/BasePhpCustomConfigSchema"; export * as php from "./php"; +export { getSafeClassName } from "./utils"; diff --git a/generators/php/codegen/src/utils.ts b/generators/php/codegen/src/utils.ts new file mode 100644 index 00000000000..9167b3fa84f --- /dev/null +++ b/generators/php/codegen/src/utils.ts @@ -0,0 +1,15 @@ +import { PHP_RESERVED_KEYWORDS } from "./constants"; + +/** + * Returns a safe class name, adding a trailing underscore if the name + * conflicts with a PHP reserved keyword. + */ +export function getSafeClassName(className: string): string { + // Check if the class name is a reserved keyword (case-insensitive) + if (PHP_RESERVED_KEYWORDS.has(className.toLowerCase())) { + // Add trailing underscore to avoid collision + return `${className}_`; + } + + return className; +} diff --git a/generators/php/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts b/generators/php/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts index 8a0aee306b0..927daac1b11 100644 --- a/generators/php/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts +++ b/generators/php/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts @@ -3,7 +3,7 @@ import { FernGeneratorExec } from "@fern-api/browser-compatible-base-generator"; import { FernIr } from "@fern-api/dynamic-ir-sdk"; -import { BasePhpCustomConfigSchema, php } from "@fern-api/php-codegen"; +import { BasePhpCustomConfigSchema, getSafeClassName, php } from "@fern-api/php-codegen"; import { camelCase, upperFirst } from "lodash-es"; import { DynamicTypeLiteralMapper } from "./DynamicTypeLiteralMapper"; @@ -63,7 +63,7 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene } public getClassName(name: FernIr.Name): string { - return name.pascalCase.safeName; + return getSafeClassName(name.pascalCase.safeName); } public getRootClientClassName(): string { diff --git a/generators/php/sdk/versions.yml b/generators/php/sdk/versions.yml index d9d99c256e9..400162aa53a 100644 --- a/generators/php/sdk/versions.yml +++ b/generators/php/sdk/versions.yml @@ -1,4 +1,14 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.17.2 + changelogEntry: + - summary: | + Fix enum string escaping and PHP reserved keyword handling. + - Enum values containing quotes are now properly escaped to prevent syntax errors. + - Class names that conflict with PHP reserved keywords (e.g., "eval", "list") automatically receive a trailing underscore (e.g., "Eval_"). + type: fix + createdAt: "2025-10-17" + irVersion: 59 + - version: 1.17.1 changelogEntry: - summary: |