@omnifyjp/omnify 3.5.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
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
|
+
}
|
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, resolveGlobalTraitNamespace, } 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) {
|
|
@@ -71,7 +72,7 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
71
72
|
const fillable = buildFillable(properties, expandedProperties, propertyOrder);
|
|
72
73
|
const hidden = buildHidden(properties, expandedProperties, propertyOrder);
|
|
73
74
|
const appends = buildAppends(expandedProperties);
|
|
74
|
-
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader);
|
|
75
|
+
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader, config);
|
|
75
76
|
const relations = buildRelations(name, properties, propertyOrder, modelNamespace, reader);
|
|
76
77
|
const accessors = buildAccessors(expandedProperties);
|
|
77
78
|
const fileAccessors = hasFiles ? buildFileAccessors(properties, propertyOrder, modelNamespace) : '';
|
|
@@ -253,9 +254,14 @@ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable
|
|
|
253
254
|
lines.push(`use ${traitsNamespace || baseNamespace + '\\Traits'}\\HasLocalizedDisplayName;`);
|
|
254
255
|
lines.push(`use ${localesNamespace || baseNamespace + '\\Locales'}\\${modelName}Locales;`);
|
|
255
256
|
if (hasFiles) {
|
|
256
|
-
// 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`.
|
|
257
263
|
const fileTraitsNs = config
|
|
258
|
-
?
|
|
264
|
+
? resolveGlobalTraitNamespace(config, baseNamespace + '\\Traits')
|
|
259
265
|
: (traitsNamespace || baseNamespace + '\\Traits');
|
|
260
266
|
lines.push(`use ${fileTraitsNs}\\HasFiles;`);
|
|
261
267
|
if (modelNamespace) {
|
|
@@ -423,13 +429,29 @@ function buildAppends(expandedProperties) {
|
|
|
423
429
|
return '';
|
|
424
430
|
return appends.map(a => ` '${a}',`).join('\n') + '\n';
|
|
425
431
|
}
|
|
426
|
-
function buildCasts(properties, expandedProperties, propertyOrder, reader) {
|
|
432
|
+
function buildCasts(properties, expandedProperties, propertyOrder, reader, config) {
|
|
427
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
|
+
: '';
|
|
428
440
|
for (const propName of propertyOrder) {
|
|
429
441
|
const prop = properties[propName];
|
|
430
442
|
if (!prop)
|
|
431
443
|
continue;
|
|
432
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
|
+
}
|
|
433
455
|
if (type === 'Association') {
|
|
434
456
|
const relation = prop['relation'] ?? '';
|
|
435
457
|
if (relation === 'ManyToOne') {
|