88use Doctrine \DBAL \Platforms \AbstractPlatform ;
99use Doctrine \DBAL \Schema \AbstractAsset ;
1010use Doctrine \DBAL \Schema \AbstractSchemaManager ;
11+ use Doctrine \DBAL \Schema \ColumnEditor ;
1112use Doctrine \DBAL \Schema \ComparatorConfig ;
1213use Doctrine \DBAL \Schema \DefaultExpression ;
1314use Doctrine \DBAL \Schema \DefaultExpression \CurrentDate ;
2122use Doctrine \DBAL \Schema \NamedObject ;
2223use Doctrine \DBAL \Schema \PrimaryKeyConstraint ;
2324use Doctrine \DBAL \Schema \Schema ;
25+ use Doctrine \DBAL \Schema \SchemaConfig ;
2426use Doctrine \DBAL \Schema \Table ;
2527use Doctrine \DBAL \Types \Types ;
2628use Doctrine \Deprecations \Deprecation ;
@@ -199,16 +201,40 @@ public function getSchemaFromMetadata(array $classes): Schema
199201 $ addedFks = [];
200202 $ blacklistedFks = [];
201203
204+ // For new schema editor API: collect join tables to add at the end
205+ // This ensures entity tables appear before join tables in the final schema
206+ $ joinTablesToAdd = [];
207+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
208+ $ usingNewSchemaAPI = method_exists (Schema::class, 'edit ' );
209+
202210 foreach ($ classes as $ class ) {
203211 if ($ this ->processingNotRequired ($ class , $ processedClasses )) {
204212 continue ;
205213 }
206214
207- $ table = $ schema ->createTable ($ this ->quoteStrategy ->getTableName ($ class , $ this ->platform ));
215+ // Create table object
216+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
217+ if (method_exists (Schema::class, 'edit ' )) {
218+ $ table = new Table (
219+ $ this ->quoteStrategy ->getTableName ($ class , $ this ->platform ),
220+ [],
221+ [],
222+ [],
223+ [],
224+ [],
225+ $ metadataSchemaConfig ->toTableConfiguration (),
226+ );
227+ // Add default table options (charset, collation, engine, etc.)
228+ foreach ($ metadataSchemaConfig ->getDefaultTableOptions () as $ option => $ value ) {
229+ $ table ->addOption ($ option , $ value );
230+ }
231+ } else {
232+ $ table = $ schema ->createTable ($ this ->quoteStrategy ->getTableName ($ class , $ this ->platform ));
233+ }
208234
209235 if ($ class ->isInheritanceTypeSingleTable ()) {
210236 $ this ->gatherColumns ($ class , $ table );
211- $ this ->gatherRelationsSql ($ class , $ table , $ schema , $ addedFks , $ blacklistedFks );
237+ $ this ->gatherRelationsSql ($ class , $ table , $ schema , $ addedFks , $ blacklistedFks, $ metadataSchemaConfig , $ joinTablesToAdd );
212238
213239 // Add the discriminator column
214240 $ this ->addDiscriminatorColumnDefinition ($ class , $ table );
@@ -222,7 +248,15 @@ public function getSchemaFromMetadata(array $classes): Schema
222248 foreach ($ class ->subClasses as $ subClassName ) {
223249 $ subClass = $ this ->em ->getClassMetadata ($ subClassName );
224250 $ this ->gatherColumns ($ subClass , $ table );
225- $ this ->gatherRelationsSql ($ subClass , $ table , $ schema , $ addedFks , $ blacklistedFks );
251+ $ this ->gatherRelationsSql (
252+ $ subClass ,
253+ $ table ,
254+ $ schema ,
255+ $ addedFks ,
256+ $ blacklistedFks ,
257+ $ metadataSchemaConfig ,
258+ $ joinTablesToAdd ,
259+ );
226260 $ processedClasses [$ subClassName ] = true ;
227261 }
228262 } elseif ($ class ->isInheritanceTypeJoined ()) {
@@ -233,7 +267,15 @@ public function getSchemaFromMetadata(array $classes): Schema
233267 }
234268 }
235269
236- $ this ->gatherRelationsSql ($ class , $ table , $ schema , $ addedFks , $ blacklistedFks );
270+ $ this ->gatherRelationsSql (
271+ $ class ,
272+ $ table ,
273+ $ schema ,
274+ $ addedFks ,
275+ $ blacklistedFks ,
276+ $ metadataSchemaConfig ,
277+ $ joinTablesToAdd ,
278+ );
237279
238280 // Add the discriminator column only to the root table
239281 if ($ class ->name === $ class ->rootEntityName ) {
@@ -253,7 +295,18 @@ public function getSchemaFromMetadata(array $classes): Schema
253295 $ this ->platform ,
254296 );
255297 // TODO: This seems rather hackish, can we optimize it?
256- $ table ->getColumn ($ columnName )->setAutoincrement (false );
298+ // Use new column editor API only when new schema editor API is available (DBAL 4.5+)
299+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API for version detection)
300+ if (method_exists (Schema::class, 'edit ' )) {
301+ // New API: modify column using table editor (creates new table object)
302+ // This is safe because we'll add the table to schema later after all modifications
303+ $ table = $ table ->edit ()->modifyColumnByUnquotedName (
304+ $ columnName ,
305+ static fn (ColumnEditor $ column ) => $ column ->setAutoincrement (false ),
306+ )->create ();
307+ } else {
308+ $ table ->getColumn ($ columnName )->setAutoincrement (false );
309+ }
257310
258311 $ pkColumns [] = $ columnName ;
259312 $ inheritedKeyColumns [] = $ columnName ;
@@ -306,7 +359,15 @@ public function getSchemaFromMetadata(array $classes): Schema
306359 }
307360 } else {
308361 $ this ->gatherColumns ($ class , $ table );
309- $ this ->gatherRelationsSql ($ class , $ table , $ schema , $ addedFks , $ blacklistedFks );
362+ $ this ->gatherRelationsSql (
363+ $ class ,
364+ $ table ,
365+ $ schema ,
366+ $ addedFks ,
367+ $ blacklistedFks ,
368+ $ metadataSchemaConfig ,
369+ $ joinTablesToAdd ,
370+ );
310371 }
311372
312373 $ pkColumns = [];
@@ -377,6 +438,15 @@ public function getSchemaFromMetadata(array $classes): Schema
377438
378439 $ processedClasses [$ class ->name ] = true ;
379440
441+ // Add the fully populated table to the schema
442+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
443+ if (method_exists (Schema::class, 'edit ' )) {
444+ // @phpstan-ignore method.notFound (Using unreleased Schema::edit() API)
445+ $ schemaEditor = $ schema ->edit ();
446+ $ schemaEditor ->addTable ($ table );
447+ $ schema = $ schemaEditor ->create ();
448+ }
449+
380450 if ($ class ->isIdGeneratorSequence () && $ class ->name === $ class ->rootEntityName ) {
381451 $ seqDef = $ class ->sequenceGeneratorDefinition ;
382452 $ quotedName = $ this ->quoteStrategy ->getSequenceName ($ seqDef , $ class , $ this ->platform );
@@ -415,6 +485,17 @@ public function getSchemaFromMetadata(array $classes): Schema
415485 }
416486 }
417487
488+ // Add all collected join tables at the end (for new schema API only)
489+ // This ensures entity tables appear before join tables in the final schema
490+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
491+ if (method_exists (Schema::class, 'edit ' )) {
492+ foreach ($ joinTablesToAdd as $ joinTable ) {
493+ $ schemaEditor = $ schema ->edit ();
494+ $ schemaEditor ->addTable ($ joinTable );
495+ $ schema = $ schemaEditor ->create ();
496+ }
497+ }
498+
418499 if ($ eventManager ->hasListeners (ToolEvents::postGenerateSchema)) {
419500 $ schemaEventArgs = new GenerateSchemaEventArgs ($ this ->em , $ schema );
420501 $ eventManager ->dispatchEvent (
@@ -614,15 +695,18 @@ private function gatherColumn(
614695 * fkOptions: array{onDelete?: string, deferrable?: bool, deferred?: bool}
615696 * }> $addedFks
616697 * @phpstan-param array<string, bool> $blacklistedFks
698+ * @phpstan-param list<Table> $joinTablesToAdd
617699 *
618700 * @throws NotSupported
619701 */
620702 private function gatherRelationsSql (
621703 ClassMetadata $ class ,
622704 Table $ table ,
623- Schema $ schema ,
705+ Schema & $ schema ,
624706 array &$ addedFks ,
625707 array &$ blacklistedFks ,
708+ SchemaConfig $ schemaConfig ,
709+ array &$ joinTablesToAdd ,
626710 ): void {
627711 foreach ($ class ->associationMappings as $ id => $ mapping ) {
628712 if (isset ($ mapping ->inherited ) && ! in_array ($ id , $ class ->identifier , true )) {
@@ -647,12 +731,29 @@ private function gatherRelationsSql(
647731 // create join table
648732 $ joinTable = $ mapping ->joinTable ;
649733
650- $ theJoinTable = $ schema ->createTable (
651- $ this ->quoteStrategy ->getJoinTableName ($ mapping , $ foreignClass , $ this ->platform ),
652- );
653-
654- foreach ($ joinTable ->options as $ key => $ val ) {
655- $ theJoinTable ->addOption ($ key , $ val );
734+ $ tableName = $ this ->quoteStrategy ->getJoinTableName ($ mapping , $ foreignClass , $ this ->platform );
735+
736+ // Create the join table object
737+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
738+ if (method_exists (Schema::class, 'edit ' )) {
739+ $ theJoinTable = new Table (
740+ $ tableName ,
741+ [],
742+ [],
743+ [],
744+ [],
745+ $ joinTable ->options ,
746+ $ schemaConfig ->toTableConfiguration (),
747+ );
748+ // Add default table options (charset, collation, engine, etc.)
749+ foreach ($ schemaConfig ->getDefaultTableOptions () as $ option => $ value ) {
750+ $ theJoinTable ->addOption ($ option , $ value );
751+ }
752+ } else {
753+ $ theJoinTable = $ schema ->createTable ($ tableName );
754+ foreach ($ joinTable ->options as $ key => $ val ) {
755+ $ theJoinTable ->addOption ($ key , $ val );
756+ }
656757 }
657758
658759 $ primaryKeyColumns = [];
@@ -680,6 +781,13 @@ private function gatherRelationsSql(
680781 );
681782
682783 self ::addPrimaryKeyConstraint ($ theJoinTable , $ primaryKeyColumns );
784+
785+ // For new schema API: collect join table for deferred addition at end
786+ // For old schema API: table already added via createTable() above
787+ // @phpstan-ignore function.impossibleType (Using unreleased Schema::edit() API)
788+ if (method_exists (Schema::class, 'edit ' )) {
789+ $ joinTablesToAdd [] = $ theJoinTable ;
790+ }
683791 }
684792 }
685793 }
0 commit comments