@omnifyjp/omnify 6.2.0 → 6.3.1
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/cli.js +25 -1
- package/ts-dist/php/audit-observer-generator.js +2 -4
- package/ts-dist/php/audit-trait-generator.js +1 -0
- package/ts-dist/php/enum-generator.js +1 -1
- package/ts-dist/php/file-model-generator.js +1 -1
- package/ts-dist/php/file-trait-generator.js +6 -4
- package/ts-dist/php/index.js +7 -1
- package/ts-dist/php/model-generator.js +18 -13
- package/ts-dist/php/php-style.d.ts +83 -0
- package/ts-dist/php/php-style.js +251 -0
- package/ts-dist/php/pint-format.js +11 -1
- package/ts-dist/php/resource-generator.js +18 -7
- package/ts-dist/php/service-generator.js +5 -6
- package/ts-dist/php/trait-generator.js +5 -7
- package/ts-dist/php/types.d.ts +51 -1
- package/ts-dist/php/types.js +64 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/omnify",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
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": "6.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "6.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "6.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "6.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "6.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "6.3.1",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "6.3.1",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "6.3.1",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "6.3.1",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "6.3.1"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/ts-dist/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import { Command } from 'commander';
|
|
|
29
29
|
import { parse as parseYaml } from 'yaml';
|
|
30
30
|
import { generateTypeScript } from './generator.js';
|
|
31
31
|
import { generatePhp, derivePhpConfig } from './php/index.js';
|
|
32
|
+
import { stripPhpComments } from './php/types.js';
|
|
32
33
|
import { pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
33
34
|
import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
|
|
34
35
|
import { runPintFormat } from './php/pint-format.js';
|
|
@@ -100,6 +101,11 @@ function resolveFromConfig(configPath) {
|
|
|
100
101
|
route: laravelConfig?.route,
|
|
101
102
|
config: laravelConfig?.config,
|
|
102
103
|
nestedset: laravelConfig?.nestedset,
|
|
104
|
+
modules: laravelConfig?.modules,
|
|
105
|
+
shared: laravelConfig?.shared,
|
|
106
|
+
enums: laravelConfig?.enums,
|
|
107
|
+
traits: laravelConfig?.traits,
|
|
108
|
+
openapi: laravelConfig?.openapi,
|
|
103
109
|
} : undefined;
|
|
104
110
|
return {
|
|
105
111
|
inputSpec,
|
|
@@ -226,6 +232,7 @@ program
|
|
|
226
232
|
let phpOverwritten = 0;
|
|
227
233
|
let phpSkipped = 0;
|
|
228
234
|
let phpSkippedSibling = 0;
|
|
235
|
+
let phpShadowed = 0;
|
|
229
236
|
const skippedSiblingExamples = [];
|
|
230
237
|
const writtenPaths = [];
|
|
231
238
|
for (const file of phpFiles) {
|
|
@@ -233,11 +240,24 @@ program
|
|
|
233
240
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
234
241
|
if (!file.overwrite && existsSync(filePath)) {
|
|
235
242
|
phpSkipped++;
|
|
243
|
+
const existing = file.expects || (file.conflicts && file.conflicts.length > 0)
|
|
244
|
+
? readFileSync(filePath, 'utf-8')
|
|
245
|
+
: '';
|
|
236
246
|
// The file predates something the template now needs. Say so once,
|
|
237
247
|
// naming the file and the line, rather than rewriting user code.
|
|
238
|
-
if (file.expects && !
|
|
248
|
+
if (file.expects && !existing.includes(file.expects.needle)) {
|
|
239
249
|
console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${file.expects.hint}`);
|
|
240
250
|
}
|
|
251
|
+
// The mirror: the file contains an edit that silently disables what
|
|
252
|
+
// the base generates. #171 — a redeclared $fillable shadows the
|
|
253
|
+
// parent's, so every property added afterwards is stripped on mass
|
|
254
|
+
// assignment with no error anywhere.
|
|
255
|
+
for (const conflict of file.conflicts ?? []) {
|
|
256
|
+
if (new RegExp(conflict.pattern).test(stripPhpComments(existing))) {
|
|
257
|
+
phpShadowed++;
|
|
258
|
+
console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${conflict.hint}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
241
261
|
continue;
|
|
242
262
|
}
|
|
243
263
|
// Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
|
|
@@ -285,6 +305,10 @@ program
|
|
|
285
305
|
console.log(` ${phpCreated} files created (user-editable)`);
|
|
286
306
|
if (phpSkipped > 0)
|
|
287
307
|
console.log(` ${phpSkipped} files skipped (already exist)`);
|
|
308
|
+
if (phpShadowed > 0) {
|
|
309
|
+
console.warn(` ${phpShadowed} editable file(s) shadow a generated property — ` +
|
|
310
|
+
`schema changes will NOT reach them until that is fixed (see warnings above).`);
|
|
311
|
+
}
|
|
288
312
|
if (phpSkippedSibling > 0) {
|
|
289
313
|
console.log(` ${phpSkippedSibling} user-editable stub(s) skipped (project sibling with same name found in subfolder)`);
|
|
290
314
|
for (const ex of skippedSiblingExamples) {
|
|
@@ -72,9 +72,7 @@ class WriteAuditLog implements ShouldQueue
|
|
|
72
72
|
* ip_address, user_agent, tags,
|
|
73
73
|
* created_at).
|
|
74
74
|
*/
|
|
75
|
-
public function __construct(public array $payload)
|
|
76
|
-
{
|
|
77
|
-
}
|
|
75
|
+
public function __construct(public array $payload) {}
|
|
78
76
|
|
|
79
77
|
/**
|
|
80
78
|
* Default retry behaviour: try 3 times with 5-second backoff between
|
|
@@ -197,7 +195,7 @@ class Audit extends Model
|
|
|
197
195
|
*/
|
|
198
196
|
public function prunable(): Builder
|
|
199
197
|
{
|
|
200
|
-
$retention = (string) config('omnify.audit.logRetention', ${
|
|
198
|
+
$retention = (string) config('omnify.audit.logRetention', '${retention.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}');
|
|
201
199
|
if ($retention === '') {
|
|
202
200
|
return static::query()->whereRaw('1 = 0');
|
|
203
201
|
}
|
|
@@ -59,7 +59,7 @@ ${labelsConst}
|
|
|
59
59
|
public function label(?string $locale = null): string
|
|
60
60
|
{
|
|
61
61
|
$locale = $locale ?? app()->getLocale();
|
|
62
|
-
$labels =
|
|
62
|
+
$labels = self::LABELS[$this->value] ?? [];
|
|
63
63
|
|
|
64
64
|
return $labels[$locale]
|
|
65
65
|
?? $labels[config('app.fallback_locale', 'en')]
|
|
@@ -138,9 +138,9 @@ use ${traitsNamespace}\\HasLocalizedDisplayName;${sharedModelsNamespace !== base
|
|
|
138
138
|
*/
|
|
139
139
|
class FileBaseModel extends BaseModel
|
|
140
140
|
{
|
|
141
|
+
use HasLocalizedDisplayName;
|
|
141
142
|
use HasUuids;
|
|
142
143
|
use SoftDeletes;
|
|
143
|
-
use HasLocalizedDisplayName;
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* The table associated with the model.
|
|
@@ -55,11 +55,13 @@ trait HasFiles
|
|
|
55
55
|
/**
|
|
56
56
|
* Attach files by IDs to a collection, making them permanent.
|
|
57
57
|
*
|
|
58
|
-
* @param
|
|
58
|
+
* @param array<string> $fileIds
|
|
59
59
|
*/
|
|
60
60
|
public function attachFiles(array $fileIds, string $collection = 'default', int $startOrder = 0): void
|
|
61
61
|
{
|
|
62
|
-
if (empty($fileIds))
|
|
62
|
+
if (empty($fileIds)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
63
65
|
|
|
64
66
|
File::whereIn('id', $fileIds)->update([
|
|
65
67
|
'fileable_type' => $this->getMorphClass(),
|
|
@@ -67,14 +69,14 @@ trait HasFiles
|
|
|
67
69
|
'collection' => $collection,
|
|
68
70
|
'status' => FileStatusEnum::Permanent,
|
|
69
71
|
'expires_at' => null,
|
|
70
|
-
'sort_order' => \\Illuminate\\Support\\Facades\\DB::raw('sort_order + '
|
|
72
|
+
'sort_order' => \\Illuminate\\Support\\Facades\\DB::raw('sort_order + '.$startOrder),
|
|
71
73
|
]);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Sync files for a collection (detach old, attach new).
|
|
76
78
|
*
|
|
77
|
-
* @param
|
|
79
|
+
* @param array<string> $fileIds
|
|
78
80
|
*/
|
|
79
81
|
public function syncFiles(array $fileIds, string $collection = 'default'): void
|
|
80
82
|
{
|
package/ts-dist/php/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { generateControllers } from './controller-generator.js';
|
|
|
35
35
|
import { generateServices } from './service-generator.js';
|
|
36
36
|
import { generateRoutes } from './route-generator.js';
|
|
37
37
|
import { generateEnums } from './enum-generator.js';
|
|
38
|
+
import { normalizeNotOperator, normalizePhpBlankLines, normalizePhpStatements, normalizeTopLevelUses } from './php-style.js';
|
|
38
39
|
import { generateOpenApi } from './openapi-generator.js';
|
|
39
40
|
import { generateArchitectureDoc } from './architecture-doc-generator.js';
|
|
40
41
|
export { derivePhpConfig } from './types.js';
|
|
@@ -111,5 +112,10 @@ export function generatePhp(data, overrides) {
|
|
|
111
112
|
// no behavior change for existing consumers, and the doc updates
|
|
112
113
|
// automatically as schemas / config change so it never drifts.
|
|
113
114
|
files.push(...generateArchitectureDoc(reader, config));
|
|
114
|
-
|
|
115
|
+
// #174: every generated PHP file leaves here in Pint's import shape —
|
|
116
|
+
// unused imports dropped, the rest ordered. Doing it once, centrally, is why
|
|
117
|
+
// no template has to remember: the two rules fired on almost every layer.
|
|
118
|
+
return files.map((file) => file.path.endsWith('.php')
|
|
119
|
+
? { ...file, content: normalizePhpBlankLines(normalizePhpStatements(normalizeNotOperator(normalizeTopLevelUses(file.content)))) }
|
|
120
|
+
: file);
|
|
115
121
|
}
|
|
@@ -5,7 +5,7 @@ import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName, pluralize } fro
|
|
|
5
5
|
import { resolveTimestamps } from './timestamps.js';
|
|
6
6
|
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
7
7
|
import { buildRelation } from './relation-builder.js';
|
|
8
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
|
|
8
|
+
import { baseFile, userFile, shadowingPropertyConflicts, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
|
|
9
9
|
import { enumClassName } from './enum-generator.js';
|
|
10
10
|
/** Generate base model and user model for all project-owned object schemas,
|
|
11
11
|
* plus user models for package schemas (extending the package model). */
|
|
@@ -474,7 +474,11 @@ ${traits.join('\n')}
|
|
|
474
474
|
//
|
|
475
475
|
}
|
|
476
476
|
`;
|
|
477
|
-
|
|
477
|
+
// #171: this subclass is created once and then owned by the project. The
|
|
478
|
+
// most common edit to it — redeclaring $fillable — silently disables every
|
|
479
|
+
// property omnify adds to the base afterwards. We cannot prevent the edit,
|
|
480
|
+
// but we can stop it from being invisible.
|
|
481
|
+
return userFile(`${editable.path}/${editable.fileName}`, content, undefined, shadowingPropertyConflicts(baseAlias));
|
|
478
482
|
}
|
|
479
483
|
function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset', hasFiles = false, modelNamespace = '', localesNamespace = '', traitsNamespace = '', sharedModelsNamespace = '', config, hasAuditLog = false) {
|
|
480
484
|
const lines = [];
|
|
@@ -608,25 +612,26 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
|
|
|
608
612
|
return lines.length === 0 ? '' : lines.join('\n') + '\n';
|
|
609
613
|
}
|
|
610
614
|
function buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, hasFiles = false, hasAuditLog = false) {
|
|
611
|
-
const
|
|
612
|
-
lines.push(' use HasLocalizedDisplayName;');
|
|
615
|
+
const names = ['HasLocalizedDisplayName'];
|
|
613
616
|
if (hasFiles)
|
|
614
|
-
|
|
617
|
+
names.push('HasFiles');
|
|
615
618
|
if (hasAuditLog)
|
|
616
|
-
|
|
619
|
+
names.push('HasOmnifyAuditLog');
|
|
617
620
|
if (needsUuidTrait)
|
|
618
|
-
|
|
621
|
+
names.push('HasUuids');
|
|
619
622
|
if (needsUlidTrait)
|
|
620
|
-
|
|
623
|
+
names.push('HasUlids');
|
|
621
624
|
if (isAuthenticatable)
|
|
622
|
-
|
|
625
|
+
names.push('Notifiable');
|
|
623
626
|
if (hasNestedSet)
|
|
624
|
-
|
|
627
|
+
names.push('NodeTrait');
|
|
625
628
|
if (hasSoftDelete)
|
|
626
|
-
|
|
629
|
+
names.push('SoftDeletes');
|
|
627
630
|
if (hasTranslatable)
|
|
628
|
-
|
|
629
|
-
|
|
631
|
+
names.push('Translatable');
|
|
632
|
+
// #174: Pint's `ordered_traits` sorts them, so emit them sorted.
|
|
633
|
+
names.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0));
|
|
634
|
+
return names.map((n) => ` use ${n};`).join('\n') + '\n';
|
|
630
635
|
}
|
|
631
636
|
function buildFillable(properties, expandedProperties, propertyOrder) {
|
|
632
637
|
const fields = [];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting helpers that keep emitted PHP byte-stable under Laravel Pint.
|
|
3
|
+
*
|
|
4
|
+
* Issue #174: `omnify generate` wrote PHP that Pint immediately rewrote, so a
|
|
5
|
+
* version bump with no schema change produced hundreds of modified files that
|
|
6
|
+
* a formatter pass then reverted. Consumers running Pint paid review noise;
|
|
7
|
+
* consumers without it kept the churn forever. Codegen a formatter rewrites
|
|
8
|
+
* can never be verified byte-for-byte, so the generators emit Pint's own shape
|
|
9
|
+
* instead of relying on the post-generate Pint pass to fix it up.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Collects `use` imports for the classes a Resource references, so the emitted
|
|
13
|
+
* file matches what Laravel Pint's `laravel` preset produces.
|
|
14
|
+
*
|
|
15
|
+
* Issue #174: the generator emitted inline `\App\Http\Resources\FooResource`,
|
|
16
|
+
* which Pint rewrites into an import plus the short name. Every consumer with
|
|
17
|
+
* Pint therefore saw the whole Resource layer change on `generate` and change
|
|
18
|
+
* back on `pint` — 315 files of churn per run on the reporting project — and
|
|
19
|
+
* every consumer WITHOUT Pint kept the churn permanently. Codegen a formatter
|
|
20
|
+
* immediately rewrites can never be verified byte-for-byte.
|
|
21
|
+
*
|
|
22
|
+
* Two cases keep the FQCN, because an import would be wrong rather than noisy:
|
|
23
|
+
* - the short name is already taken by a different FQN (a genuine collision);
|
|
24
|
+
* - the class lives in this file's own namespace, where PHP resolves the
|
|
25
|
+
* short name without an import and Pint adds none.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createClassImporter(currentNamespace: string, reserved?: string[]): {
|
|
28
|
+
/** Reference `fqn` the way Pint would: short name + import when possible. */
|
|
29
|
+
ref(fqn: string): string;
|
|
30
|
+
/** Every FQN to import, minus the reserved ones the template writes itself. */
|
|
31
|
+
list(): string[];
|
|
32
|
+
};
|
|
33
|
+
export type ClassImporter = ReturnType<typeof createClassImporter>;
|
|
34
|
+
/**
|
|
35
|
+
* Render a `use` block in Pint's `ordered_imports` order. PHP-CS-Fixer's alpha
|
|
36
|
+
* algorithm compares with `\` replaced by a space, so a shorter namespace
|
|
37
|
+
* sorts before a longer one sharing its prefix.
|
|
38
|
+
*/
|
|
39
|
+
export declare function renderUseBlock(fqns: string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* Rewrite a file's top-level `use` block the way Pint's `ordered_imports` and
|
|
42
|
+
* `no_unused_imports` would: drop imports nothing in the file references, then
|
|
43
|
+
* sort what is left.
|
|
44
|
+
*
|
|
45
|
+
* Applied to every generated PHP file rather than to each template, because
|
|
46
|
+
* every template that grew an import grew the same two violations (#174). Only
|
|
47
|
+
* `use` at column 0 is touched — a trait `use` inside a class body is indented,
|
|
48
|
+
* and grouped or function/const imports are left exactly as written.
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeTopLevelUses(content: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The blank-line half of Pint's `laravel` preset, applied to every generated
|
|
53
|
+
* PHP file (#174):
|
|
54
|
+
*
|
|
55
|
+
* - `no_extra_blank_lines` — never two blank lines in a row, and none left
|
|
56
|
+
* hanging before a closing brace;
|
|
57
|
+
* - `class_attributes_separation` — one blank line between a class's trait
|
|
58
|
+
* `use` group and the member that follows it.
|
|
59
|
+
*
|
|
60
|
+
* Templates composed from optional sections (a `$incrementing` block that is
|
|
61
|
+
* usually absent, a trait list that is sometimes empty) produce these by
|
|
62
|
+
* construction, which is why this is central rather than per template.
|
|
63
|
+
*/
|
|
64
|
+
export declare function normalizePhpBlankLines(content: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The remaining whole-file rules from Pint's `laravel` preset that templates
|
|
67
|
+
* kept getting wrong (#174):
|
|
68
|
+
*
|
|
69
|
+
* - `function_declaration` — an arrow function is `fn () =>`, not `fn() =>`;
|
|
70
|
+
* - `blank_line_before_statement` — a `return` that follows a statement gets
|
|
71
|
+
* a blank line before it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function normalizePhpStatements(content: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* `not_operator_with_successor_space` — Laravel's preset writes `! $x`, not
|
|
76
|
+
* `!$x` (#174).
|
|
77
|
+
*
|
|
78
|
+
* Done with a scanner rather than a regex because the same character is
|
|
79
|
+
* ordinary text inside a string literal, and generated services embed SQL and
|
|
80
|
+
* message strings. Only code outside quotes is touched, and `!=` / `!==` are
|
|
81
|
+
* comparisons, not negations.
|
|
82
|
+
*/
|
|
83
|
+
export declare function normalizeNotOperator(content: string): string;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting helpers that keep emitted PHP byte-stable under Laravel Pint.
|
|
3
|
+
*
|
|
4
|
+
* Issue #174: `omnify generate` wrote PHP that Pint immediately rewrote, so a
|
|
5
|
+
* version bump with no schema change produced hundreds of modified files that
|
|
6
|
+
* a formatter pass then reverted. Consumers running Pint paid review noise;
|
|
7
|
+
* consumers without it kept the churn forever. Codegen a formatter rewrites
|
|
8
|
+
* can never be verified byte-for-byte, so the generators emit Pint's own shape
|
|
9
|
+
* instead of relying on the post-generate Pint pass to fix it up.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Collects `use` imports for the classes a Resource references, so the emitted
|
|
13
|
+
* file matches what Laravel Pint's `laravel` preset produces.
|
|
14
|
+
*
|
|
15
|
+
* Issue #174: the generator emitted inline `\App\Http\Resources\FooResource`,
|
|
16
|
+
* which Pint rewrites into an import plus the short name. Every consumer with
|
|
17
|
+
* Pint therefore saw the whole Resource layer change on `generate` and change
|
|
18
|
+
* back on `pint` — 315 files of churn per run on the reporting project — and
|
|
19
|
+
* every consumer WITHOUT Pint kept the churn permanently. Codegen a formatter
|
|
20
|
+
* immediately rewrites can never be verified byte-for-byte.
|
|
21
|
+
*
|
|
22
|
+
* Two cases keep the FQCN, because an import would be wrong rather than noisy:
|
|
23
|
+
* - the short name is already taken by a different FQN (a genuine collision);
|
|
24
|
+
* - the class lives in this file's own namespace, where PHP resolves the
|
|
25
|
+
* short name without an import and Pint adds none.
|
|
26
|
+
*/
|
|
27
|
+
export function createClassImporter(currentNamespace, reserved = []) {
|
|
28
|
+
// short name → FQN it is bound to. Seeded with the names the template always
|
|
29
|
+
// imports, so a schema called e.g. `Request` cannot shadow them.
|
|
30
|
+
const bound = new Map();
|
|
31
|
+
for (const fqn of reserved) {
|
|
32
|
+
bound.set(fqn.slice(fqn.lastIndexOf('\\') + 1), fqn);
|
|
33
|
+
}
|
|
34
|
+
const imported = new Set();
|
|
35
|
+
return {
|
|
36
|
+
/** Reference `fqn` the way Pint would: short name + import when possible. */
|
|
37
|
+
ref(fqn) {
|
|
38
|
+
const clean = fqn.replace(/^\\+/, '');
|
|
39
|
+
const shortName = clean.slice(clean.lastIndexOf('\\') + 1);
|
|
40
|
+
const ns = clean.slice(0, clean.lastIndexOf('\\'));
|
|
41
|
+
if (ns === currentNamespace)
|
|
42
|
+
return shortName;
|
|
43
|
+
const existing = bound.get(shortName);
|
|
44
|
+
if (existing && existing !== clean)
|
|
45
|
+
return `\\${clean}`;
|
|
46
|
+
if (!existing)
|
|
47
|
+
bound.set(shortName, clean);
|
|
48
|
+
imported.add(clean);
|
|
49
|
+
return shortName;
|
|
50
|
+
},
|
|
51
|
+
/** Every FQN to import, minus the reserved ones the template writes itself. */
|
|
52
|
+
list() {
|
|
53
|
+
return [...imported];
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Render a `use` block in Pint's `ordered_imports` order. PHP-CS-Fixer's alpha
|
|
59
|
+
* algorithm compares with `\` replaced by a space, so a shorter namespace
|
|
60
|
+
* sorts before a longer one sharing its prefix.
|
|
61
|
+
*/
|
|
62
|
+
export function renderUseBlock(fqns) {
|
|
63
|
+
const sortKey = (fqn) => fqn.split(' as ')[0].replace(/\\/g, ' ').toLowerCase();
|
|
64
|
+
return [...new Set(fqns)]
|
|
65
|
+
.sort((a, b) => (sortKey(a) < sortKey(b) ? -1 : sortKey(a) > sortKey(b) ? 1 : 0))
|
|
66
|
+
.map((fqn) => `use ${fqn};`)
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Rewrite a file's top-level `use` block the way Pint's `ordered_imports` and
|
|
71
|
+
* `no_unused_imports` would: drop imports nothing in the file references, then
|
|
72
|
+
* sort what is left.
|
|
73
|
+
*
|
|
74
|
+
* Applied to every generated PHP file rather than to each template, because
|
|
75
|
+
* every template that grew an import grew the same two violations (#174). Only
|
|
76
|
+
* `use` at column 0 is touched — a trait `use` inside a class body is indented,
|
|
77
|
+
* and grouped or function/const imports are left exactly as written.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeTopLevelUses(content) {
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
const useIdx = [];
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
if (/^(class|trait|interface|enum|abstract|final|return)\b/.test(line))
|
|
85
|
+
break;
|
|
86
|
+
if (/^use\s+[A-Za-z_\\][^;]*;\s*$/.test(line) && !line.includes('{'))
|
|
87
|
+
useIdx.push(i);
|
|
88
|
+
}
|
|
89
|
+
if (useIdx.length === 0)
|
|
90
|
+
return content;
|
|
91
|
+
const first = useIdx[0];
|
|
92
|
+
const last = useIdx[useIdx.length - 1];
|
|
93
|
+
// Anything other than blank lines between the imports means this is not a
|
|
94
|
+
// single block (a comment splitting it, say); leave the file alone.
|
|
95
|
+
for (let i = first; i <= last; i++) {
|
|
96
|
+
if (!useIdx.includes(i) && lines[i].trim() !== '')
|
|
97
|
+
return content;
|
|
98
|
+
}
|
|
99
|
+
const body = [...lines.slice(0, first), ...lines.slice(last + 1)].join('\n');
|
|
100
|
+
const kept = useIdx
|
|
101
|
+
.map((i) => lines[i])
|
|
102
|
+
.filter((line) => {
|
|
103
|
+
const m = line.match(/^use\s+(?:function\s+|const\s+)?([^;]+);$/);
|
|
104
|
+
if (!m)
|
|
105
|
+
return true;
|
|
106
|
+
const spec = m[1].trim();
|
|
107
|
+
const alias = spec.includes(' as ')
|
|
108
|
+
? spec.slice(spec.lastIndexOf(' as ') + 4).trim()
|
|
109
|
+
: spec.slice(spec.lastIndexOf('\\') + 1);
|
|
110
|
+
// A name used anywhere in the body — code, docblock or attribute — keeps
|
|
111
|
+
// its import, which is the same call `no_unused_imports` makes.
|
|
112
|
+
return new RegExp(`(?<![\\w\\\\])${alias}(?![\\w])`).test(body);
|
|
113
|
+
});
|
|
114
|
+
const sorted = renderUseBlock(kept.map((line) => line.replace(/^use\s+/, '').replace(/;$/, ''))).split('\n').filter((l) => l !== '');
|
|
115
|
+
return [...lines.slice(0, first), ...sorted, ...lines.slice(last + 1)].join('\n');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The blank-line half of Pint's `laravel` preset, applied to every generated
|
|
119
|
+
* PHP file (#174):
|
|
120
|
+
*
|
|
121
|
+
* - `no_extra_blank_lines` — never two blank lines in a row, and none left
|
|
122
|
+
* hanging before a closing brace;
|
|
123
|
+
* - `class_attributes_separation` — one blank line between a class's trait
|
|
124
|
+
* `use` group and the member that follows it.
|
|
125
|
+
*
|
|
126
|
+
* Templates composed from optional sections (a `$incrementing` block that is
|
|
127
|
+
* usually absent, a trait list that is sometimes empty) produce these by
|
|
128
|
+
* construction, which is why this is central rather than per template.
|
|
129
|
+
*/
|
|
130
|
+
export function normalizePhpBlankLines(content) {
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
const out = [];
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
const line = lines[i];
|
|
135
|
+
const isBlank = line.trim() === '';
|
|
136
|
+
if (isBlank) {
|
|
137
|
+
// Collapse a run of blank lines, and drop it entirely when the next
|
|
138
|
+
// real line closes a block.
|
|
139
|
+
if (out.length > 0 && out[out.length - 1].trim() === '')
|
|
140
|
+
continue;
|
|
141
|
+
let j = i;
|
|
142
|
+
while (j < lines.length && lines[j].trim() === '')
|
|
143
|
+
j++;
|
|
144
|
+
if (j < lines.length && lines[j].trim().startsWith('}'))
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const prev = out[out.length - 1];
|
|
148
|
+
// A method's closing brace must be separated from the next member.
|
|
149
|
+
if (prev !== undefined
|
|
150
|
+
&& /^ {4}\}$/.test(prev)
|
|
151
|
+
&& !isBlank
|
|
152
|
+
&& !line.trim().startsWith('}')) {
|
|
153
|
+
out.push('');
|
|
154
|
+
}
|
|
155
|
+
// A class's trait `use` group must be separated from what follows it.
|
|
156
|
+
const prevUse = out[out.length - 1];
|
|
157
|
+
if (prevUse !== undefined
|
|
158
|
+
&& /^\s+use\s+[A-Za-z_\\][^;]*;\s*$/.test(prevUse)
|
|
159
|
+
&& !isBlank
|
|
160
|
+
&& !/^\s+use\s+[A-Za-z_\\][^;]*;\s*$/.test(line)
|
|
161
|
+
&& !line.trim().startsWith('}')) {
|
|
162
|
+
out.push('');
|
|
163
|
+
}
|
|
164
|
+
out.push(line);
|
|
165
|
+
}
|
|
166
|
+
return out.join('\n');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The remaining whole-file rules from Pint's `laravel` preset that templates
|
|
170
|
+
* kept getting wrong (#174):
|
|
171
|
+
*
|
|
172
|
+
* - `function_declaration` — an arrow function is `fn () =>`, not `fn() =>`;
|
|
173
|
+
* - `blank_line_before_statement` — a `return` that follows a statement gets
|
|
174
|
+
* a blank line before it.
|
|
175
|
+
*/
|
|
176
|
+
export function normalizePhpStatements(content) {
|
|
177
|
+
const spaced = content.replace(/(?<![\w$>\\])fn\(/g, 'fn (');
|
|
178
|
+
const lines = spaced.split('\n');
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
const prev = out[out.length - 1];
|
|
182
|
+
if (prev !== undefined
|
|
183
|
+
&& /^\s*return\b/.test(line)
|
|
184
|
+
&& prev.trim() !== ''
|
|
185
|
+
&& !prev.trimEnd().endsWith('{')
|
|
186
|
+
&& !/^\s*(\/\/|\/\*|\*)/.test(prev)) {
|
|
187
|
+
out.push('');
|
|
188
|
+
}
|
|
189
|
+
out.push(line);
|
|
190
|
+
}
|
|
191
|
+
return out.join('\n');
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `not_operator_with_successor_space` — Laravel's preset writes `! $x`, not
|
|
195
|
+
* `!$x` (#174).
|
|
196
|
+
*
|
|
197
|
+
* Done with a scanner rather than a regex because the same character is
|
|
198
|
+
* ordinary text inside a string literal, and generated services embed SQL and
|
|
199
|
+
* message strings. Only code outside quotes is touched, and `!=` / `!==` are
|
|
200
|
+
* comparisons, not negations.
|
|
201
|
+
*/
|
|
202
|
+
export function normalizeNotOperator(content) {
|
|
203
|
+
let out = '';
|
|
204
|
+
let i = 0;
|
|
205
|
+
while (i < content.length) {
|
|
206
|
+
const ch = content[i];
|
|
207
|
+
const next = content[i + 1];
|
|
208
|
+
// Comments are prose: an apostrophe in "Laravel's" must not be read as a
|
|
209
|
+
// string opening, or everything after it goes untouched.
|
|
210
|
+
if (ch === '/' && next === '/') {
|
|
211
|
+
const end = content.indexOf('\n', i);
|
|
212
|
+
const stop = end === -1 ? content.length : end;
|
|
213
|
+
out += content.slice(i, stop);
|
|
214
|
+
i = stop;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (ch === '/' && next === '*') {
|
|
218
|
+
const end = content.indexOf('*/', i + 2);
|
|
219
|
+
const stop = end === -1 ? content.length : end + 2;
|
|
220
|
+
out += content.slice(i, stop);
|
|
221
|
+
i = stop;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === "'" || ch === '"') {
|
|
225
|
+
const quote = ch;
|
|
226
|
+
let j = i + 1;
|
|
227
|
+
while (j < content.length) {
|
|
228
|
+
if (content[j] === '\\') {
|
|
229
|
+
j += 2;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (content[j] === quote) {
|
|
233
|
+
j++;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
j++;
|
|
237
|
+
}
|
|
238
|
+
out += content.slice(i, j);
|
|
239
|
+
i = j;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ch === '!' && next !== '=' && next !== ' ') {
|
|
243
|
+
out += '! ';
|
|
244
|
+
i++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
out += ch;
|
|
248
|
+
i++;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
@@ -65,7 +65,17 @@ export function runPintFormat(writtenPhpPaths) {
|
|
|
65
65
|
if (writtenPhpPaths.length === 0) {
|
|
66
66
|
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null };
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
// Opt-out (#174). The auto-run hides what the generators actually emit: a
|
|
69
|
+
// template that Pint rewrites looks fine here and produces churn in every
|
|
70
|
+
// project where Pint is absent or does not run. `OMNIFY_NO_PINT=1` is how
|
|
71
|
+
// the repo's own fixture check measures the raw output.
|
|
72
|
+
if (process.env.OMNIFY_NO_PINT === '1') {
|
|
73
|
+
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null };
|
|
74
|
+
}
|
|
75
|
+
// Resolution walks up from a written file. Trying only the first one makes
|
|
76
|
+
// it depend on emission order — a single file outside the Laravel root and
|
|
77
|
+
// the whole run goes unformatted, silently. Try each until one resolves.
|
|
78
|
+
const pintPath = writtenPhpPaths.reduce((found, path) => found ?? findPintBinary(path), null);
|
|
69
79
|
if (!pintPath) {
|
|
70
80
|
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null };
|
|
71
81
|
}
|
|
@@ -5,6 +5,7 @@ import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName } from './naming
|
|
|
5
5
|
import { resolveTimestamps } from './timestamps.js';
|
|
6
6
|
import { isHiddenByDefault, toResourceExpression } from './type-mapper.js';
|
|
7
7
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
8
|
+
import { createClassImporter, renderUseBlock } from './php-style.js';
|
|
8
9
|
/** Generate Resource classes for all project-owned visible object schemas.
|
|
9
10
|
*
|
|
10
11
|
* Issue #101 v5.8.23 emitted ONLY the base — but child resources embed a
|
|
@@ -72,6 +73,12 @@ function generateBaseResource(name, schema, reader, config) {
|
|
|
72
73
|
const hasSoftDelete = options.softDelete ?? false;
|
|
73
74
|
const hasId = options.id ?? true;
|
|
74
75
|
const fields = [];
|
|
76
|
+
// #174: reference other Resources by short name + `use` import, the way Pint
|
|
77
|
+
// would rewrite them, so `generate` and the formatter stop disagreeing.
|
|
78
|
+
const importer = createClassImporter(baseNamespace, [
|
|
79
|
+
'Illuminate\\Http\\Request',
|
|
80
|
+
'Illuminate\\Http\\Resources\\Json\\JsonResource',
|
|
81
|
+
]);
|
|
75
82
|
if (hasId) {
|
|
76
83
|
fields.push(" 'id' => $this->id,");
|
|
77
84
|
}
|
|
@@ -84,7 +91,7 @@ function generateBaseResource(name, schema, reader, config) {
|
|
|
84
91
|
if (isHiddenByDefault(type) || hidden)
|
|
85
92
|
continue;
|
|
86
93
|
if (type === 'Association') {
|
|
87
|
-
addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config);
|
|
94
|
+
addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config, importer);
|
|
88
95
|
continue;
|
|
89
96
|
}
|
|
90
97
|
if (expandedProperties[propName]) {
|
|
@@ -129,6 +136,11 @@ function generateBaseResource(name, schema, reader, config) {
|
|
|
129
136
|
fields.push(" 'deleted_at' => $this->deleted_at?->toISOString(),");
|
|
130
137
|
}
|
|
131
138
|
const fieldsContent = fields.join('\n');
|
|
139
|
+
const useBlock = renderUseBlock([
|
|
140
|
+
'Illuminate\\Http\\Request',
|
|
141
|
+
'Illuminate\\Http\\Resources\\Json\\JsonResource',
|
|
142
|
+
...importer.list(),
|
|
143
|
+
]);
|
|
132
144
|
const content = `<?php
|
|
133
145
|
|
|
134
146
|
namespace ${baseNamespace};
|
|
@@ -140,8 +152,7 @@ namespace ${baseNamespace};
|
|
|
140
152
|
* @generated by omnify
|
|
141
153
|
*/
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
use Illuminate\\Http\\Resources\\Json\\JsonResource;
|
|
155
|
+
${useBlock}
|
|
145
156
|
|
|
146
157
|
class ${modelName}ResourceBase extends JsonResource
|
|
147
158
|
{
|
|
@@ -236,7 +247,7 @@ class ${editable.className} extends ${baseAlias}
|
|
|
236
247
|
`;
|
|
237
248
|
return userFile(`${editable.path}/${editable.fileName}`, content);
|
|
238
249
|
}
|
|
239
|
-
function addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config) {
|
|
250
|
+
function addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config, importer) {
|
|
240
251
|
const relation = prop['relation'] ?? '';
|
|
241
252
|
const target = prop['target'] ?? '';
|
|
242
253
|
const methodName = toCamelCase(propName);
|
|
@@ -299,17 +310,17 @@ function addAssociationFields(propName, prop, fields, resourceNamespace, modelNa
|
|
|
299
310
|
case 'ManyToOne': {
|
|
300
311
|
const snakeName = toFkColumnName(propName);
|
|
301
312
|
fields.push(` '${snakeName}' => $this->${snakeName},`);
|
|
302
|
-
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn() => new
|
|
313
|
+
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn () => new ${importer.ref(`${targetResNs}\\${targetResource}`)}($this->${methodName})),`);
|
|
303
314
|
break;
|
|
304
315
|
}
|
|
305
316
|
case 'OneToMany':
|
|
306
317
|
case 'ManyToMany':
|
|
307
318
|
case 'MorphMany':
|
|
308
|
-
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn() =>
|
|
319
|
+
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn () => ${importer.ref(`${targetResNs}\\${targetResource}`)}::collection($this->${methodName})),`);
|
|
309
320
|
break;
|
|
310
321
|
case 'OneToOne':
|
|
311
322
|
case 'MorphOne':
|
|
312
|
-
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn() => new
|
|
323
|
+
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}', fn () => new ${importer.ref(`${targetResNs}\\${targetResource}`)}($this->${methodName})),`);
|
|
313
324
|
break;
|
|
314
325
|
case 'MorphTo':
|
|
315
326
|
fields.push(` '${methodName}' => $this->whenLoaded('${methodName}'),`);
|
|
@@ -1257,14 +1257,14 @@ function buildSortSection(_sortableFields, defaultSort, allowedSortColumns, tx)
|
|
|
1257
1257
|
$query
|
|
1258
1258
|
->leftJoin('${trTable} as tr_sort', function ($j) use ($locale) {
|
|
1259
1259
|
$j->on('tr_sort.${fk}', '=', '${mainTable}.id')
|
|
1260
|
-
|
|
1260
|
+
->where('tr_sort.locale', $locale);
|
|
1261
1261
|
})
|
|
1262
1262
|
->leftJoin('${trTable} as tr_sort_fb', function ($j) use ($fallback) {
|
|
1263
1263
|
$j->on('tr_sort_fb.${fk}', '=', '${mainTable}.id')
|
|
1264
|
-
|
|
1264
|
+
->where('tr_sort_fb.locale', $fallback);
|
|
1265
1265
|
})
|
|
1266
1266
|
->select('${mainTable}.*')
|
|
1267
|
-
->orderByRaw("COALESCE(tr_sort.\`{$column}\`, tr_sort_fb.\`{$column}\`) "
|
|
1267
|
+
->orderByRaw("COALESCE(tr_sort.\`{$column}\`, tr_sort_fb.\`{$column}\`) ".strtoupper($direction));
|
|
1268
1268
|
} else {
|
|
1269
1269
|
$query->orderBy($column, $direction);
|
|
1270
1270
|
}`;
|
|
@@ -1314,7 +1314,6 @@ function buildFindByIdMethod(modelName, eagerLoad, eagerCount, hasSoftDelete) {
|
|
|
1314
1314
|
*
|
|
1315
1315
|
* @example Fetch an active ${modelName}
|
|
1316
1316
|
* $model = $service->findById('01h000000000000000000000000');
|
|
1317
|
-
*
|
|
1318
1317
|
* @example Resolve a trashed ${modelName} for restore() / forceDelete()
|
|
1319
1318
|
* $trashed = $service->findById('01h000000000000000000000000', withTrashed: true);
|
|
1320
1319
|
* $service->restore($trashed);
|
|
@@ -1713,11 +1712,11 @@ ${lookupExample}
|
|
|
1713
1712
|
])
|
|
1714
1713
|
->leftJoin('${trTable} as tr_current', function ($j) use ($locale) {
|
|
1715
1714
|
$j->on('tr_current.${fk}', '=', '${mainTable}.id')
|
|
1716
|
-
|
|
1715
|
+
->where('tr_current.locale', $locale);
|
|
1717
1716
|
})
|
|
1718
1717
|
->leftJoin('${trTable} as tr_fallback', function ($j) use ($fallback) {
|
|
1719
1718
|
$j->on('tr_fallback.${fk}', '=', '${mainTable}.id')
|
|
1720
|
-
|
|
1719
|
+
->where('tr_fallback.locale', $fallback);
|
|
1721
1720
|
})
|
|
1722
1721
|
${orderByLine};
|
|
1723
1722
|
|
|
@@ -26,8 +26,7 @@ trait HasLocalizedDisplayName
|
|
|
26
26
|
/**
|
|
27
27
|
* Get the localized display name for this model.
|
|
28
28
|
*
|
|
29
|
-
* @param
|
|
30
|
-
* @return string
|
|
29
|
+
* @param string|null $locale Locale code (defaults to app locale)
|
|
31
30
|
*/
|
|
32
31
|
public static function displayName(?string $locale = null): string
|
|
33
32
|
{
|
|
@@ -53,9 +52,8 @@ trait HasLocalizedDisplayName
|
|
|
53
52
|
/**
|
|
54
53
|
* Get the localized display name for a property.
|
|
55
54
|
*
|
|
56
|
-
* @param
|
|
57
|
-
* @param
|
|
58
|
-
* @return string
|
|
55
|
+
* @param string $property Property name
|
|
56
|
+
* @param string|null $locale Locale code (defaults to app locale)
|
|
59
57
|
*/
|
|
60
58
|
public static function propertyDisplayName(string $property, ?string $locale = null): string
|
|
61
59
|
{
|
|
@@ -71,7 +69,7 @@ trait HasLocalizedDisplayName
|
|
|
71
69
|
/**
|
|
72
70
|
* Get all localized display names for a property.
|
|
73
71
|
*
|
|
74
|
-
* @param
|
|
72
|
+
* @param string $property Property name
|
|
75
73
|
* @return array<string, string>
|
|
76
74
|
*/
|
|
77
75
|
public static function allPropertyDisplayNames(string $property): array
|
|
@@ -82,7 +80,7 @@ trait HasLocalizedDisplayName
|
|
|
82
80
|
/**
|
|
83
81
|
* Get all property display names for a given locale.
|
|
84
82
|
*
|
|
85
|
-
* @param
|
|
83
|
+
* @param string|null $locale Locale code (defaults to app locale)
|
|
86
84
|
* @return array<string, string>
|
|
87
85
|
*/
|
|
88
86
|
public static function allPropertyDisplayNamesForLocale(?string $locale = null): array
|
package/ts-dist/php/types.d.ts
CHANGED
|
@@ -21,6 +21,23 @@ export interface GeneratedFile {
|
|
|
21
21
|
readonly needle: string;
|
|
22
22
|
readonly hint: string;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* For a user file only: things the file must NOT contain, and why.
|
|
26
|
+
*
|
|
27
|
+
* The mirror of `expects`. Some edits to a user-editable subclass silently
|
|
28
|
+
* disable what the base class generates — the worst being a redeclared
|
|
29
|
+
* `protected $fillable`, which in PHP REPLACES the parent's rather than
|
|
30
|
+
* merging, so every property added to the schema afterwards is stripped by
|
|
31
|
+
* Eloquent on mass assignment. Nothing throws; `save()` returns true and the
|
|
32
|
+
* column stays NULL (issue #171).
|
|
33
|
+
*
|
|
34
|
+
* Each entry is matched against the content of the file we skipped, and
|
|
35
|
+
* `hint` is printed when it matches.
|
|
36
|
+
*/
|
|
37
|
+
readonly conflicts?: readonly {
|
|
38
|
+
readonly pattern: string;
|
|
39
|
+
readonly hint: string;
|
|
40
|
+
}[];
|
|
24
41
|
}
|
|
25
42
|
/** Create a base file (always overwritten). */
|
|
26
43
|
export declare function baseFile(path: string, content: string): GeneratedFile;
|
|
@@ -28,7 +45,28 @@ export declare function baseFile(path: string, content: string): GeneratedFile;
|
|
|
28
45
|
export declare function userFile(path: string, content: string, expects?: {
|
|
29
46
|
needle: string;
|
|
30
47
|
hint: string;
|
|
31
|
-
}
|
|
48
|
+
}, conflicts?: readonly {
|
|
49
|
+
pattern: string;
|
|
50
|
+
hint: string;
|
|
51
|
+
}[]): GeneratedFile;
|
|
52
|
+
/**
|
|
53
|
+
* Eloquent array properties that REPLACE the parent's when redeclared in a
|
|
54
|
+
* subclass, rather than merging. Redeclaring any of these in a user-editable
|
|
55
|
+
* model shadows whatever the generated base puts there, and the failure is
|
|
56
|
+
* completely silent. Issue #171.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SHADOWING_ELOQUENT_PROPERTIES: readonly ["fillable", "guarded", "casts", "hidden", "visible", "appends", "dates", "touches", "with"];
|
|
59
|
+
/**
|
|
60
|
+
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
61
|
+
*
|
|
62
|
+
* `baseClass` is named in the hint because the whole point is that the base is
|
|
63
|
+
* still correct — the generator did emit the property — and the reader needs to
|
|
64
|
+
* know which file they are shadowing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function shadowingPropertyConflicts(baseClass: string): {
|
|
67
|
+
pattern: string;
|
|
68
|
+
hint: string;
|
|
69
|
+
}[];
|
|
32
70
|
/** Category of base file for modular path resolution. */
|
|
33
71
|
export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
|
|
34
72
|
/**
|
|
@@ -418,3 +456,15 @@ export declare function nestEditableByGroup(layer: BaseEditableLayer, loc: {
|
|
|
418
456
|
* All paths and namespaces fall back to sensible defaults.
|
|
419
457
|
*/
|
|
420
458
|
export declare function derivePhpConfig(overrides?: LaravelCodegenOverrides): PhpConfig;
|
|
459
|
+
/**
|
|
460
|
+
* Strip PHP comments so a commented-out declaration does not read as a real
|
|
461
|
+
* one. Without this, the very common
|
|
462
|
+
*
|
|
463
|
+
* // protected $fillable = ['name']; // left from a refactor
|
|
464
|
+
*
|
|
465
|
+
* would be reported as shadowing the base — and a warning that fires on code
|
|
466
|
+
* that is not running is worse than no warning, because people learn to ignore
|
|
467
|
+
* it. Naive on purpose: it can strip a `//` inside a string literal, which only
|
|
468
|
+
* ever costs us a warning we would otherwise have printed, never a false one.
|
|
469
|
+
*/
|
|
470
|
+
export declare function stripPhpComments(source: string): string;
|
package/ts-dist/php/types.js
CHANGED
|
@@ -6,8 +6,53 @@ export function baseFile(path, content) {
|
|
|
6
6
|
return { path, content, overwrite: true, type: 'base' };
|
|
7
7
|
}
|
|
8
8
|
/** Create a user file (created once, skip if exists). */
|
|
9
|
-
export function userFile(path, content, expects) {
|
|
10
|
-
return {
|
|
9
|
+
export function userFile(path, content, expects, conflicts) {
|
|
10
|
+
return {
|
|
11
|
+
path,
|
|
12
|
+
content,
|
|
13
|
+
overwrite: false,
|
|
14
|
+
type: 'user',
|
|
15
|
+
...(expects ? { expects } : {}),
|
|
16
|
+
...(conflicts && conflicts.length > 0 ? { conflicts } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Eloquent array properties that REPLACE the parent's when redeclared in a
|
|
21
|
+
* subclass, rather than merging. Redeclaring any of these in a user-editable
|
|
22
|
+
* model shadows whatever the generated base puts there, and the failure is
|
|
23
|
+
* completely silent. Issue #171.
|
|
24
|
+
*/
|
|
25
|
+
export const SHADOWING_ELOQUENT_PROPERTIES = [
|
|
26
|
+
'fillable',
|
|
27
|
+
'guarded',
|
|
28
|
+
'casts',
|
|
29
|
+
'hidden',
|
|
30
|
+
'visible',
|
|
31
|
+
'appends',
|
|
32
|
+
'dates',
|
|
33
|
+
'touches',
|
|
34
|
+
'with',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
38
|
+
*
|
|
39
|
+
* `baseClass` is named in the hint because the whole point is that the base is
|
|
40
|
+
* still correct — the generator did emit the property — and the reader needs to
|
|
41
|
+
* know which file they are shadowing.
|
|
42
|
+
*/
|
|
43
|
+
export function shadowingPropertyConflicts(baseClass) {
|
|
44
|
+
return SHADOWING_ELOQUENT_PROPERTIES.map((prop) => ({
|
|
45
|
+
// Matches `protected $fillable`, `public array $casts`, `protected static
|
|
46
|
+
// $with` — any redeclaration, whatever the visibility or type hint.
|
|
47
|
+
pattern: String.raw `(?:public|protected|private)\s+(?:static\s+)?(?:array\s+)?\$` + prop + String.raw `\s*=`,
|
|
48
|
+
hint: `redeclares $${prop}, which REPLACES ` +
|
|
49
|
+
`${baseClass}::$${prop} rather than merging with it (PHP does not merge ` +
|
|
50
|
+
`redeclared properties). Anything omnify adds to $${prop} from now on is ` +
|
|
51
|
+
`silently dropped: mass assignment strips it, save() still returns true, ` +
|
|
52
|
+
`and the column stays NULL. Fix: delete the redeclaration and let the base ` +
|
|
53
|
+
`provide it, or build it from the parent, e.g. ` +
|
|
54
|
+
`__construct() { $this->${prop} = array_merge(parent::$${prop} ?? [], [...]); }`,
|
|
55
|
+
}));
|
|
11
56
|
}
|
|
12
57
|
/**
|
|
13
58
|
* Resolve base file path for a schema based on structure.
|
|
@@ -382,3 +427,20 @@ export function derivePhpConfig(overrides) {
|
|
|
382
427
|
},
|
|
383
428
|
};
|
|
384
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Strip PHP comments so a commented-out declaration does not read as a real
|
|
432
|
+
* one. Without this, the very common
|
|
433
|
+
*
|
|
434
|
+
* // protected $fillable = ['name']; // left from a refactor
|
|
435
|
+
*
|
|
436
|
+
* would be reported as shadowing the base — and a warning that fires on code
|
|
437
|
+
* that is not running is worse than no warning, because people learn to ignore
|
|
438
|
+
* it. Naive on purpose: it can strip a `//` inside a string literal, which only
|
|
439
|
+
* ever costs us a warning we would otherwise have printed, never a false one.
|
|
440
|
+
*/
|
|
441
|
+
export function stripPhpComments(source) {
|
|
442
|
+
return source
|
|
443
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
444
|
+
.replace(/(^|\s)\/\/[^\n]*/g, '$1')
|
|
445
|
+
.replace(/(^|\s)#[^\n]*/g, '$1');
|
|
446
|
+
}
|