@polyprism/php-shared 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/type-mapper.ts"],"sourcesContent":["// Maps an IR FieldDef to its PHP type expression.\n//\n// Resolution order (highest to lowest priority):\n// 1. @type(...) — explicit override, used verbatim. The user owns\n// import-correctness; if the type lives in a different\n// namespace, they should write a fully-qualified name.\n// 2. @json(...) — Json fields with @json annotations. PHP can't\n// statically express the same flavour of structural\n// types TS can, so for v0 we fall back to `mixed` and\n// emit a warning rather than try to invent a PHP\n// equivalent. (Future: emit readonly value classes for\n// inline-named json shapes — that's its own feature.)\n// 3. Default mapping from IR scalar/enum/relation kind.\n//\n// All paths funnel through wrapNullability() so list-ness and nullability\n// land consistently.\n//\n// PHP-specific notes that drove the mapping:\n// - `bigint` becomes `int`. PHP `int` is platform-int — 64-bit on every\n// non-trivial deployment target since PHP 7, so values up to\n// `PHP_INT_MAX` (9.2e18) round-trip cleanly. Anyone needing bigger\n// numerics (cryptographic counters, scientific use) should set\n// `@type(\"string\", ...)` per-field; the alternative of forcing a\n// bignum dep on every consumer doesn't earn its weight.\n// - `Decimal` becomes `string`. PHP has no native arbitrary-precision\n// decimal type; the universal lowest common denominator is \"exact\n// string, parse with brick/math or BCMath at the consumer\". Same\n// escape hatch (`@type`) applies if the consumer wants `BigDecimal`.\n// - `DateTime` becomes `\\DateTimeImmutable`. Always immutable — mutable\n// `\\DateTime` is a known-foot-bullet API in PHP.\n// - `Json` becomes `mixed`. No structural typing for JSON in PHP.\n// - `Bytes` becomes `string`. PHP convention for binary data.\n// - Lists are `array` in the signature; PHPDoc carries the element type.\n\nimport type { FieldDef, ScalarType } from \"@polyprism/core\";\nimport { autoNameInlineJson } from \"@polyprism/core\";\n\nimport type { Diagnostic } from \"./diagnostics.js\";\nimport type { UseCollector } from \"./use-collector.js\";\n\nexport interface PhpTypeMapperOptions {\n readonly field: FieldDef;\n readonly modelSchemaName: string;\n readonly uses: UseCollector;\n readonly enumFqnLookup: ReadonlyMap<string, string>;\n readonly modelFqnLookup: ReadonlyMap<string, string>;\n /** FQN of the currently rendered model — relation fields back at this skip a `use`. */\n readonly selfModelFqn: string;\n /** Root namespace of generated JSON value classes, e.g. `\"Generated\\\\JsonTypes\"`. */\n readonly jsonTypesNamespace: string;\n /**\n * Names of JSON value classes that emit-models successfully generated\n * for inline @json forms. Inline-form Json fields resolve to one of\n * these names (registering a `use`); inline shapes that failed parser\n * validation aren't in this set, so the field falls back to `mixed`\n * — the emit-models loop already produced a Diagnostic for the\n * unparseable shape, and double-warning would be noisy.\n */\n readonly jsonTypeClassNames: ReadonlySet<string>;\n /** Optional sink for warnings raised during mapping (unsupported @json, etc). */\n readonly onDiagnostic?: (d: Diagnostic) => void;\n}\n\nexport interface PhpTypeMapping {\n /** The type as it appears in the constructor signature (e.g. `?string`, `array`, `\\DateTimeImmutable`). */\n readonly signatureType: string;\n /** The element type for PHPDoc `@var array<int, X>` hints, or null when not a list. */\n readonly listElementDoc: string | null;\n}\n\nexport function mapFieldPhpType(opts: PhpTypeMapperOptions): PhpTypeMapping {\n const baseType = mapBasePhpType(opts);\n return wrapNullability(baseType, opts.field);\n}\n\nfunction mapBasePhpType(opts: PhpTypeMapperOptions): string {\n const { field } = opts;\n\n // (1) @type override\n if (field.annotations.type) {\n // PHP's @type ignores the import-path side of the annotation — the user\n // is responsible for namespace correctness, same way the bare `@json(X)`\n // form works in the TS family. We DO strip a leading `?` from the\n // override so that combining `@type(\"?Foo\")` with an optional-typed\n // field doesn't produce `??Foo` after wrapNullability prepends its own.\n // The user's leading `?` reads as intent (\"this might be null\"), and\n // wrapNullability will reapply it for optional fields anyway.\n const overrideType = field.annotations.type.typeName;\n return overrideType.startsWith(\"?\") ? overrideType.slice(1).trimStart() : overrideType;\n }\n\n // (2) @json on Json field — resolves by form:\n // - inline-anonymous / inline-named → generated class under JsonTypes/\n // namespace, registered with the use collector\n // - bare / with-path → warn and fall back to `mixed`. Bare TS @json\n // trusts the user to have imported the type, which has no clean PHP\n // equivalent (you'd want to point at a real PHP class via @type\n // instead). With-path's \"from './path'\" doesn't translate either.\n if (field.type.kind === \"scalar\" && field.type.scalar === \"Json\" && field.annotations.json) {\n const json = field.annotations.json;\n const fieldPath = `${opts.modelSchemaName}.${field.name}`;\n if (json.kind === \"inline-anonymous\" || json.kind === \"inline-named\") {\n const className =\n json.kind === \"inline-anonymous\"\n ? autoNameInlineJson(opts.modelSchemaName, field.name)\n : json.typeName;\n if (opts.jsonTypeClassNames.has(className)) {\n return opts.uses.add(`${opts.jsonTypesNamespace}\\\\${className}`);\n }\n // The emit-models pipeline rejected the inline shape (and already\n // emitted a Diagnostic explaining why). Fall back silently.\n return \"mixed\";\n }\n // Bare and with-path forms — warn the user about the PHP gap once.\n const reason =\n json.kind === \"bare\"\n ? `the bare \\`@json(${json.typeName})\\` form trusts the user to have imported ` +\n `${json.typeName} in TypeScript, which doesn't translate to PHP autoloading. ` +\n `Use \\`@type(\"\\\\\\\\Vendor\\\\\\\\YourType\")\\` to point at a hand-written PHP class.`\n : `the with-path \\`@json(${json.typeName} from \"...\")\\` form refers to a ` +\n `TypeScript source file, which doesn't translate to PHP. Use ` +\n `\\`@type(\"\\\\\\\\Vendor\\\\\\\\YourType\")\\` to point at a hand-written PHP class.`;\n opts.onDiagnostic?.({\n severity: \"warning\",\n context: fieldPath,\n message: `PHP emitter cannot resolve this @json form: ${reason} Falling back to \\`mixed\\`.`,\n });\n return \"mixed\";\n }\n\n // (3) Default mapping\n switch (field.type.kind) {\n case \"scalar\":\n return mapScalar(field.type.scalar);\n case \"enum\":\n return resolveEnumShort(field.type.enumName, opts);\n case \"relation\":\n return resolveRelationShort(field.type.modelName, opts);\n case \"unsupported\":\n return \"mixed\";\n }\n}\n\nfunction mapScalar(scalar: ScalarType): string {\n switch (scalar) {\n case \"String\":\n return \"string\";\n case \"Boolean\":\n return \"bool\";\n case \"Int\":\n return \"int\";\n case \"Float\":\n return \"float\";\n case \"BigInt\":\n // See header comment — PHP int is 64-bit on all modern targets;\n // @type(string) is the escape hatch for the rare overflow case.\n return \"int\";\n case \"Decimal\":\n return \"string\";\n case \"DateTime\":\n // Leading backslash signals \"global namespace\" so this works whether\n // or not the current file `use \\DateTimeImmutable;`. We don't bother\n // registering a use for the built-in.\n return \"\\\\DateTimeImmutable\";\n case \"Json\":\n return \"mixed\";\n case \"Bytes\":\n return \"string\";\n }\n}\n\nfunction resolveEnumShort(enumName: string, opts: PhpTypeMapperOptions): string {\n const fqn = opts.enumFqnLookup.get(enumName);\n if (!fqn) return enumName; // best-effort fallback; the renderer's caller is supposed to populate the lookup\n return opts.uses.add(fqn);\n}\n\nfunction resolveRelationShort(modelName: string, opts: PhpTypeMapperOptions): string {\n const fqn = opts.modelFqnLookup.get(modelName);\n if (!fqn) return modelName;\n // Skip use registration for self-references — the type resolves inside\n // the current namespace without a use statement.\n if (fqn === opts.selfModelFqn) {\n return shortNameOf(fqn);\n }\n return opts.uses.add(fqn);\n}\n\nfunction shortNameOf(fqn: string): string {\n const i = fqn.lastIndexOf(\"\\\\\");\n return i === -1 ? fqn : fqn.slice(i + 1);\n}\n\nfunction wrapNullability(baseType: string, field: FieldDef): PhpTypeMapping {\n if (field.isList) {\n // Lists are typed as `array` in the constructor — PHP's signature\n // doesn't carry an element type — and the element shape is recorded\n // separately for PHPDoc emission. Lists themselves are non-nullable:\n // Prisma returns `[]` for an empty list, never `null`.\n return { signatureType: \"array\", listElementDoc: baseType };\n }\n if (!field.isRequired) {\n // PHP nullable shorthand. Won't compose for built-ins prefixed with `\\`\n // since `?\\DateTimeImmutable` is the standard idiomatic form (and PHP\n // parses it correctly), but worth noting in case a future override\n // changes how `\\` prefixes are emitted.\n //\n // `mixed` already includes null in PHP's type system — `?mixed` is a\n // syntax error. The only path that produces `mixed` today is the Json\n // scalar mapping, but we keep the check by base-type rather than by\n // field shape so a future @type override that resolves to `mixed`\n // doesn't trip the same footgun.\n if (baseType === \"mixed\") {\n return { signatureType: baseType, listElementDoc: null };\n }\n return { signatureType: `?${baseType}`, listElementDoc: null };\n }\n return { signatureType: baseType, listElementDoc: null };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ declare class UseCollector {
2
+ private readonly uses;
3
+ private readonly currentNamespace;
4
+ constructor(currentNamespace: string);
5
+ /**
6
+ * Register a class reference. `fqn` is the fully-qualified name without a
7
+ * leading backslash: e.g. `"Generated\\Enums\\Role"`. If the class is
8
+ * in the current namespace, this is a no-op — short-name references
9
+ * resolve in-namespace without a use statement.
10
+ *
11
+ * Returns the short name that callers should emit at the use site.
12
+ */
13
+ add(fqn: string): string;
14
+ /**
15
+ * Render the accumulated `use` statements. Sorted lexicographically — the
16
+ * widely-used Symfony convention groups by depth then alphabetises within
17
+ * each group, but a simple alpha sort is good enough for v0 and matches
18
+ * what most PHP-CS tools normalise to anyway.
19
+ */
20
+ render(): string;
21
+ }
22
+
23
+ export { UseCollector };
@@ -0,0 +1,47 @@
1
+ class UseCollector {
2
+ uses = /* @__PURE__ */ new Set();
3
+ currentNamespace;
4
+ constructor(currentNamespace) {
5
+ this.currentNamespace = currentNamespace;
6
+ }
7
+ /**
8
+ * Register a class reference. `fqn` is the fully-qualified name without a
9
+ * leading backslash: e.g. `"Generated\\Enums\\Role"`. If the class is
10
+ * in the current namespace, this is a no-op — short-name references
11
+ * resolve in-namespace without a use statement.
12
+ *
13
+ * Returns the short name that callers should emit at the use site.
14
+ */
15
+ add(fqn) {
16
+ const shortName = extractShortName(fqn);
17
+ const ns = extractNamespace(fqn);
18
+ if (ns === this.currentNamespace) return shortName;
19
+ this.uses.add(fqn);
20
+ return shortName;
21
+ }
22
+ /**
23
+ * Render the accumulated `use` statements. Sorted lexicographically — the
24
+ * widely-used Symfony convention groups by depth then alphabetises within
25
+ * each group, but a simple alpha sort is good enough for v0 and matches
26
+ * what most PHP-CS tools normalise to anyway.
27
+ */
28
+ render() {
29
+ if (this.uses.size === 0) return "";
30
+ const lines = [...this.uses].sort().map((fqn) => `use ${fqn};`);
31
+ return `${lines.join("\n")}
32
+
33
+ `;
34
+ }
35
+ }
36
+ function extractShortName(fqn) {
37
+ const lastSlash = fqn.lastIndexOf("\\");
38
+ return lastSlash === -1 ? fqn : fqn.slice(lastSlash + 1);
39
+ }
40
+ function extractNamespace(fqn) {
41
+ const lastSlash = fqn.lastIndexOf("\\");
42
+ return lastSlash === -1 ? "" : fqn.slice(0, lastSlash);
43
+ }
44
+ export {
45
+ UseCollector
46
+ };
47
+ //# sourceMappingURL=use-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/use-collector.ts"],"sourcesContent":["// Collects PHP `use` statements during model rendering.\n//\n// Same idea as the TS ImportCollector, but PHP namespacing is different\n// enough that the two collectors don't share code:\n//\n// - In PHP, every class reference can either be the FQN written inline\n// (`\\Generated\\Enums\\Role`) or a short name backed by a `use` statement\n// at the top of the file (`use Generated\\Enums\\Role;` → `Role`).\n// - There's no type-vs-value distinction — PHP doesn't have a separate\n// type-only import syntax, and an unused `use` is just dead code.\n// - The current file's own namespace is excluded automatically: classes\n// in the same namespace are referenceable without a `use` statement.\n//\n// The collector tracks a single bag of FQNs; render() emits the sorted\n// `use` block. Same-namespace references skip the collector entirely\n// (they're emitted bare and never need a use).\n\nexport class UseCollector {\n private readonly uses = new Set<string>();\n private readonly currentNamespace: string;\n\n constructor(currentNamespace: string) {\n this.currentNamespace = currentNamespace;\n }\n\n /**\n * Register a class reference. `fqn` is the fully-qualified name without a\n * leading backslash: e.g. `\"Generated\\\\Enums\\\\Role\"`. If the class is\n * in the current namespace, this is a no-op — short-name references\n * resolve in-namespace without a use statement.\n *\n * Returns the short name that callers should emit at the use site.\n */\n add(fqn: string): string {\n const shortName = extractShortName(fqn);\n const ns = extractNamespace(fqn);\n if (ns === this.currentNamespace) return shortName;\n this.uses.add(fqn);\n return shortName;\n }\n\n /**\n * Render the accumulated `use` statements. Sorted lexicographically — the\n * widely-used Symfony convention groups by depth then alphabetises within\n * each group, but a simple alpha sort is good enough for v0 and matches\n * what most PHP-CS tools normalise to anyway.\n */\n render(): string {\n if (this.uses.size === 0) return \"\";\n const lines = [...this.uses].sort().map((fqn) => `use ${fqn};`);\n return `${lines.join(\"\\n\")}\\n\\n`;\n }\n}\n\nfunction extractShortName(fqn: string): string {\n const lastSlash = fqn.lastIndexOf(\"\\\\\");\n return lastSlash === -1 ? fqn : fqn.slice(lastSlash + 1);\n}\n\nfunction extractNamespace(fqn: string): string {\n const lastSlash = fqn.lastIndexOf(\"\\\\\");\n return lastSlash === -1 ? \"\" : fqn.slice(0, lastSlash);\n}\n"],"mappings":"AAiBO,MAAM,aAAa;AAAA,EACP,OAAO,oBAAI,IAAY;AAAA,EACvB;AAAA,EAEjB,YAAY,kBAA0B;AACpC,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,IAAI,KAAqB;AACvB,UAAM,YAAY,iBAAiB,GAAG;AACtC,UAAM,KAAK,iBAAiB,GAAG;AAC/B,QAAI,OAAO,KAAK,iBAAkB,QAAO;AACzC,SAAK,KAAK,IAAI,GAAG;AACjB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAiB;AACf,QAAI,KAAK,KAAK,SAAS,EAAG,QAAO;AACjC,UAAM,QAAQ,CAAC,GAAG,KAAK,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,OAAO,GAAG,GAAG;AAC9D,WAAO,GAAG,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,EAC5B;AACF;AAEA,SAAS,iBAAiB,KAAqB;AAC7C,QAAM,YAAY,IAAI,YAAY,IAAI;AACtC,SAAO,cAAc,KAAK,MAAM,IAAI,MAAM,YAAY,CAAC;AACzD;AAEA,SAAS,iBAAiB,KAAqB;AAC7C,QAAM,YAAY,IAAI,YAAY,IAAI;AACtC,SAAO,cAAc,KAAK,KAAK,IAAI,MAAM,GAAG,SAAS;AACvD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@polyprism/php-shared",
3
+ "version": "0.2.0",
4
+ "description": "Shared PHP rendering primitives used by PolyPrism's Prisma 6 & 7 PHP generators (php-class, php-readonly). Pure ESM.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Travis Fitzgerald",
8
+ "homepage": "https://github.com/TravFitz/polyprism/tree/main/packages/php-shared",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/TravFitz/polyprism.git",
12
+ "directory": "packages/php-shared"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/TravFitz/polyprism/issues"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "polyprism-source": {
22
+ "types": "./src/index.ts",
23
+ "import": "./src/index.ts"
24
+ },
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "keywords": [
37
+ "prisma",
38
+ "prisma-generator",
39
+ "prisma-7",
40
+ "php",
41
+ "codegen",
42
+ "esm",
43
+ "polyprism"
44
+ ],
45
+ "dependencies": {
46
+ "@polyprism/core": "0.2.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run"
53
+ }
54
+ }