@@ -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