2.14.2: Entity Inheritance (at least STI) with intermediate abstract class is broken #7133

Open
opened 2026-01-22 15:45:22 +01:00 by admin · 0 comments
Owner

Originally created by @ghost on GitHub (Apr 13, 2023).

BC Break Report

The Single Table Inheritance Entity Inheritance is defined in your documentation with

#[DiscriminatorMap] declares the possible values for the discriminator column and maps them to class names in the hierarchy. This discriminator map has to declare all non-abstract entity classes that exist in that particular inheritance hierarchy. That includes the root as well as any intermediate entity classes, given they are not abstract.

but will fail on EntityManager::find() with an intermediate abstract entity class:

Undefined index: "DoctrineOrmTest\Intermediate"

vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php:157
vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php:128
vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:1092
vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:748
vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:768
vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:521
vendor/doctrine/orm/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php:205

as the code iterates over all subclasses (abstract and non-abstract) and expects them in the discriminator map.

Q A
BC Break yes
Version 2.14.2

Summary

Apparently the intermediate abstract class seems to be expected in the discriminator map, even though the documentation states otherwise.

Not quite sure if the ClassMetadataInfo::subClasses iteration should directly access the discriminator values discrValues in SingleTablePersister::getSelectConditionDiscriminatorValueSQL()

        $discrValues = array_flip($this->class->discriminatorMap);

        foreach ($this->class->subClasses as $subclassName) {
            $values[] = $this->conn->quote($discrValues[$subclassName]);
        }

as subClasses is documented with having also abstract classes

     * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all                  
     * <em>entity</em> subclasses of this class. These may also be abstract classes. 

where those are not to be expected in the discriminator map.

Previous behavior

It worked ™️

Current behavior

Access violation on runtime, but not on validation.

How to reproduce

Assume the following example of entities:

namespace DoctrineOrmTest;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorMap({"1" = "DoctrineOrmTest\Concrete"})
 * @ORM\Entity()
 */
abstract class Base
{
    /**
     * @ORM\Column
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private int $id;
}
namespace DoctrineOrmTest;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 */
abstract class Intermediate extends Base
{
}
namespace DoctrineOrmTest;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 */
class Concrete extends Intermediate
{
}

plus some boilerplate:

composer.json

{
    "require": {
        "doctrine/annotations": "^2.0",
        "doctrine/orm": "^2.14.1",
        "symfony/cache": "^5.4"
    },
    "autoload": {
        "psr-4": {
            "DoctrineOrmTest\\": "src"
        }
    }
}

bootstrap.php

require_once(__DIR__ . '/vendor/autoload.php');

$config = Doctrine\ORM\ORMSetup::createAnnotationMetadataConfiguration([__DIR__ . '/src'], true);
$connection = Doctrine\DBAL\DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true,], $config);
return new Doctrine\ORM\EntityManager($connection, $config);

cli-config.php

return Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet(require_once(__DIR__ . '/bootstrap.php'));

and test.php

set_error_handler(fn () => die(var_export(func_get_args(), true)));

$em = require_once(__DIR__ . '/bootstrap.php');
(new Doctrine\ORM\Tools\SchemaTool($em))->createSchema([$em->getClassMetadata(DoctrineOrmTest\Base::class)]);
$em->clear();

$em->find(DoctrineOrmTest\Concrete::class, 1);
$em->find(DoctrineOrmTest\Intermediate::class, 1);
$em->find(DoctrineOrmTest\Base::class, 1);

will result in

 $ php test.php 
array (
  0 => 8,
  1 => 'Undefined index: DoctrineOrmTest\\Intermediate',
  2 => 'vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php',
  3 => 157,
  4 => 
  array (
    'values' => 
    array (
      0 => '\'1\'',
    ),
    'discrValues' => 
    array (
      'DoctrineOrmTest\\Concrete' => 1,
    ),
    'subclassName' => 'DoctrineOrmTest\\Intermediate',
  ),

while

$ vendor/bin/doctrine orm:info
 Found 3 mapped entities:

 [OK]   DoctrineOrmTest\Base
 [OK]   DoctrineOrmTest\Concrete
 [OK]   DoctrineOrmTest\Intermediate
Originally created by @ghost on GitHub (Apr 13, 2023). ### BC Break Report The Single Table Inheritance Entity Inheritance is defined in your documentation with > #[DiscriminatorMap] declares the possible values for the discriminator column and maps them to class names in the hierarchy. This discriminator map has to declare all non-abstract entity classes that exist in that particular inheritance hierarchy. That includes the root as well as any intermediate entity classes, given they are not abstract. but will fail on EntityManager::find() with an intermediate abstract entity class: ``` Undefined index: "DoctrineOrmTest\Intermediate" vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php:157 vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php:128 vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:1092 vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:748 vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:768 vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:521 vendor/doctrine/orm/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php:205 ``` as the code iterates over all subclasses (abstract and non-abstract) and expects them in the discriminator map. | Q | A |------------ | ------ | BC Break | yes | Version | 2.14.2 #### Summary Apparently the intermediate abstract class seems to be expected in the discriminator map, even though the documentation states otherwise. Not quite sure if the `ClassMetadataInfo::subClasses` iteration should directly access the discriminator values `discrValues` in `SingleTablePersister::getSelectConditionDiscriminatorValueSQL()` ``` $discrValues = array_flip($this->class->discriminatorMap); foreach ($this->class->subClasses as $subclassName) { $values[] = $this->conn->quote($discrValues[$subclassName]); } ``` as `subClasses` is documented with having also abstract classes ``` * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all * <em>entity</em> subclasses of this class. These may also be abstract classes. ``` where those are not to be expected in the discriminator map. #### Previous behavior It worked :tm: #### Current behavior Access violation on runtime, but not on validation. #### How to reproduce Assume the following example of entities: ```php namespace DoctrineOrmTest; use Doctrine\ORM\Mapping as ORM; /** * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorMap({"1" = "DoctrineOrmTest\Concrete"}) * @ORM\Entity() */ abstract class Base { /** * @ORM\Column * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private int $id; } ``` ```php namespace DoctrineOrmTest; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ abstract class Intermediate extends Base { } ``` ```php namespace DoctrineOrmTest; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Concrete extends Intermediate { } ``` plus some boilerplate: composer.json ```json { "require": { "doctrine/annotations": "^2.0", "doctrine/orm": "^2.14.1", "symfony/cache": "^5.4" }, "autoload": { "psr-4": { "DoctrineOrmTest\\": "src" } } } ``` bootstrap.php ```php require_once(__DIR__ . '/vendor/autoload.php'); $config = Doctrine\ORM\ORMSetup::createAnnotationMetadataConfiguration([__DIR__ . '/src'], true); $connection = Doctrine\DBAL\DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true,], $config); return new Doctrine\ORM\EntityManager($connection, $config); ``` cli-config.php ```php return Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet(require_once(__DIR__ . '/bootstrap.php')); ``` and test.php ```php set_error_handler(fn () => die(var_export(func_get_args(), true))); $em = require_once(__DIR__ . '/bootstrap.php'); (new Doctrine\ORM\Tools\SchemaTool($em))->createSchema([$em->getClassMetadata(DoctrineOrmTest\Base::class)]); $em->clear(); $em->find(DoctrineOrmTest\Concrete::class, 1); $em->find(DoctrineOrmTest\Intermediate::class, 1); $em->find(DoctrineOrmTest\Base::class, 1); ``` will result in ``` $ php test.php array ( 0 => 8, 1 => 'Undefined index: DoctrineOrmTest\\Intermediate', 2 => 'vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/SingleTablePersister.php', 3 => 157, 4 => array ( 'values' => array ( 0 => '\'1\'', ), 'discrValues' => array ( 'DoctrineOrmTest\\Concrete' => 1, ), 'subclassName' => 'DoctrineOrmTest\\Intermediate', ), ``` while ``` $ vendor/bin/doctrine orm:info Found 3 mapped entities: [OK] DoctrineOrmTest\Base [OK] DoctrineOrmTest\Concrete [OK] DoctrineOrmTest\Intermediate ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7133