Skip to content

Commit c848880

Browse files
update
1 parent 771768a commit c848880

File tree

1 file changed

+142
-37
lines changed

1 file changed

+142
-37
lines changed

src/Migrations/Traits/MigrationTrait.php

Lines changed: 142 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -183,77 +183,182 @@ private static function scanDirectoryFiles($directory)
183183
$value = rtrim($directory, '/') . "/{$value}";
184184
});
185185

186-
dd(
187-
self::sortParentFiles($files)
188-
);
189-
186+
// Sort with enhanced dependency-aware ordering
190187
return self::sortParentFiles($files);
191188
}
192189

193190
/**
194-
* Sort parent files according to their names
191+
* Sort parent files according to their names and detected foreign key dependencies
195192
*
196-
* Ensures that base tables (e.g., "ads", "users") are migrated
197-
* before their derivative/child tables (e.g., "ads_data", "users_address", "ads_info").
193+
* Ensures parent/base tables (e.g., "blogs_categories") are executed before
194+
* child tables that reference them (e.g., "blogs" referencing blogs_categories),
195+
* while also keeping base-before-derivative ordering (e.g., ads before ads_data/info).
198196
*
199-
* @param array $files
197+
* @param array $files Full paths to migration files
200198
* @return array
201199
*/
202200
private static function sortParentFiles(array $files)
203201
{
204-
// Robust sort: parent tables before child tables of the same base
205-
$parse = function (string $path): array {
202+
// Helper: parse name cues from filename
203+
$parseNameFromFilename = function (string $path): array {
206204
$name = basename($path);
207-
// extract between "create_" and "_table"
208205
if (preg_match('/create_(.+)_table\.php$/', $name, $m)) {
209-
$full = $m[1]; // e.g., "ads", "ads_data", "users_address"
210-
$parts = explode('_', $full); // split by underscore
211-
$base = $parts[0]; // the base entity
206+
$full = $m[1];
207+
$parts = explode('_', $full);
212208
return [
209+
'file' => $path,
213210
'name' => $name,
214-
'full' => $full,
215-
'base' => $base,
211+
'full' => $full, // e.g., blogs, blogs_categories
216212
'parts' => $parts,
213+
'base' => $parts[0] ?? $full,
217214
'is_parent' => count($parts) === 1,
218215
];
219216
}
220-
// Non-standard name: treat as parent so it runs early
221217
return [
218+
'file' => $path,
222219
'name' => $name,
223220
'full' => $name,
224-
'base' => $name,
225221
'parts' => [$name],
222+
'base' => $name,
226223
'is_parent' => true,
227224
];
228225
};
229226

230-
usort($files, function (string $a, string $b) use ($parse) {
231-
$ai = $parse($a);
232-
$bi = $parse($b);
227+
// Helper: extract created table and referenced tables from file content
228+
$parseMigrationContent = function (string $path) use ($parseNameFromFilename): array {
229+
$info = $parseNameFromFilename($path);
230+
try {
231+
$content = File::get($path);
232+
} catch (\Throwable $e) {
233+
$content = '';
234+
}
235+
236+
// Detect created table: Schema::create('table', ...) or Schema::create("table", ...)
237+
$creates = null;
238+
if (preg_match("/Schema::create\(['\"]([a-zA-Z0-9_]+)['\"]/", $content, $m)) {
239+
$creates = $m[1];
240+
} else {
241+
// fallback to filename cue
242+
$creates = $info['full'];
243+
}
233244

234-
// Same base entity (e.g., "ads" vs "ads_data", "users" vs "users_address")
235-
if ($ai['base'] === $bi['base']) {
236-
// Parent must come before children
237-
if ($ai['is_parent'] !== $bi['is_parent']) {
238-
return $ai['is_parent'] ? -1 : 1;
245+
// Detect referenced tables via ->on('table') or ->on("table")
246+
$refs = [];
247+
if (preg_match_all("/->on\(['\"]([a-zA-Z0-9_]+)['\"]\)/", $content, $mm)) {
248+
$refs = array_values(array_unique($mm[1]));
249+
}
250+
251+
return [
252+
'file' => $path,
253+
'name' => $info['name'],
254+
'base' => $info['base'],
255+
'full' => $info['full'],
256+
'parts' => $info['parts'],
257+
'is_parent' => $info['is_parent'],
258+
'creates' => $creates,
259+
'refs' => $refs,
260+
];
261+
};
262+
263+
// Parse all migrations
264+
$nodes = [];
265+
foreach ($files as $f) {
266+
// only consider php files
267+
if (!is_string($f)) continue;
268+
if (substr($f, -4) !== '.php') continue;
269+
$nodes[] = $parseMigrationContent($f);
270+
}
271+
272+
// Map created table -> node index
273+
$creatorIndexByTable = [];
274+
foreach ($nodes as $i => $n) {
275+
if (!empty($n['creates'])) {
276+
$creatorIndexByTable[$n['creates']] = $i;
277+
}
278+
}
279+
280+
// Build dependency graph (Kahn): edge creator -> dependent
281+
$adj = array_fill(0, count($nodes), []);
282+
$inDegree = array_fill(0, count($nodes), 0);
283+
284+
foreach ($nodes as $i => $n) {
285+
foreach ($n['refs'] as $rt) {
286+
if (isset($creatorIndexByTable[$rt])) {
287+
$p = $creatorIndexByTable[$rt]; // parent index
288+
if ($p !== $i) {
289+
$adj[$p][] = $i;
290+
$inDegree[$i]++;
291+
}
239292
}
293+
}
294+
}
295+
296+
// Prepare initial queue (in-degree 0). Use tie-breaker to keep base/parent-first preference
297+
$queue = [];
298+
foreach ($nodes as $i => $n) {
299+
if ($inDegree[$i] === 0) {
300+
$queue[] = $i;
301+
}
302+
}
303+
304+
$tieBreaker = function (int $a, int $b) use ($nodes) {
305+
$na = $nodes[$a];
306+
$nb = $nodes[$b];
307+
// Same base: parent-first, fewer parts first, then alpha by full
308+
if ($na['base'] === $nb['base']) {
309+
if ($na['is_parent'] !== $nb['is_parent']) {
310+
return $na['is_parent'] ? -1 : 1;
311+
}
312+
$cmpParts = count($na['parts']) <=> count($nb['parts']);
313+
if ($cmpParts !== 0) return $cmpParts;
314+
return strcmp($na['full'], $nb['full']);
315+
}
316+
// Different bases: alphabetical by created table name fallback
317+
return strcmp($na['creates'] ?? $na['name'], $nb['creates'] ?? $nb['name']);
318+
};
319+
320+
usort($queue, $tieBreaker);
240321

241-
// Both children or both parents:
242-
// fewer segments first (e.g., users_address before users_address_history)
243-
$cmpParts = count($ai['parts']) <=> count($bi['parts']);
244-
if ($cmpParts !== 0) {
245-
return $cmpParts;
322+
$ordered = [];
323+
while (!empty($queue)) {
324+
$curr = array_shift($queue);
325+
$ordered[] = $curr;
326+
foreach ($adj[$curr] as $v) {
327+
$inDegree[$v]--;
328+
if ($inDegree[$v] === 0) {
329+
$queue[] = $v;
246330
}
331+
}
332+
// keep queue stable
333+
if (!empty($queue)) {
334+
usort($queue, $tieBreaker);
335+
}
336+
}
247337

248-
// then alphabetical by the entity part for stability
249-
return strcmp($ai['full'], $bi['full']);
338+
// If there is a cycle or unresolved deps, append remaining in a safe order
339+
if (count($ordered) < count($nodes)) {
340+
$remaining = [];
341+
foreach ($nodes as $i => $_) {
342+
if (!in_array($i, $ordered, true)) {
343+
$remaining[] = $i;
344+
}
250345
}
346+
usort($remaining, $tieBreaker);
347+
$ordered = array_merge($ordered, $remaining);
348+
}
251349

252-
// Different bases: keep natural alphabetical (includes timestamp prefix)
253-
return strcmp($ai['name'], $bi['name']);
254-
});
350+
// Map back to file paths in computed order
351+
$result = [];
352+
foreach ($ordered as $idx) {
353+
$result[] = $nodes[$idx]['file'];
354+
}
355+
356+
// Fallback: if for some reason result is empty, use filename-based ordering
357+
if (empty($result)) {
358+
$result = $files; // as-is
359+
}
255360

256-
return $files;
361+
return $result;
257362
}
258363

259364
/**

0 commit comments

Comments
 (0)