@omnifyjp/omnify 3.4.0 → 3.6.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.
- package/package.json +6 -6
- package/ts-dist/php/enum-generator.d.ts +19 -0
- package/ts-dist/php/enum-generator.js +165 -0
- package/ts-dist/php/file-model-generator.js +9 -5
- package/ts-dist/php/file-trait-generator.js +6 -4
- package/ts-dist/php/index.js +3 -0
- package/ts-dist/php/model-generator.js +29 -6
- package/ts-dist/php/trait-generator.js +6 -3
- package/ts-dist/php/types.d.ts +68 -5
- package/ts-dist/php/types.js +57 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/omnify",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"zod": "^3.24.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@omnifyjp/omnify-darwin-arm64": "3.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "3.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "3.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "3.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "3.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "3.6.0",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "3.6.0",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "3.6.0",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "3.6.0",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "3.6.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates PHP enum classes for `kind: enum` schemas.
|
|
3
|
+
*
|
|
4
|
+
* Issue #34: every enum schema becomes a self-contained PHP 8.1+ backed enum
|
|
5
|
+
* placed in the global Omnify enums namespace (default `App\Omnify\Enums`).
|
|
6
|
+
* Localized labels from the schema are embedded as a `LABELS` constant so
|
|
7
|
+
* the generated enum is usable without setting up Laravel translation files.
|
|
8
|
+
*
|
|
9
|
+
* Class name convention: `{SchemaName}Enum.php` (e.g. schema `MenuStatus` →
|
|
10
|
+
* `MenuStatusEnum`). The trailing `Enum` suffix is intentional — it prevents
|
|
11
|
+
* collisions with model classes of the same base name and signals "this is an
|
|
12
|
+
* enum class" at every callsite (`MenuStatusEnum::Draft`).
|
|
13
|
+
*/
|
|
14
|
+
import { SchemaReader } from './schema-reader.js';
|
|
15
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
16
|
+
/** Generate one PHP enum class for each `kind: enum` schema. */
|
|
17
|
+
export declare function generateEnums(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
18
|
+
/** Convert a schema name (e.g. `MenuStatus`) to its enum class name (`MenuStatusEnum`). */
|
|
19
|
+
export declare function enumClassName(schemaName: string): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates PHP enum classes for `kind: enum` schemas.
|
|
3
|
+
*
|
|
4
|
+
* Issue #34: every enum schema becomes a self-contained PHP 8.1+ backed enum
|
|
5
|
+
* placed in the global Omnify enums namespace (default `App\Omnify\Enums`).
|
|
6
|
+
* Localized labels from the schema are embedded as a `LABELS` constant so
|
|
7
|
+
* the generated enum is usable without setting up Laravel translation files.
|
|
8
|
+
*
|
|
9
|
+
* Class name convention: `{SchemaName}Enum.php` (e.g. schema `MenuStatus` →
|
|
10
|
+
* `MenuStatusEnum`). The trailing `Enum` suffix is intentional — it prevents
|
|
11
|
+
* collisions with model classes of the same base name and signals "this is an
|
|
12
|
+
* enum class" at every callsite (`MenuStatusEnum::Draft`).
|
|
13
|
+
*/
|
|
14
|
+
import { toPascalCase } from './naming-helper.js';
|
|
15
|
+
import { baseFile, resolveGlobalEnumPath, resolveGlobalEnumNamespace } from './types.js';
|
|
16
|
+
/** Generate one PHP enum class for each `kind: enum` schema. */
|
|
17
|
+
export function generateEnums(reader, config) {
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const [name, schema] of Object.entries(reader.getEnumSchemas())) {
|
|
20
|
+
// Skip package-owned enums — those are generated by their owning package's
|
|
21
|
+
// codegen, not the host project's. Mirrors how object schemas are filtered
|
|
22
|
+
// by `getProjectObjectSchemas()` elsewhere.
|
|
23
|
+
if (schema.package != null)
|
|
24
|
+
continue;
|
|
25
|
+
files.push(generateEnumClass(name, schema, config));
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
function generateEnumClass(name, schema, config) {
|
|
30
|
+
const className = enumClassName(name);
|
|
31
|
+
const namespace = resolveGlobalEnumNamespace(config, config.models.namespace);
|
|
32
|
+
const values = (schema.values ?? []);
|
|
33
|
+
const cases = values.map(v => buildCase(v));
|
|
34
|
+
const labelsConst = buildLabelsConstant(values);
|
|
35
|
+
const content = `<?php
|
|
36
|
+
|
|
37
|
+
namespace ${namespace};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* ${name}${schema.displayName ? ` — ${pickDefaultLabel(schema.displayName)}` : ''}
|
|
41
|
+
*
|
|
42
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
43
|
+
* Any changes will be overwritten on next generation.
|
|
44
|
+
*
|
|
45
|
+
* @generated by omnify
|
|
46
|
+
*/
|
|
47
|
+
enum ${className}: string
|
|
48
|
+
{
|
|
49
|
+
${cases.map(c => ` ${c}`).join('\n')}
|
|
50
|
+
|
|
51
|
+
${labelsConst}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the localized label for this enum case.
|
|
55
|
+
*
|
|
56
|
+
* Falls back to the configured app fallback locale, then any defined
|
|
57
|
+
* locale, then the raw value if no labels were declared in the schema.
|
|
58
|
+
*/
|
|
59
|
+
public function label(?string $locale = null): string
|
|
60
|
+
{
|
|
61
|
+
$locale = $locale ?? app()->getLocale();
|
|
62
|
+
$labels = static::LABELS[$this->value] ?? [];
|
|
63
|
+
|
|
64
|
+
return $labels[$locale]
|
|
65
|
+
?? $labels[config('app.fallback_locale', 'en')]
|
|
66
|
+
?? $labels[array_key_first($labels) ?? 'en']
|
|
67
|
+
?? $this->value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all enum values as a flat list.
|
|
72
|
+
*
|
|
73
|
+
* @return array<int, string>
|
|
74
|
+
*/
|
|
75
|
+
public static function values(): array
|
|
76
|
+
{
|
|
77
|
+
return array_column(self::cases(), 'value');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get an array of value → localized label suitable for select dropdowns.
|
|
82
|
+
*
|
|
83
|
+
* @return array<string, string>
|
|
84
|
+
*/
|
|
85
|
+
public static function options(?string $locale = null): array
|
|
86
|
+
{
|
|
87
|
+
$out = [];
|
|
88
|
+
foreach (self::cases() as $case) {
|
|
89
|
+
$out[$case->value] = $case->label($locale);
|
|
90
|
+
}
|
|
91
|
+
return $out;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
return baseFile(resolveGlobalEnumPath(config, `${className}.php`, config.models.path), content);
|
|
96
|
+
}
|
|
97
|
+
/** Convert a schema name (e.g. `MenuStatus`) to its enum class name (`MenuStatusEnum`). */
|
|
98
|
+
export function enumClassName(schemaName) {
|
|
99
|
+
const pascal = toPascalCase(schemaName);
|
|
100
|
+
return pascal.endsWith('Enum') ? pascal : `${pascal}Enum`;
|
|
101
|
+
}
|
|
102
|
+
/** Build a single `case Foo = 'foo';` line from a schema enum value. */
|
|
103
|
+
function buildCase(v) {
|
|
104
|
+
const value = v.value;
|
|
105
|
+
return `case ${toPhpCaseName(value)} = '${escapePhpString(value)}';`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Convert an enum value into a valid PHP case identifier. PHP case names must
|
|
109
|
+
* start with a letter or underscore and contain only `[A-Za-z0-9_]`. We map
|
|
110
|
+
* everything else through PascalCase and prefix a digit-leading name with
|
|
111
|
+
* `Case` so it's still legal.
|
|
112
|
+
*/
|
|
113
|
+
function toPhpCaseName(value) {
|
|
114
|
+
const cleaned = value
|
|
115
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
116
|
+
.split('_')
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
119
|
+
.join('');
|
|
120
|
+
if (!cleaned)
|
|
121
|
+
return 'Unknown';
|
|
122
|
+
return /^[0-9]/.test(cleaned) ? `Case${cleaned}` : cleaned;
|
|
123
|
+
}
|
|
124
|
+
/** Build the `LABELS` constant containing every locale label for every value. */
|
|
125
|
+
function buildLabelsConstant(values) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push(' /**');
|
|
128
|
+
lines.push(' * Localized labels for each case, embedded directly so the generated enum');
|
|
129
|
+
lines.push(' * is self-contained and works without Laravel translation files.');
|
|
130
|
+
lines.push(' *');
|
|
131
|
+
lines.push(' * @var array<string, array<string, string>>');
|
|
132
|
+
lines.push(' */');
|
|
133
|
+
lines.push(' public const LABELS = [');
|
|
134
|
+
for (const v of values) {
|
|
135
|
+
const rawLabel = v.label;
|
|
136
|
+
// `label` can be either a plain string or a `{locale: text}` map. Normalize.
|
|
137
|
+
const labels = typeof rawLabel === 'string'
|
|
138
|
+
? { en: rawLabel }
|
|
139
|
+
: (rawLabel ?? {});
|
|
140
|
+
const entries = Object.entries(labels);
|
|
141
|
+
if (entries.length === 0) {
|
|
142
|
+
lines.push(` '${escapePhpString(v.value)}' => [],`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const inner = entries
|
|
146
|
+
.map(([loc, txt]) => `'${escapePhpString(loc)}' => '${escapePhpString(String(txt))}'`)
|
|
147
|
+
.join(', ');
|
|
148
|
+
lines.push(` '${escapePhpString(v.value)}' => [${inner}],`);
|
|
149
|
+
}
|
|
150
|
+
lines.push(' ];');
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
/** Pick a sensible default label from a localized string for the doc comment. */
|
|
154
|
+
function pickDefaultLabel(displayName) {
|
|
155
|
+
if (typeof displayName === 'string')
|
|
156
|
+
return displayName;
|
|
157
|
+
return (displayName.en ??
|
|
158
|
+
displayName.ja ??
|
|
159
|
+
Object.values(displayName)[0] ??
|
|
160
|
+
'');
|
|
161
|
+
}
|
|
162
|
+
/** Escape single quotes and backslashes for inclusion in a PHP single-quoted string. */
|
|
163
|
+
function escapePhpString(value) {
|
|
164
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
165
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - FileStatusEnum.php (base, always overwritten)
|
|
6
6
|
* - FileLocales.php (base, always overwritten)
|
|
7
7
|
*/
|
|
8
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace } from './types.js';
|
|
8
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalEnumPath, resolveGlobalEnumNamespace, resolveGlobalTraitNamespace, } from './types.js';
|
|
9
9
|
/** Generate all File model infrastructure files. */
|
|
10
10
|
export function generateFileModels(reader, config) {
|
|
11
11
|
if (!reader.hasFileProperties())
|
|
@@ -96,9 +96,10 @@ class FileResource extends FileResourceBase
|
|
|
96
96
|
function generateFileBaseModel(config, defaultDisk) {
|
|
97
97
|
const baseNamespace = resolveModularBaseNamespace(config, 'File', 'Models', config.models.baseNamespace);
|
|
98
98
|
const localesNamespace = resolveModularBaseNamespace(config, 'File', 'Locales', config.models.baseNamespace + '\\Locales');
|
|
99
|
-
|
|
99
|
+
// Issue #33: HasLocalizedDisplayName is a global Omnify trait — see trait-generator.ts.
|
|
100
|
+
const traitsNamespace = resolveGlobalTraitNamespace(config, config.models.baseNamespace + '\\Traits');
|
|
100
101
|
const sharedModelsNamespace = resolveSharedBaseNamespace(config, 'Models', config.models.baseNamespace);
|
|
101
|
-
const enumNamespace =
|
|
102
|
+
const enumNamespace = resolveGlobalEnumNamespace(config, config.models.namespace);
|
|
102
103
|
const modelNamespace = config.models.namespace;
|
|
103
104
|
const content = `<?php
|
|
104
105
|
|
|
@@ -334,7 +335,10 @@ class File extends FileBaseModel
|
|
|
334
335
|
return userFile(`${config.models.path}/File.php`, content);
|
|
335
336
|
}
|
|
336
337
|
function generateFileStatusEnum(config) {
|
|
337
|
-
|
|
338
|
+
// Issue #33: FileStatusEnum belongs to the Omnify file-attachment subsystem,
|
|
339
|
+
// not to a user-defined "File" schema, so it lives in the global enums
|
|
340
|
+
// folder rather than under `Modules/File/Enums/`.
|
|
341
|
+
const modelNamespace = resolveGlobalEnumNamespace(config, config.models.namespace);
|
|
338
342
|
const content = `<?php
|
|
339
343
|
|
|
340
344
|
namespace ${modelNamespace};
|
|
@@ -372,7 +376,7 @@ enum FileStatusEnum: string
|
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
378
|
`;
|
|
375
|
-
return baseFile(
|
|
379
|
+
return baseFile(resolveGlobalEnumPath(config, 'FileStatusEnum.php', config.models.path), content);
|
|
376
380
|
}
|
|
377
381
|
function generateFileLocales(config, locales) {
|
|
378
382
|
const baseNamespace = resolveModularBaseNamespace(config, 'File', 'Locales', config.models.baseNamespace + '\\Locales');
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generates the HasFiles trait for models with File-type properties.
|
|
3
3
|
*/
|
|
4
|
-
import { baseFile,
|
|
4
|
+
import { baseFile, resolveGlobalTraitPath, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, } from './types.js';
|
|
5
5
|
/** Generate the HasFiles trait. */
|
|
6
6
|
export function generateFileTrait(config) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Issue #33: HasFiles is a global Omnify trait — not scoped to any user
|
|
8
|
+
// schema — so it lives in the dedicated global traits folder.
|
|
9
|
+
const traitsNamespace = resolveGlobalTraitNamespace(config, config.models.baseNamespace + '\\Traits');
|
|
10
|
+
const enumNamespace = resolveGlobalEnumNamespace(config, config.models.namespace);
|
|
9
11
|
const modelNamespace = config.models.namespace;
|
|
10
12
|
const content = `<?php
|
|
11
13
|
|
|
@@ -92,6 +94,6 @@ trait HasFiles
|
|
|
92
94
|
}
|
|
93
95
|
`;
|
|
94
96
|
return [
|
|
95
|
-
baseFile(
|
|
97
|
+
baseFile(resolveGlobalTraitPath(config, 'HasFiles.php', config.models.basePath + '/Traits'), content),
|
|
96
98
|
];
|
|
97
99
|
}
|
package/ts-dist/php/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { baseFile } from './types.js';
|
|
|
30
30
|
import { generateControllers } from './controller-generator.js';
|
|
31
31
|
import { generateServices } from './service-generator.js';
|
|
32
32
|
import { generateRoutes } from './route-generator.js';
|
|
33
|
+
import { generateEnums } from './enum-generator.js';
|
|
33
34
|
export { derivePhpConfig } from './types.js';
|
|
34
35
|
/** Generate all PHP files from schemas.json data. */
|
|
35
36
|
export function generatePhp(data, overrides) {
|
|
@@ -61,6 +62,8 @@ export function generatePhp(data, overrides) {
|
|
|
61
62
|
files.push(...generateServices(reader, config));
|
|
62
63
|
files.push(...generateRoutes(reader, config));
|
|
63
64
|
}
|
|
65
|
+
// Issue #34: every `kind: enum` schema becomes a global PHP enum class.
|
|
66
|
+
files.push(...generateEnums(reader, config));
|
|
64
67
|
// Per-schema files
|
|
65
68
|
files.push(...generateLocales(reader, config));
|
|
66
69
|
files.push(...generateModels(reader, config));
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
5
5
|
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
6
6
|
import { buildRelation } from './relation-builder.js';
|
|
7
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace } from './types.js';
|
|
7
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, } from './types.js';
|
|
8
|
+
import { enumClassName } from './enum-generator.js';
|
|
8
9
|
/** Generate base model and user model for all project-owned object schemas,
|
|
9
10
|
* plus user models for package schemas (extending the package model). */
|
|
10
11
|
export function generateModels(reader, config) {
|
|
@@ -35,7 +36,8 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
35
36
|
const modelName = toPascalCase(name);
|
|
36
37
|
const baseNamespace = resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace);
|
|
37
38
|
const localesNamespace = resolveModularBaseNamespace(config, name, 'Locales', config.models.baseNamespace + '\\Locales');
|
|
38
|
-
|
|
39
|
+
// Issue #33: HasLocalizedDisplayName is a global Omnify trait, not a per-shared module file.
|
|
40
|
+
const traitsNamespace = resolveGlobalTraitNamespace(config, config.models.baseNamespace + '\\Traits');
|
|
39
41
|
const sharedModelsNamespace = resolveSharedBaseNamespace(config, 'Models', config.models.baseNamespace);
|
|
40
42
|
const modelNamespace = config.models.namespace;
|
|
41
43
|
const options = schema.options ?? {};
|
|
@@ -70,7 +72,7 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
70
72
|
const fillable = buildFillable(properties, expandedProperties, propertyOrder);
|
|
71
73
|
const hidden = buildHidden(properties, expandedProperties, propertyOrder);
|
|
72
74
|
const appends = buildAppends(expandedProperties);
|
|
73
|
-
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader);
|
|
75
|
+
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader, config);
|
|
74
76
|
const relations = buildRelations(name, properties, propertyOrder, modelNamespace, reader);
|
|
75
77
|
const accessors = buildAccessors(expandedProperties);
|
|
76
78
|
const fileAccessors = hasFiles ? buildFileAccessors(properties, propertyOrder, modelNamespace) : '';
|
|
@@ -252,9 +254,14 @@ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable
|
|
|
252
254
|
lines.push(`use ${traitsNamespace || baseNamespace + '\\Traits'}\\HasLocalizedDisplayName;`);
|
|
253
255
|
lines.push(`use ${localesNamespace || baseNamespace + '\\Locales'}\\${modelName}Locales;`);
|
|
254
256
|
if (hasFiles) {
|
|
255
|
-
// HasFiles
|
|
257
|
+
// Issue #33 followup: HasFiles is a global Omnify trait — see
|
|
258
|
+
// file-trait-generator.ts. The trait now lives at
|
|
259
|
+
// `app/Omnify/Traits/HasFiles.php` (namespace `App\Omnify\Traits`),
|
|
260
|
+
// not under per-module `Modules/File/Traits/`. The import statement
|
|
261
|
+
// here must match, otherwise generated models crash with
|
|
262
|
+
// `Trait "App\Omnify\Modules\File\Traits\HasFiles" not found`.
|
|
256
263
|
const fileTraitsNs = config
|
|
257
|
-
?
|
|
264
|
+
? resolveGlobalTraitNamespace(config, baseNamespace + '\\Traits')
|
|
258
265
|
: (traitsNamespace || baseNamespace + '\\Traits');
|
|
259
266
|
lines.push(`use ${fileTraitsNs}\\HasFiles;`);
|
|
260
267
|
if (modelNamespace) {
|
|
@@ -422,13 +429,29 @@ function buildAppends(expandedProperties) {
|
|
|
422
429
|
return '';
|
|
423
430
|
return appends.map(a => ` '${a}',`).join('\n') + '\n';
|
|
424
431
|
}
|
|
425
|
-
function buildCasts(properties, expandedProperties, propertyOrder, reader) {
|
|
432
|
+
function buildCasts(properties, expandedProperties, propertyOrder, reader, config) {
|
|
426
433
|
const casts = [];
|
|
434
|
+
// Issue #34: cast `EnumRef` properties to their generated PHP enum class.
|
|
435
|
+
// Use a fully-qualified `\Namespace\FooEnum::class` reference so we don't
|
|
436
|
+
// need to track imports — Laravel resolves the enum cast by class name.
|
|
437
|
+
const globalEnumNs = config
|
|
438
|
+
? resolveGlobalEnumNamespace(config, config.models.namespace)
|
|
439
|
+
: '';
|
|
427
440
|
for (const propName of propertyOrder) {
|
|
428
441
|
const prop = properties[propName];
|
|
429
442
|
if (!prop)
|
|
430
443
|
continue;
|
|
431
444
|
const type = prop['type'] ?? 'String';
|
|
445
|
+
// EnumRef → cast to generated enum class (only when the referenced enum
|
|
446
|
+
// schema actually exists; otherwise fall through to a plain string).
|
|
447
|
+
if (type === 'EnumRef') {
|
|
448
|
+
const enumName = prop['enum'];
|
|
449
|
+
if (typeof enumName === 'string' && reader.getSchema(enumName)?.kind === 'enum' && globalEnumNs) {
|
|
450
|
+
const snakeName = toSnakeCase(propName);
|
|
451
|
+
casts.push(`'${snakeName}' => \\${globalEnumNs}\\${enumClassName(enumName)}::class,`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
432
455
|
if (type === 'Association') {
|
|
433
456
|
const relation = prop['relation'] ?? '';
|
|
434
457
|
if (relation === 'ManyToOne') {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of TraitGenerator.php — generates HasLocalizedDisplayName trait.
|
|
3
3
|
*/
|
|
4
|
-
import { baseFile,
|
|
4
|
+
import { baseFile, resolveGlobalTraitPath, resolveGlobalTraitNamespace } from './types.js';
|
|
5
5
|
/** Generate the HasLocalizedDisplayName trait. */
|
|
6
6
|
export function generateTrait(config) {
|
|
7
|
-
|
|
7
|
+
// Issue #33: HasLocalizedDisplayName is a global Omnify trait shared across
|
|
8
|
+
// all schemas, so it belongs in the global traits folder, not under any
|
|
9
|
+
// single schema's `Modules/{Schema}/Traits/` subfolder.
|
|
10
|
+
const ns = resolveGlobalTraitNamespace(config, config.models.baseNamespace + '\\Traits');
|
|
8
11
|
const content = `<?php
|
|
9
12
|
|
|
10
13
|
namespace ${ns};
|
|
@@ -99,6 +102,6 @@ trait HasLocalizedDisplayName
|
|
|
99
102
|
}
|
|
100
103
|
`;
|
|
101
104
|
return [
|
|
102
|
-
baseFile(
|
|
105
|
+
baseFile(resolveGlobalTraitPath(config, 'HasLocalizedDisplayName.php', config.models.basePath + '/Traits'), content),
|
|
103
106
|
];
|
|
104
107
|
}
|
package/ts-dist/php/types.d.ts
CHANGED
|
@@ -16,28 +16,42 @@ export declare function userFile(path: string, content: string): GeneratedFile;
|
|
|
16
16
|
export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
|
|
17
17
|
/**
|
|
18
18
|
* Resolve base file path for a schema based on structure.
|
|
19
|
-
* Modular:
|
|
19
|
+
* Modular: {modules.path}/{SchemaName}/{category}/{fileName}
|
|
20
20
|
* Legacy: {legacyPath}/{fileName}
|
|
21
21
|
*/
|
|
22
22
|
export declare function resolveModularBasePath(config: PhpConfig, schemaName: string, category: BaseCategory, fileName: string, legacyPath: string): string;
|
|
23
23
|
/**
|
|
24
24
|
* Resolve base namespace for a schema based on structure.
|
|
25
|
-
* Modular:
|
|
25
|
+
* Modular: {modules.namespace}\{SchemaName}\{Category}
|
|
26
26
|
* Legacy: {legacyNamespace}
|
|
27
27
|
*/
|
|
28
28
|
export declare function resolveModularBaseNamespace(config: PhpConfig, schemaName: string, category: BaseCategory, legacyNamespace: string): string;
|
|
29
29
|
/**
|
|
30
|
-
* Resolve shared base path (for cross-module files like BaseModel
|
|
31
|
-
* Modular:
|
|
30
|
+
* Resolve shared base path (for cross-module files like BaseModel).
|
|
31
|
+
* Modular: {shared.path}/{category}/{fileName}
|
|
32
32
|
* Legacy: {legacyPath}/{fileName}
|
|
33
33
|
*/
|
|
34
34
|
export declare function resolveSharedBasePath(config: PhpConfig, category: BaseCategory, fileName: string, legacyPath: string): string;
|
|
35
35
|
/**
|
|
36
36
|
* Resolve shared base namespace.
|
|
37
|
-
* Modular:
|
|
37
|
+
* Modular: {shared.namespace}\{Category}
|
|
38
38
|
* Legacy: {legacyNamespace}
|
|
39
39
|
*/
|
|
40
40
|
export declare function resolveSharedBaseNamespace(config: PhpConfig, category: BaseCategory, legacyNamespace: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve global Omnify enum file path. Used for system enums that aren't tied
|
|
43
|
+
* to a single user-defined schema (e.g. FileStatusEnum belongs to the Omnify
|
|
44
|
+
* file-attachment subsystem). In modular mode these live under their own
|
|
45
|
+
* top-level folder so they're easy to find. In legacy mode the original
|
|
46
|
+
* `legacyPath` is preserved for backward compat.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGlobalEnumPath(config: PhpConfig, fileName: string, legacyPath: string): string;
|
|
49
|
+
/** Resolve global Omnify enum namespace. Modular: {globalEnums.namespace}. Legacy: passthrough. */
|
|
50
|
+
export declare function resolveGlobalEnumNamespace(config: PhpConfig, legacyNamespace: string): string;
|
|
51
|
+
/** Resolve global Omnify trait file path (HasFiles, HasLocalizedDisplayName, ...). */
|
|
52
|
+
export declare function resolveGlobalTraitPath(config: PhpConfig, fileName: string, legacyPath: string): string;
|
|
53
|
+
/** Resolve global Omnify trait namespace. */
|
|
54
|
+
export declare function resolveGlobalTraitNamespace(config: PhpConfig, legacyNamespace: string): string;
|
|
41
55
|
/** Per-target path and namespace override from codegen.laravel config. */
|
|
42
56
|
export interface LaravelPathOverride {
|
|
43
57
|
path?: string;
|
|
@@ -65,6 +79,26 @@ export interface LaravelCodegenOverrides {
|
|
|
65
79
|
policy?: LaravelPathOverride;
|
|
66
80
|
controller?: LaravelPathOverride;
|
|
67
81
|
service?: LaravelPathOverride;
|
|
82
|
+
/**
|
|
83
|
+
* Per-schema base-class root for `structure: modular`. Default
|
|
84
|
+
* `App\Omnify\Modules` (path `app/Omnify/Modules`) so generated base classes
|
|
85
|
+
* never collide with a team's hand-written `app/Modules/` (Modular Monolith
|
|
86
|
+
* pattern is common in Laravel).
|
|
87
|
+
*/
|
|
88
|
+
modules?: LaravelPathOverride;
|
|
89
|
+
/** Cross-module shared classes (e.g. `BaseModel`). Default `App\Omnify\Shared`. */
|
|
90
|
+
shared?: LaravelPathOverride;
|
|
91
|
+
/**
|
|
92
|
+
* Global Omnify enums that aren't tied to a single user-defined schema
|
|
93
|
+
* (e.g. `FileStatusEnum` for the file-attachment subsystem).
|
|
94
|
+
* Default `App\Omnify\Enums`.
|
|
95
|
+
*/
|
|
96
|
+
enums?: LaravelPathOverride;
|
|
97
|
+
/**
|
|
98
|
+
* Global Omnify traits (e.g. `HasFiles`, `HasLocalizedDisplayName`).
|
|
99
|
+
* Default `App\Omnify\Traits`.
|
|
100
|
+
*/
|
|
101
|
+
traits?: LaravelPathOverride;
|
|
68
102
|
route?: LaravelPathOverride;
|
|
69
103
|
config?: LaravelPathOverride;
|
|
70
104
|
nestedset?: NestedSetOverride;
|
|
@@ -74,6 +108,35 @@ export interface PhpConfig {
|
|
|
74
108
|
/** Filesystem prefix applied to all generated paths. Empty when not set. */
|
|
75
109
|
rootPath: string;
|
|
76
110
|
structure: 'legacy' | 'modular';
|
|
111
|
+
/**
|
|
112
|
+
* Modular base-class root: `{path}/{Schema}/{Layer}/...` and
|
|
113
|
+
* `{namespace}\{Schema}\{Layer}`. Only consulted in `structure: modular`.
|
|
114
|
+
*/
|
|
115
|
+
modules: {
|
|
116
|
+
namespace: string;
|
|
117
|
+
path: string;
|
|
118
|
+
};
|
|
119
|
+
/** Cross-module shared root (e.g. BaseModel lives at `{shared.path}/Models/`). */
|
|
120
|
+
shared: {
|
|
121
|
+
namespace: string;
|
|
122
|
+
path: string;
|
|
123
|
+
};
|
|
124
|
+
/** Global Omnify enums (e.g. FileStatusEnum). */
|
|
125
|
+
globalEnums: {
|
|
126
|
+
namespace: string;
|
|
127
|
+
path: string;
|
|
128
|
+
};
|
|
129
|
+
/** Global Omnify traits (e.g. HasFiles, HasLocalizedDisplayName). */
|
|
130
|
+
globalTraits: {
|
|
131
|
+
namespace: string;
|
|
132
|
+
path: string;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Backward-compat alias for `modules.path`. Kept so external callers and
|
|
136
|
+
* tests that read `config.modulesPath` keep working; new code should use
|
|
137
|
+
* `config.modules.path` instead.
|
|
138
|
+
* @deprecated use `modules.path`
|
|
139
|
+
*/
|
|
77
140
|
modulesPath: string;
|
|
78
141
|
models: {
|
|
79
142
|
namespace: string;
|
package/ts-dist/php/types.js
CHANGED
|
@@ -11,57 +11,81 @@ export function userFile(path, content) {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Resolve base file path for a schema based on structure.
|
|
14
|
-
* Modular:
|
|
14
|
+
* Modular: {modules.path}/{SchemaName}/{category}/{fileName}
|
|
15
15
|
* Legacy: {legacyPath}/{fileName}
|
|
16
16
|
*/
|
|
17
17
|
export function resolveModularBasePath(config, schemaName, category, fileName, legacyPath) {
|
|
18
18
|
if (config.structure === 'modular') {
|
|
19
|
-
return `${config.
|
|
19
|
+
return `${config.modules.path}/${schemaName}/${category}/${fileName}`;
|
|
20
20
|
}
|
|
21
21
|
return `${legacyPath}/${fileName}`;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Resolve base namespace for a schema based on structure.
|
|
25
|
-
* Modular:
|
|
25
|
+
* Modular: {modules.namespace}\{SchemaName}\{Category}
|
|
26
26
|
* Legacy: {legacyNamespace}
|
|
27
27
|
*/
|
|
28
28
|
export function resolveModularBaseNamespace(config, schemaName, category, legacyNamespace) {
|
|
29
29
|
if (config.structure === 'modular') {
|
|
30
|
-
return
|
|
30
|
+
return `${config.modules.namespace}\\${schemaName}\\${category}`;
|
|
31
31
|
}
|
|
32
32
|
return legacyNamespace;
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Resolve shared base path (for cross-module files like BaseModel
|
|
36
|
-
* Modular:
|
|
35
|
+
* Resolve shared base path (for cross-module files like BaseModel).
|
|
36
|
+
* Modular: {shared.path}/{category}/{fileName}
|
|
37
37
|
* Legacy: {legacyPath}/{fileName}
|
|
38
38
|
*/
|
|
39
39
|
export function resolveSharedBasePath(config, category, fileName, legacyPath) {
|
|
40
40
|
if (config.structure === 'modular') {
|
|
41
|
-
return `${config.
|
|
41
|
+
return `${config.shared.path}/${category}/${fileName}`;
|
|
42
42
|
}
|
|
43
43
|
return `${legacyPath}/${fileName}`;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Resolve shared base namespace.
|
|
47
|
-
* Modular:
|
|
47
|
+
* Modular: {shared.namespace}\{Category}
|
|
48
48
|
* Legacy: {legacyNamespace}
|
|
49
49
|
*/
|
|
50
50
|
export function resolveSharedBaseNamespace(config, category, legacyNamespace) {
|
|
51
51
|
if (config.structure === 'modular') {
|
|
52
|
-
return
|
|
52
|
+
return `${config.shared.namespace}\\${category}`;
|
|
53
53
|
}
|
|
54
54
|
return legacyNamespace;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Resolve global Omnify enum file path. Used for system enums that aren't tied
|
|
58
|
+
* to a single user-defined schema (e.g. FileStatusEnum belongs to the Omnify
|
|
59
|
+
* file-attachment subsystem). In modular mode these live under their own
|
|
60
|
+
* top-level folder so they're easy to find. In legacy mode the original
|
|
61
|
+
* `legacyPath` is preserved for backward compat.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveGlobalEnumPath(config, fileName, legacyPath) {
|
|
64
|
+
if (config.structure === 'modular') {
|
|
65
|
+
return `${config.globalEnums.path}/${fileName}`;
|
|
66
|
+
}
|
|
67
|
+
return `${legacyPath}/${fileName}`;
|
|
68
|
+
}
|
|
69
|
+
/** Resolve global Omnify enum namespace. Modular: {globalEnums.namespace}. Legacy: passthrough. */
|
|
70
|
+
export function resolveGlobalEnumNamespace(config, legacyNamespace) {
|
|
71
|
+
if (config.structure === 'modular') {
|
|
72
|
+
return config.globalEnums.namespace;
|
|
73
|
+
}
|
|
74
|
+
return legacyNamespace;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve global Omnify trait file path (HasFiles, HasLocalizedDisplayName, ...). */
|
|
77
|
+
export function resolveGlobalTraitPath(config, fileName, legacyPath) {
|
|
78
|
+
if (config.structure === 'modular') {
|
|
79
|
+
return `${config.globalTraits.path}/${fileName}`;
|
|
80
|
+
}
|
|
81
|
+
return `${legacyPath}/${fileName}`;
|
|
82
|
+
}
|
|
83
|
+
/** Resolve global Omnify trait namespace. */
|
|
84
|
+
export function resolveGlobalTraitNamespace(config, legacyNamespace) {
|
|
85
|
+
if (config.structure === 'modular') {
|
|
86
|
+
return config.globalTraits.namespace;
|
|
87
|
+
}
|
|
88
|
+
return legacyNamespace;
|
|
65
89
|
}
|
|
66
90
|
/** Convert a file path to a PHP namespace (e.g., app/Models/Omnify → App\Models\Omnify). */
|
|
67
91
|
function pathToNamespace(p) {
|
|
@@ -124,7 +148,13 @@ const DEFAULT_CONTROLLER_PATH = 'app/Http/Controllers/Omnify';
|
|
|
124
148
|
const DEFAULT_SERVICE_PATH = 'app/Services/Omnify';
|
|
125
149
|
const DEFAULT_ROUTE_PATH = 'routes/api/omnify';
|
|
126
150
|
const DEFAULT_CONFIG_FILE_PATH = 'config/omnify-schemas.php';
|
|
127
|
-
|
|
151
|
+
// Modular base-class roots — issue #33. All Omnify auto-generated code lives
|
|
152
|
+
// under `app/Omnify/` so it's clearly separated from team's manual `app/Modules/`
|
|
153
|
+
// (Modular Monolith pattern), and easy to gitignore / nuke as a single tree.
|
|
154
|
+
const DEFAULT_MODULES_PATH = 'app/Omnify/Modules';
|
|
155
|
+
const DEFAULT_SHARED_PATH = 'app/Omnify/Shared';
|
|
156
|
+
const DEFAULT_GLOBAL_ENUMS_PATH = 'app/Omnify/Enums';
|
|
157
|
+
const DEFAULT_GLOBAL_TRAITS_PATH = 'app/Omnify/Traits';
|
|
128
158
|
/**
|
|
129
159
|
* Apply a rootPath prefix to a path. Absolute paths and paths that already
|
|
130
160
|
* begin with `${rootPath}/` are returned as-is, so users can mix and match
|
|
@@ -166,14 +196,21 @@ export function derivePhpConfig(overrides) {
|
|
|
166
196
|
const { path: policyPath, namespace: policyNs } = resolvePathAndNamespace(rootPath, overrides?.policy, DEFAULT_POLICY_PATH, nsFromPath);
|
|
167
197
|
const { path: controllerPath, namespace: controllerNs } = resolvePathAndNamespace(rootPath, overrides?.controller, DEFAULT_CONTROLLER_PATH, nsFromPath);
|
|
168
198
|
const { path: servicePath, namespace: serviceNs } = resolvePathAndNamespace(rootPath, overrides?.service, DEFAULT_SERVICE_PATH, nsFromPath);
|
|
199
|
+
const { path: modulesPath, namespace: modulesNs } = resolvePathAndNamespace(rootPath, overrides?.modules, DEFAULT_MODULES_PATH, nsFromPath);
|
|
200
|
+
const { path: sharedPath, namespace: sharedNs } = resolvePathAndNamespace(rootPath, overrides?.shared, DEFAULT_SHARED_PATH, nsFromPath);
|
|
201
|
+
const { path: globalEnumsPath, namespace: globalEnumsNs } = resolvePathAndNamespace(rootPath, overrides?.enums, DEFAULT_GLOBAL_ENUMS_PATH, nsFromPath);
|
|
202
|
+
const { path: globalTraitsPath, namespace: globalTraitsNs } = resolvePathAndNamespace(rootPath, overrides?.traits, DEFAULT_GLOBAL_TRAITS_PATH, nsFromPath);
|
|
169
203
|
const routePath = withRoot(rootPath, overrides?.route?.path ?? DEFAULT_ROUTE_PATH);
|
|
170
204
|
const configFilePath = withRoot(rootPath, overrides?.config?.path ?? DEFAULT_CONFIG_FILE_PATH);
|
|
171
205
|
const nestedsetNs = overrides?.nestedset?.namespace ?? 'Aimeos\\Nestedset';
|
|
172
206
|
const structure = overrides?.structure ?? 'legacy';
|
|
173
|
-
const modulesPath = withRoot(rootPath, DEFAULT_MODULES_PATH);
|
|
174
207
|
return {
|
|
175
208
|
rootPath,
|
|
176
209
|
structure,
|
|
210
|
+
modules: { namespace: modulesNs, path: modulesPath },
|
|
211
|
+
shared: { namespace: sharedNs, path: sharedPath },
|
|
212
|
+
globalEnums: { namespace: globalEnumsNs, path: globalEnumsPath },
|
|
213
|
+
globalTraits: { namespace: globalTraitsNs, path: globalTraitsPath },
|
|
177
214
|
modulesPath,
|
|
178
215
|
models: {
|
|
179
216
|
namespace: modelNs,
|