@omnifyjp/ts 6.7.15 → 6.7.17
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.
|
@@ -462,6 +462,7 @@ function generateUserModel(name, schema, reader, config, hasNestedSet = false) {
|
|
|
462
462
|
fillable: fillableFieldNames(properties, expandedProperties, propertyOrder),
|
|
463
463
|
hidden: hiddenFieldNames(properties, expandedProperties, propertyOrder),
|
|
464
464
|
appends: appendsFieldNames(expandedProperties),
|
|
465
|
+
casts: castsFieldNames(properties, expandedProperties, propertyOrder, reader, config),
|
|
465
466
|
};
|
|
466
467
|
const content = `<?php
|
|
467
468
|
|
|
@@ -837,6 +838,17 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
837
838
|
return '';
|
|
838
839
|
return casts.map(c => ` ${c}`).join('\n') + '\n';
|
|
839
840
|
}
|
|
841
|
+
function castsFieldNames(properties, expandedProperties, propertyOrder, reader, config) {
|
|
842
|
+
const body = buildCasts(properties, expandedProperties, propertyOrder, reader, config);
|
|
843
|
+
if (body === '') {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
const keys = [];
|
|
847
|
+
for (const m of body.matchAll(/'((?:[^'\\]|\\.)*)'\s*=>/g)) {
|
|
848
|
+
keys.push(m[1].replace(/\\'/g, "'"));
|
|
849
|
+
}
|
|
850
|
+
return keys;
|
|
851
|
+
}
|
|
840
852
|
/**
|
|
841
853
|
* Issue #98 v5.8.10: resolve the FQCN namespace of a related Model for
|
|
842
854
|
* inline `belongsTo / hasMany / belongsToMany / morphTo` references.
|
|
@@ -31,10 +31,11 @@ export declare function getReturnType(relation: string): string;
|
|
|
31
31
|
/**
|
|
32
32
|
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
33
33
|
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
34
|
-
* `column:` override included); if zero
|
|
35
|
-
*
|
|
34
|
+
* `column:` override included); if zero matches, fall back to the owning schema
|
|
35
|
+
* name (external targets). If two or more matches, throw — omnify-go#242.
|
|
36
|
+
* Issue #237.
|
|
36
37
|
*/
|
|
37
|
-
export declare function resolveHasManyForeignKey(
|
|
38
|
+
export declare function resolveHasManyForeignKey(propName: string, target: string, context?: RelationContext): string;
|
|
38
39
|
/**
|
|
39
40
|
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
40
41
|
* Returns an empty string if no ordering is configured. Issue #40.
|
|
@@ -116,10 +116,11 @@ function hasMany(propName, property, target, ns, context) {
|
|
|
116
116
|
/**
|
|
117
117
|
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
118
118
|
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
119
|
-
* `column:` override included); if zero
|
|
120
|
-
*
|
|
119
|
+
* `column:` override included); if zero matches, fall back to the owning schema
|
|
120
|
+
* name (external targets). If two or more matches, throw — omnify-go#242.
|
|
121
|
+
* Issue #237.
|
|
121
122
|
*/
|
|
122
|
-
export function resolveHasManyForeignKey(
|
|
123
|
+
export function resolveHasManyForeignKey(propName, target, context) {
|
|
123
124
|
const reader = context?.reader;
|
|
124
125
|
const owningSchemaName = context?.sourceSchemaName;
|
|
125
126
|
if (reader && owningSchemaName && target) {
|
|
@@ -127,9 +128,13 @@ export function resolveHasManyForeignKey(_propName, target, context) {
|
|
|
127
128
|
if (matches.length === 1) {
|
|
128
129
|
return matches[0];
|
|
129
130
|
}
|
|
131
|
+
if (matches.length >= 2) {
|
|
132
|
+
const columnList = matches.join(', ');
|
|
133
|
+
throw new Error(`${owningSchemaName}.${propName}: target ${target} has ${matches.length} ManyToOne associations back to ${owningSchemaName} (${columnList}) — set mappedBy: on the OneToMany to pick one`);
|
|
134
|
+
}
|
|
130
135
|
return toFkColumnName(owningSchemaName);
|
|
131
136
|
}
|
|
132
|
-
return toFkColumnName(
|
|
137
|
+
return toFkColumnName(propName);
|
|
133
138
|
}
|
|
134
139
|
function findInverseManyToOneFkColumns(reader, targetSchemaName, owningSchemaName) {
|
|
135
140
|
const targetSchema = reader.getSchema(targetSchemaName);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ShadowingEloquentProperty, type ShadowingPropertyConflict } from './types.js';
|
|
2
2
|
/** Case-sensitive intent marker checked against raw file content (issue #222). */
|
|
3
3
|
export declare function hasIntentionalShadowMarker(rawContent: string, prop: string): boolean;
|
|
4
|
+
/** Quoted array keys before `=>` in a PHP array literal segment (casts keys only). */
|
|
5
|
+
export declare function parseCastAssociativeArrayKeys(segment: string): string[];
|
|
6
|
+
export declare function parseRedeclaredCastsPropertyKeys(source: string): string[] | null;
|
|
7
|
+
export declare function parseRedeclaredCastsMethodKeys(source: string): string[] | null;
|
|
4
8
|
/**
|
|
5
9
|
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
6
10
|
*/
|
|
@@ -17,7 +21,4 @@ export type ShadowingConflictResult = {
|
|
|
17
21
|
message: string;
|
|
18
22
|
drift: ShadowingDrift;
|
|
19
23
|
};
|
|
20
|
-
/**
|
|
21
|
-
* Evaluate shadowing conflicts for a skipped user-editable model file.
|
|
22
|
-
*/
|
|
23
24
|
export declare function evaluateShadowingConflicts(existingRaw: string, conflicts: readonly ShadowingPropertyConflict[]): ShadowingConflictResult[];
|
|
@@ -20,10 +20,66 @@ function shadowingConsequence(prop) {
|
|
|
20
20
|
case 'appends':
|
|
21
21
|
return (`Accessors omnify adds to $appends from now on would not be appended to ` +
|
|
22
22
|
`the model's array or JSON form.`);
|
|
23
|
+
case 'casts':
|
|
24
|
+
return (`Casts omnify adds from now on are lost: a Json column without its cast throws ` +
|
|
25
|
+
`"Array to string conversion" at INSERT, and a Decimal column silently returns a ` +
|
|
26
|
+
`differently-formatted string per driver.`);
|
|
23
27
|
default:
|
|
24
28
|
return `Configuration omnify adds to $${prop} from now on stops applying on this model.`;
|
|
25
29
|
}
|
|
26
30
|
}
|
|
31
|
+
const CASTS_METHOD_PATTERN = String.raw `(?:public|protected)\s+function\s+casts\s*\(`;
|
|
32
|
+
function shadowingCastsMethodHint(baseClass) {
|
|
33
|
+
return (`redeclares casts(), which REPLACES ${baseClass}::casts() rather than merging with it ` +
|
|
34
|
+
`(PHP does not merge methods). Casts omnify adds from now on are lost: a Json column ` +
|
|
35
|
+
`without its cast throws "Array to string conversion" at INSERT, and a Decimal column ` +
|
|
36
|
+
`silently returns a differently-formatted string per driver. ` +
|
|
37
|
+
`Fix: return array_merge(parent::casts(), [ … ]). ` +
|
|
38
|
+
`If intentional, add \`/** @omnify-casts intentional */\` above the method to silence ` +
|
|
39
|
+
`this warning.`);
|
|
40
|
+
}
|
|
41
|
+
/** Quoted array keys before `=>` in a PHP array literal segment (casts keys only). */
|
|
42
|
+
export function parseCastAssociativeArrayKeys(segment) {
|
|
43
|
+
const keys = [];
|
|
44
|
+
const singleQuoted = /'((?:[^'\\]|\\.)*)'\s*=>/g;
|
|
45
|
+
for (const m of segment.matchAll(singleQuoted)) {
|
|
46
|
+
keys.push(m[1].replace(/\\'/g, "'"));
|
|
47
|
+
}
|
|
48
|
+
const doubleQuoted = /"((?:[^"\\]|\\.)*)"\s*=>/g;
|
|
49
|
+
for (const m of segment.matchAll(doubleQuoted)) {
|
|
50
|
+
keys.push(m[1].replace(/\\"/g, '"'));
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
}
|
|
54
|
+
export function parseRedeclaredCastsPropertyKeys(source) {
|
|
55
|
+
const stripped = stripPhpComments(source);
|
|
56
|
+
const match = stripped.match(/\$casts\s*=\s*\[[\s\S]*?\];/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return parseCastAssociativeArrayKeys(match[0]);
|
|
61
|
+
}
|
|
62
|
+
export function parseRedeclaredCastsMethodKeys(source) {
|
|
63
|
+
const stripped = stripPhpComments(source);
|
|
64
|
+
const fnIdx = stripped.search(/function\s+casts\s*\(/);
|
|
65
|
+
if (fnIdx < 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const tail = stripped.slice(fnIdx);
|
|
69
|
+
const returnMatch = tail.match(/return\s*\[[\s\S]*?\];/);
|
|
70
|
+
if (!returnMatch) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return parseCastAssociativeArrayKeys(returnMatch[0]);
|
|
74
|
+
}
|
|
75
|
+
function redeclaredCastsMethodBody(source) {
|
|
76
|
+
const stripped = stripPhpComments(source);
|
|
77
|
+
const match = stripped.match(new RegExp(CASTS_METHOD_PATTERN + String.raw `[\s\S]*?\{([\s\S]*?)\}`));
|
|
78
|
+
return match ? match[1] : null;
|
|
79
|
+
}
|
|
80
|
+
function childDeclaresCastsMethod(stripped) {
|
|
81
|
+
return new RegExp(CASTS_METHOD_PATTERN).test(stripped);
|
|
82
|
+
}
|
|
27
83
|
function shadowingFixAdvice(prop, baseClass) {
|
|
28
84
|
return (`If this narrowing is intentional, add \`/** @omnify-${prop} intentional */\` ` +
|
|
29
85
|
`above the property to silence this warning. If not, remove the redeclaration ` +
|
|
@@ -33,13 +89,20 @@ function shadowingFixAdvice(prop, baseClass) {
|
|
|
33
89
|
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
34
90
|
*/
|
|
35
91
|
export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
|
|
36
|
-
|
|
92
|
+
const castsBaseValues = baseArrayValues?.casts;
|
|
93
|
+
const castsKeys = castsBaseValues &&
|
|
94
|
+
castsBaseValues.length > 0 &&
|
|
95
|
+
castsBaseValues.every((v) => typeof v === 'string')
|
|
96
|
+
? castsBaseValues
|
|
97
|
+
: undefined;
|
|
98
|
+
const propertyConflicts = SHADOWING_ELOQUENT_PROPERTIES.map((prop) => {
|
|
37
99
|
const values = baseArrayValues?.[prop];
|
|
38
100
|
const baseValues = values && values.length > 0 && values.every((v) => typeof v === 'string')
|
|
39
101
|
? values
|
|
40
102
|
: undefined;
|
|
41
103
|
return {
|
|
42
104
|
property: prop,
|
|
105
|
+
form: 'property',
|
|
43
106
|
pattern: String.raw `(?:public|protected|private)\s+(?:static\s+)?(?:array\s+)?\$` +
|
|
44
107
|
prop +
|
|
45
108
|
String.raw `\s*=`,
|
|
@@ -49,6 +112,14 @@ export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
|
|
|
49
112
|
...(baseValues ? { baseValues } : {}),
|
|
50
113
|
};
|
|
51
114
|
});
|
|
115
|
+
const methodCastsConflict = {
|
|
116
|
+
property: 'casts',
|
|
117
|
+
form: 'method',
|
|
118
|
+
pattern: CASTS_METHOD_PATTERN,
|
|
119
|
+
hint: shadowingCastsMethodHint(baseClass),
|
|
120
|
+
...(castsKeys ? { baseValues: castsKeys } : {}),
|
|
121
|
+
};
|
|
122
|
+
return [...propertyConflicts, methodCastsConflict];
|
|
52
123
|
}
|
|
53
124
|
/**
|
|
54
125
|
* Parse a redeclared PHP string array property (first `];` closes the array).
|
|
@@ -114,21 +185,41 @@ function classifyShadowingDrift(baseValues, childValues) {
|
|
|
114
185
|
/**
|
|
115
186
|
* Evaluate shadowing conflicts for a skipped user-editable model file.
|
|
116
187
|
*/
|
|
188
|
+
function childValuesForConflict(existingRaw, conflict, baseValues) {
|
|
189
|
+
if (!baseValues) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (conflict.property === 'casts') {
|
|
193
|
+
if (conflict.form === 'method') {
|
|
194
|
+
return parseRedeclaredCastsMethodKeys(existingRaw);
|
|
195
|
+
}
|
|
196
|
+
return parseRedeclaredCastsPropertyKeys(existingRaw);
|
|
197
|
+
}
|
|
198
|
+
return parseRedeclaredStringArray(existingRaw, conflict.property);
|
|
199
|
+
}
|
|
117
200
|
export function evaluateShadowingConflicts(existingRaw, conflicts) {
|
|
118
201
|
const stripped = stripPhpComments(existingRaw);
|
|
119
202
|
const results = [];
|
|
203
|
+
const skipsPropertyCasts = childDeclaresCastsMethod(stripped);
|
|
120
204
|
for (const conflict of conflicts) {
|
|
205
|
+
if (conflict.property === 'casts' && conflict.form !== 'method' && skipsPropertyCasts) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
121
208
|
if (hasIntentionalShadowMarker(existingRaw, conflict.property)) {
|
|
122
209
|
continue;
|
|
123
210
|
}
|
|
124
211
|
if (!new RegExp(conflict.pattern).test(stripped)) {
|
|
125
212
|
continue;
|
|
126
213
|
}
|
|
214
|
+
if (conflict.form === 'method' && conflict.property === 'casts') {
|
|
215
|
+
const body = redeclaredCastsMethodBody(existingRaw);
|
|
216
|
+
if (body !== null && body.includes('parent::casts()')) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
127
220
|
let message = conflict.hint;
|
|
128
221
|
const baseValues = conflict.baseValues && conflict.baseValues.length > 0 ? conflict.baseValues : undefined;
|
|
129
|
-
const childValues = baseValues
|
|
130
|
-
? parseRedeclaredStringArray(existingRaw, conflict.property)
|
|
131
|
-
: null;
|
|
222
|
+
const childValues = childValuesForConflict(existingRaw, conflict, baseValues);
|
|
132
223
|
const drift = classifyShadowingDrift(baseValues, childValues);
|
|
133
224
|
if (baseValues) {
|
|
134
225
|
const driftText = formatShadowingDrift(baseValues, childValues);
|
package/dist/php/types.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type ShadowingPropertyConflict = {
|
|
|
56
56
|
readonly pattern: string;
|
|
57
57
|
readonly hint: string;
|
|
58
58
|
readonly baseValues?: readonly string[];
|
|
59
|
+
/** Property redeclaration vs method redeclaration (issue #245: casts()). */
|
|
60
|
+
readonly form?: 'property' | 'method';
|
|
59
61
|
};
|
|
60
62
|
/** Category of base file for modular path resolution. */
|
|
61
63
|
export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
|