Skip to content

Commit 7d7dc1b

Browse files
committed
Deprecate undeclared entity inheritance
Inheritance has to be declared as soon as one entity class extends (directly or through middle classes) another one. This is also pointed out in the opening comment for doctrine#8348: > Entities are not allowed to extend from entities without an inheritence mapping relationship (Single Table or Joined Table inheritance). [...] While Doctrine so far allowed these things, they are fragile and will break on certain scenarios. Throwing an exception in case of this misconfiguration is nothing we should do light-heartedly, given that it may surprise users in a bugfix or feature release. So, we should start with a deprecation notice and make this an exception in 3.0. The documentation is updated accordingly at doctrine#10429. Catching missing inheritance declarations early on is important to avoid weird errors further down the road, giving users a clear indication of the root cause. In case you are affected by this, please understand that although things "previously worked" for you, you have been using the ORM outside of what it was designed to do. That may have worked in simple cases, but may also have caused invalid results (wrong or missing data after hydration?) that possibly went unnoticed in subtle cases.
1 parent 7f783b5 commit 7d7dc1b

File tree

8 files changed

+161
-23
lines changed

8 files changed

+161
-23
lines changed

UPGRADE.md

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ now deprecated and will be removed in 3.0.
1212

1313
# Upgrade to 2.14
1414

15+
## Deprecated undeclared entity inheritance
16+
17+
As soon as an entity class inherits from another entity class, inheritance has to
18+
be declared by adding the appropriate configuration for the root entity.
19+
1520
## Deprecated `Doctrine\ORM\Persisters\Exception\UnrecognizedField::byName($field)` method.
1621

1722
Use `Doctrine\ORM\Persisters\Exception\UnrecognizedField::byFullyQualifiedName($className, $field)` instead.

lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php

+12
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
148148
}
149149

150150
if (! $class->isMappedSuperclass) {
151+
if ($rootEntityFound && $class->isInheritanceTypeNone()) {
152+
Deprecation::trigger(
153+
'doctrine/orm',
154+
'https://github.com/doctrine/orm/pull/10431',
155+
"Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared. This is a misconfiguration and will be an error in Doctrine ORM 3.0.",
156+
$class->name,
157+
end($nonSuperclassParents)
158+
);
159+
// enable this in 3.0
160+
// throw MappingException::missingInheritanceTypeDeclaration(end($nonSuperclassParents), $class->name);
161+
}
162+
151163
foreach ($class->embeddedClasses as $property => $embeddableClass) {
152164
if (isset($embeddableClass['inherited'])) {
153165
continue;

lib/Doctrine/ORM/Mapping/MappingException.php

+13
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,19 @@ static function ($a, $b) {
539539
);
540540
}
541541

542+
/**
543+
* @param class-string $rootEntityClass
544+
* @param class-string $childEntityClass
545+
*/
546+
//public static function missingInheritanceTypeDeclaration(string $rootEntityClass, string $childEntityClass): self
547+
//{
548+
// return new self(sprintf(
549+
// "Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.",
550+
// $childEntityClass,
551+
// $rootEntityClass
552+
// ));
553+
//}
554+
542555
/**
543556
* @param string $className
544557
*

tests/Doctrine/Tests/Models/DDC1590/DDC1590Entity.php

-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66

77
use DateTime;
88
use Doctrine\ORM\Mapping\Column;
9-
use Doctrine\ORM\Mapping\Entity;
109
use Doctrine\ORM\Mapping\GeneratedValue;
1110
use Doctrine\ORM\Mapping\Id;
1211
use Doctrine\ORM\Mapping\MappedSuperclass;
1312

1413
/**
15-
* @Entity
1614
* @MappedSuperclass
1715
*/
1816
abstract class DDC1590Entity

tests/Doctrine/Tests/Models/DDC2372/DDC2372User.php

+6
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
namespace Doctrine\Tests\Models\DDC2372;
66

77
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\DiscriminatorColumn;
9+
use Doctrine\ORM\Mapping\DiscriminatorMap;
810
use Doctrine\ORM\Mapping\Entity;
911
use Doctrine\ORM\Mapping\GeneratedValue;
1012
use Doctrine\ORM\Mapping\Id;
13+
use Doctrine\ORM\Mapping\InheritanceType;
1114
use Doctrine\ORM\Mapping\Table;
1215
use Doctrine\Tests\Models\DDC2372\Traits\DDC2372AddressAndAccessors;
1316

1417
/**
1518
* @Entity
1619
* @Table(name="users")
20+
* @InheritanceType("SINGLE_TABLE")
21+
* @DiscriminatorColumn("type")
22+
* @DiscriminatorMap({"user"="DDC2372User", "admin"="DDC2372Admin"})
1723
*/
1824
class DDC2372User
1925
{

tests/Doctrine/Tests/Models/DDC5934/DDC5934BaseContract.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
use Doctrine\Common\Collections\Collection;
99
use Doctrine\ORM\Mapping\ClassMetadata;
1010
use Doctrine\ORM\Mapping\Column;
11-
use Doctrine\ORM\Mapping\Entity;
1211
use Doctrine\ORM\Mapping\GeneratedValue;
1312
use Doctrine\ORM\Mapping\Id;
1413
use Doctrine\ORM\Mapping\ManyToMany;
14+
use Doctrine\ORM\Mapping\MappedSuperclass;
1515

16-
/** @Entity */
17-
#[Entity]
16+
/** @MappedSuperclass() */
17+
#[MappedSuperclass]
1818
class DDC5934BaseContract
1919
{
2020
/**

tests/Doctrine/Tests/Models/Forum/ForumAdministrator.php

-18
This file was deleted.

tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php

+122
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\Tests\ORM\Mapping;
66

7+
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
78
use Doctrine\ORM\EntityRepository;
89
use Doctrine\ORM\Id\SequenceGenerator as IdSequenceGenerator;
910
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -29,13 +30,16 @@
2930
use Doctrine\Tests\Models\DDC869\DDC869Payment;
3031
use Doctrine\Tests\Models\DDC869\DDC869PaymentRepository;
3132
use Doctrine\Tests\OrmTestCase;
33+
use Generator;
3234

3335
use function assert;
3436
use function serialize;
3537
use function unserialize;
3638

3739
class BasicInheritanceMappingTest extends OrmTestCase
3840
{
41+
use VerifyDeprecations;
42+
3943
/** @var ClassMetadataFactory */
4044
private $cmf;
4145

@@ -218,6 +222,42 @@ public function testMappedSuperclassIndex(): void
218222
self::assertArrayHasKey('IDX_MAPPED1_INDEX', $class->table['uniqueConstraints']);
219223
self::assertArrayHasKey('IDX_MAPPED2_INDEX', $class->table['indexes']);
220224
}
225+
226+
/**
227+
* @dataProvider invalidHierarchyDeclarationClasses
228+
*/
229+
public function testUndeclaredHierarchyRejection(string $rootEntity, string $childClass): void
230+
{
231+
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10431');
232+
233+
/* on 3.0, use this instead: */
234+
// self::expectException(MappingException::class);
235+
// self::expectExceptionMessage(\sprintf(
236+
// "Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.",
237+
// $childClass,
238+
// $rootEntity
239+
// ));
240+
241+
$this->cmf->getMetadataFor($childClass);
242+
}
243+
244+
public function invalidHierarchyDeclarationClasses(): Generator
245+
{
246+
yield 'concrete Entity root and child class, direct inheritance'
247+
=> [InvalidEntityRoot::class, InvalidEntityRootChild::class];
248+
249+
yield 'concrete Entity root and abstract child class, direct inheritance'
250+
=> [InvalidEntityRoot::class, InvalidEntityRootAbstractChild::class];
251+
252+
yield 'abstract Entity root and concrete child class, direct inheritance'
253+
=> [InvalidAbstractEntityRoot::class, InvalidAbstractEntityRootChild::class];
254+
255+
yield 'abstract Entity root and abstract child class, direct inheritance'
256+
=> [InvalidAbstractEntityRoot::class, InvalidAbstractEntityRootAbstractChild::class];
257+
258+
yield 'complex example (Entity Root -> Mapped Superclass -> transient class -> Entity)'
259+
=> [InvalidComplexRoot::class, InvalidComplexEntity::class];
260+
}
221261
}
222262

223263
class TransientBaseClass
@@ -438,3 +478,85 @@ class MediumSuperclassEntity extends MediumSuperclassBase
438478
class SubclassWithRepository extends DDC869Payment
439479
{
440480
}
481+
482+
/**
483+
* @Entity
484+
*
485+
* This class misses the DiscriminatorMap declaration
486+
*/
487+
class InvalidEntityRoot
488+
{
489+
/**
490+
* @Column(type="integer")
491+
* @Id
492+
* @GeneratedValue(strategy="AUTO")
493+
* @var int
494+
*/
495+
public $id;
496+
}
497+
498+
/** @Entity */
499+
class InvalidEntityRootChild extends InvalidEntityRoot
500+
{
501+
}
502+
503+
/** @Entity */
504+
abstract class InvalidEntityRootAbstractChild extends InvalidEntityRoot
505+
{
506+
}
507+
508+
/**
509+
* @Entity
510+
*
511+
* This class misses the DiscriminatorMap declaration
512+
*/
513+
class InvalidAbstractEntityRoot
514+
{
515+
/**
516+
* @Column(type="integer")
517+
* @Id
518+
* @GeneratedValue(strategy="AUTO")
519+
* @var int
520+
*/
521+
public $id;
522+
}
523+
524+
/** @Entity */
525+
class InvalidAbstractEntityRootChild extends InvalidAbstractEntityRoot
526+
{
527+
}
528+
529+
/** @Entity */
530+
abstract class InvalidAbstractEntityRootAbstractChild extends InvalidAbstractEntityRoot
531+
{
532+
}
533+
534+
/**
535+
* @Entity
536+
*
537+
* This class misses the DiscriminatorMap declaration
538+
*/
539+
class InvalidComplexRoot
540+
{
541+
/**
542+
* @Column(type="integer")
543+
* @Id
544+
* @GeneratedValue(strategy="AUTO")
545+
* @var int
546+
*/
547+
public $id;
548+
}
549+
550+
/** @MappedSuperclass */
551+
class InvalidComplexMappedSuperclass extends InvalidComplexRoot
552+
{
553+
}
554+
555+
class InvalidComplexTransientClass extends InvalidComplexMappedSuperclass
556+
{
557+
}
558+
559+
/** @Entity */
560+
class InvalidComplexEntity extends InvalidComplexTransientClass
561+
{
562+
}

0 commit comments

Comments
 (0)