Duplicate definition of column for a class with InheritanceType and that extend another class #7351

Open
opened 2026-01-22 15:50:29 +01:00 by admin · 9 comments
Owner

Originally created by @carross-tlhuillier on GitHub (Mar 29, 2024).

BC Break Report

Q A
BC Break yes
Version 3.1.1

Summary

If a class has InheritanceType attribute and it extend another class that has at least a property, it generate an error duplicate definication of column the properties in the extended class for orm v3.1.1 and v3.1.0, but worked for v2.7.5.

In MappingException.php line 420:
Duplicate definition of column 'id' on entity 'Entity\CategoryLink' in a field or discriminator column mapping. 

How to reproduce

The MappedSuperclass

namespace MappedSuperClass;

use \Doctrine\ORM\Mapping as ORM;

#[ORM\MappedSuperclass]
abstract class AbstractEntity {

    #[ORM\Column(name: 'id', type: 'integer')]
    #[ORM\Id]
    #[ORM\GeneratedValue]
    protected ?int $id = null;
}

The class with InheritanceType attribute

namespace Entity;

use \Doctrine\ORM\Mapping as ORM;
use \MappedSuperClass\AbstractEntity;

/**
 * @property mixed $Linked
 */
#[ORM\Entity]
#[ORM\Table(name: 'stock_tree')]
#[ORM\InheritanceType('JOINED')]
#[ORM\DiscriminatorColumn(name: 'discriminator', type: 'string')]
#[ORM\DiscriminatorMap([
    'Category' => \Entity\CategoryLink::class
])]
class StockTreeEntity extends AbstractEntity {
}

An inheritor class

namespace Entity;

use \Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'categories_link')]
class CategoryLink extends StockTreeEntity {

}

Run doctrine orm:schema-tool:update --dump-sql, generate the error

In MappingException.php line 420:
Duplicate definition of column 'id' on entity 'Entity\CategoryLink' in a field or discriminator column mapping.
Originally created by @carross-tlhuillier on GitHub (Mar 29, 2024). ### BC Break Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | yes | Version | 3.1.1 #### Summary If a class has InheritanceType attribute and it extend another class that has at least a property, it generate an error duplicate definication of column the properties in the extended class for orm v3.1.1 and v3.1.0, but worked for v2.7.5. ``` In MappingException.php line 420: Duplicate definition of column 'id' on entity 'Entity\CategoryLink' in a field or discriminator column mapping. ``` #### How to reproduce The MappedSuperclass ``` namespace MappedSuperClass; use \Doctrine\ORM\Mapping as ORM; #[ORM\MappedSuperclass] abstract class AbstractEntity { #[ORM\Column(name: 'id', type: 'integer')] #[ORM\Id] #[ORM\GeneratedValue] protected ?int $id = null; } ``` The class with InheritanceType attribute ``` namespace Entity; use \Doctrine\ORM\Mapping as ORM; use \MappedSuperClass\AbstractEntity; /** * @property mixed $Linked */ #[ORM\Entity] #[ORM\Table(name: 'stock_tree')] #[ORM\InheritanceType('JOINED')] #[ORM\DiscriminatorColumn(name: 'discriminator', type: 'string')] #[ORM\DiscriminatorMap([ 'Category' => \Entity\CategoryLink::class ])] class StockTreeEntity extends AbstractEntity { } ``` An inheritor class ``` namespace Entity; use \Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ORM\Table(name: 'categories_link')] class CategoryLink extends StockTreeEntity { } ``` Run `doctrine orm:schema-tool:update --dump-sql`, generate the error ``` In MappingException.php line 420: Duplicate definition of column 'id' on entity 'Entity\CategoryLink' in a field or discriminator column mapping. ```
Author
Owner

@mpdude commented on GitHub (Dec 19, 2025):

Can you put StockTreeEntry in the discriminator map as well?

Can you make sure that all three classes/namespaces are registered in the mapping driver configurations?

@mpdude commented on GitHub (Dec 19, 2025): Can you put StockTreeEntry in the discriminator map as well? Can you make sure that all three classes/namespaces are registered in the mapping driver configurations?
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

I confirm this bug, I experience it on many classes after upgrade in a project.

@pounard commented on GitHub (Jan 20, 2026): I confirm this bug, I experience it on many classes after upgrade in a project.
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

My use case:

#[ORM\MappedSuperclass]
abstract class AbstractRestEntity implements RestEntity
{
    #[OA\Property]
    #[ORM\Id]
    #[ORM\Column(type: "integer")]
    #[ORM\GeneratedValue(strategy: "AUTO")]
    protected ?int $id = null;

    // ... other properties
}

#[ORM\MappedSuperclass]
abstract class Transaction extends AbstractRestEntity
{
    #[ORM\Column(type: "string", nullable: true)]
    protected ?string $name = null;

    // ... own properties (no duplicates, no traits)
}

#[ORM\Entity]
#[ORM\Table(name: "income")]
class Income extends Transaction
{
    // ... own properties (no duplicates, no traits)
}

And many others, always the same schema, they all inherit from the root AbstractRestEntity class, it's always "X extends Y, Y extends AbstractRestEntity".

I'm using 3.6.1 version.

@pounard commented on GitHub (Jan 20, 2026): My use case: ```php #[ORM\MappedSuperclass] abstract class AbstractRestEntity implements RestEntity { #[OA\Property] #[ORM\Id] #[ORM\Column(type: "integer")] #[ORM\GeneratedValue(strategy: "AUTO")] protected ?int $id = null; // ... other properties } #[ORM\MappedSuperclass] abstract class Transaction extends AbstractRestEntity { #[ORM\Column(type: "string", nullable: true)] protected ?string $name = null; // ... own properties (no duplicates, no traits) } #[ORM\Entity] #[ORM\Table(name: "income")] class Income extends Transaction { // ... own properties (no duplicates, no traits) } ``` And many others, always the same schema, they all inherit from the root `AbstractRestEntity` class, it's always "X extends Y, Y extends AbstractRestEntity". I'm using 3.6.1 version.
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

Problem seems to be in trait method ReflectionBasedDriver::isRepeatedPropertyDeclaration(), when reaching here, in the above use case, in ClassMetadata::$fieldMappings['id'] the FieldMapping::$declared string value is Transaction instead of being AbstractRestEntity, which means that the following code:

    private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
    {
        $declaringClass = $property->class;

        if (
            isset($metadata->fieldMappings[$property->name]->declared)
            && $metadata->fieldMappings[$property->name]->declared === $declaringClass
        ) {
            return true;
        }

will not match and ReflectionBasedDriver::isRepeatedPropertyDeclaration() will return false.

Because in \ReflectionProperty::$class is set to AbstractRestEntity (it is where it's really being declared on the PHP code side).

I guess that the problem is when the FieldMapping::$declared is set, when inheritance has more than one level, it's been overridden with lower inheritance tree class name instead of being kept where it was declared.

@pounard commented on GitHub (Jan 20, 2026): Problem seems to be in trait method `ReflectionBasedDriver::isRepeatedPropertyDeclaration()`, when reaching here, in the above use case, in `ClassMetadata::$fieldMappings['id']` the `FieldMapping::$declared` string value is `Transaction` instead of being `AbstractRestEntity`, which means that the following code: ```php private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool { $declaringClass = $property->class; if ( isset($metadata->fieldMappings[$property->name]->declared) && $metadata->fieldMappings[$property->name]->declared === $declaringClass ) { return true; } ``` will not match and `ReflectionBasedDriver::isRepeatedPropertyDeclaration()` will return `false`. Because in `\ReflectionProperty::$class` is set to `AbstractRestEntity` (it is where it's really being declared on the PHP code side). I guess that the problem is when the `FieldMapping::$declared` is set, when inheritance has more than one level, it's been overridden with lower inheritance tree class name instead of being kept where it was declared.
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

When I reach this:

    /**
     * Adds inherited fields to the subclass mapping.
     */
    private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
    {
        foreach ($parentClass->fieldMappings as $mapping) {
            $subClassMapping = clone $mapping;
            $this->addMappingInheritanceInformation($subClassMapping, $parentClass);
            $subClass->addInheritedFieldMapping($subClassMapping);
        }

        foreach ($parentClass->propertyAccessors as $name => $field) {
            $subClass->propertyAccessors[$name] = $field;
        }
    }

With $parentClass->name being Transaction, my id field is broken, it doesn't have the declared attribute set to the root parent class AbstractRestEntity which it should have. It's simply null. This is where the bug happens, because the mapping does not have the root parent class name in its declared property, the property is being wrongly set to the current class instead of the parent one.

@pounard commented on GitHub (Jan 20, 2026): When I reach this: ```php /** * Adds inherited fields to the subclass mapping. */ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void { foreach ($parentClass->fieldMappings as $mapping) { $subClassMapping = clone $mapping; $this->addMappingInheritanceInformation($subClassMapping, $parentClass); $subClass->addInheritedFieldMapping($subClassMapping); } foreach ($parentClass->propertyAccessors as $name => $field) { $subClass->propertyAccessors[$name] = $field; } } ``` With `$parentClass->name` being `Transaction`, my `id` field is broken, it doesn't have the `declared` attribute set to the root parent class `AbstractRestEntity` which it should have. It's simply `null`. This is where the bug happens, because the mapping does not have the root parent class name in its `declared` property, the property is being wrongly set to the current class instead of the parent one.
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

I don't known the ORM internals enough, but I'd say that when an class with the MappedSuperclass attribute is instrospected, parenting information is lost in the way.

@pounard commented on GitHub (Jan 20, 2026): I don't known the ORM internals enough, but I'd say that when an class with the `MappedSuperclass` attribute is instrospected, parenting information is lost in the way.
Author
Owner

@pounard commented on GitHub (Jan 20, 2026):

OK! No bug in the end, I found the reason. My classes are not in the same namespace, but the namespace the root abstract class is within is not covered by a MappingDriver!

This causes the class to be excluded from parent class list when building child classes metadata, and properties are found and attached in the closest parent in the inheritance tree which is in a MappingDriver path.

In my case, AbstractRestEntity was not in a declared namespace, but Transaction was. So Transaction inherited from all AbstractRestEntity properties as if it was its own.

It's kind of tricky, and I think this is kind of a wrong behavior, any parent class should be considered using the same driver as the child if no namespace is declared, it would prevent this kind of very weird behavior.

The magic that pointed this to me lies in the AbstractClassMetadataFactory class in:

    protected function getParentClasses(string $name): array
    {
        // Collect parent classes, ignoring transient (not-mapped) classes.
        $parentClasses = [];

        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
            if ($this->getDriver()->isTransient($parentClass)) {
                continue;
            }

            $parentClasses[] = $parentClass;
        }

        return $parentClasses;
    }

This was a wild journey the head in xdebug to find this, ORM code is not simple (not complaining, the tool is really great).

@pounard commented on GitHub (Jan 20, 2026): OK! No bug in the end, I found the reason. My classes are not in the same namespace, but the namespace the root abstract class is within is not covered by a MappingDriver! This causes the class to be excluded from parent class list when building child classes metadata, and properties are found and attached in the closest parent in the inheritance tree which is in a MappingDriver path. In my case, `AbstractRestEntity` was not in a declared namespace, but `Transaction` was. So `Transaction` inherited from all `AbstractRestEntity` properties as if it was its own. It's kind of tricky, and I think this is kind of a wrong behavior, any parent class should be considered using the same driver as the child if no namespace is declared, it would prevent this kind of very weird behavior. The magic that pointed this to me lies in the `AbstractClassMetadataFactory` class in: ```php protected function getParentClasses(string $name): array { // Collect parent classes, ignoring transient (not-mapped) classes. $parentClasses = []; foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) { if ($this->getDriver()->isTransient($parentClass)) { continue; } $parentClasses[] = $parentClass; } return $parentClasses; } ``` This was a wild journey the head in xdebug to find this, ORM code is not simple (not complaining, the tool is really great).
Author
Owner

@davidnectarestudio commented on GitHub (Jan 21, 2026):

@pounard I think that it is same problem here https://github.com/doctrine/orm/issues/12246

@davidnectarestudio commented on GitHub (Jan 21, 2026): @pounard I think that it is same problem here https://github.com/doctrine/orm/issues/12246
Author
Owner

@mpdude commented on GitHub (Jan 21, 2026):

I have seen the pattern that a class not being covered by mapping configuration leads to "duplicate column" error way too oftern – just debugging one of these cases in a project myself, but at least I know what to look out for.

I'd need more time to think about under which conditions and where a helpful error message could be given.

@mpdude commented on GitHub (Jan 21, 2026): I have seen the pattern that a class not being covered by mapping configuration leads to "duplicate column" error way too oftern – just debugging one of these cases in a project myself, but at least I know what to look out for. I'd need more time to think about under which conditions and where a helpful error message could be given.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7351