@@ -588,7 +588,8 @@ private function gatherColumn(
588588 *
589589 * @phpstan-param array<string, array{
590590 * foreignTableName: string,
591- * foreignColumns: list<string>
591+ * foreignColumns: list<string>,
592+ * fkOptions: array{onDelete?: string, deferrable?: bool, deferred?: bool}
592593 * }> $addedFks
593594 * @phpstan-param array<string, bool> $blacklistedFks
594595 *
@@ -705,7 +706,8 @@ private function getDefiningClass(ClassMetadata $class, string $referencedColumn
705706 * @phpstan-param list<string> $primaryKeyColumns
706707 * @phpstan-param array<string, array{
707708 * foreignTableName: string,
708- * foreignColumns: list<string>
709+ * foreignColumns: list<string>,
710+ * fkOptions: array{onDelete?: string, deferrable?: bool, deferred?: bool}
709711 * }> $addedFks
710712 * @phpstan-param array<string,bool> $blacklistedFks
711713 *
@@ -720,8 +722,9 @@ private function gatherRelationJoinColumns(
720722 array &$ addedFks ,
721723 array &$ blacklistedFks ,
722724 ): void {
723- $ localColumns = [];
724- $ foreignColumns = [];
725+ $ localColumns = [];
726+ $ foreignColumns = [];
727+ /** @var array{onDelete?: string, deferrable?: bool, deferred?: bool} $fkOptions */
725728 $ fkOptions = [];
726729 $ foreignTableName = $ this ->quoteStrategy ->getTableName ($ class , $ this ->platform );
727730 $ uniqueConstraints = [];
@@ -806,11 +809,37 @@ private function gatherRelationJoinColumns(
806809 }
807810
808811 $ compositeName = $ this ->getAssetName ($ theJoinTable ) . '. ' . implode ('' , $ localColumns );
809- if (
810- isset ($ addedFks [$ compositeName ])
811- && ($ foreignTableName !== $ addedFks [$ compositeName ]['foreignTableName ' ]
812- || 0 < count (array_diff ($ foreignColumns , $ addedFks [$ compositeName ]['foreignColumns ' ])))
813- ) {
812+
813+ // Check if an FK constraint already exists for this composite key (table + columns)
814+ if (isset ($ addedFks [$ compositeName ])) {
815+ $ existingFk = $ addedFks [$ compositeName ];
816+
817+ // Determine if the new FK is identical to the existing one
818+ $ isForeignTableIdentical = $ foreignTableName === $ existingFk ['foreignTableName ' ];
819+ $ areForeignColumnsIdentical = count (array_diff ($ foreignColumns , $ existingFk ['foreignColumns ' ])) === 0
820+ && count (array_diff ($ existingFk ['foreignColumns ' ], $ foreignColumns )) === 0 ;
821+
822+ // Compare FK options that affect constraint identity (onDelete, deferrable, deferred)
823+ $ existingOptions = $ existingFk ['fkOptions ' ];
824+ $ onDeleteMatches = ($ fkOptions ['onDelete ' ] ?? null )
825+ === ($ existingOptions ['onDelete ' ] ?? null );
826+ $ deferrableMatches = ($ fkOptions ['deferrable ' ] ?? null )
827+ === ($ existingOptions ['deferrable ' ] ?? null );
828+ $ deferredMatches = ($ fkOptions ['deferred ' ] ?? null )
829+ === ($ existingOptions ['deferred ' ] ?? null );
830+ $ areOptionsIdentical = $ onDeleteMatches && $ deferrableMatches && $ deferredMatches ;
831+
832+ if ($ isForeignTableIdentical && $ areForeignColumnsIdentical && $ areOptionsIdentical ) {
833+ // Identical FK already registered - skip adding duplicate.
834+ // This prevents attempting to overwrite an existing FK constraint with an identical one.
835+ // This scenario occurs in Single Table Inheritance (STI) when multiple child entities
836+ // define their own associations using the same join column to the same target entity.
837+ // Since all STI entities share the same physical table, having identical FK constraints
838+ // is semantically correct and necessary for database normalization.
839+ return ;
840+ }
841+
842+ // FK exists but is different (conflicting FK) - drop the existing one and blacklist
814843 foreach ($ theJoinTable ->getForeignKeys () as $ fkName => $ key ) {
815844 if (
816845 class_exists (ForeignKeyConstraintEditor::class)
@@ -835,7 +864,13 @@ class_exists(ForeignKeyConstraintEditor::class)
835864
836865 $ blacklistedFks [$ compositeName ] = true ;
837866 } elseif (! isset ($ blacklistedFks [$ compositeName ])) {
838- $ addedFks [$ compositeName ] = ['foreignTableName ' => $ foreignTableName , 'foreignColumns ' => $ foreignColumns ];
867+ // No existing FK and not blacklisted - add the new FK constraint
868+ // Store FK details including options that affect constraint identity
869+ $ addedFks [$ compositeName ] = [
870+ 'foreignTableName ' => $ foreignTableName ,
871+ 'foreignColumns ' => $ foreignColumns ,
872+ 'fkOptions ' => $ fkOptions ,
873+ ];
839874 $ theJoinTable ->addForeignKeyConstraint (
840875 $ foreignTableName ,
841876 $ localColumns ,
0 commit comments