Skip to content

Commit bed3b44

Browse files
codedmonkeyweaverryan
authored andcommitted
Fix self-referencing entity associations
1 parent 60b0e27 commit bed3b44

File tree

7 files changed

+167
-3
lines changed

7 files changed

+167
-3
lines changed

src/Doctrine/BaseRelation.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ abstract class BaseRelation
1919
private $propertyName;
2020
private $targetClassName;
2121
private $targetPropertyName;
22+
private $isSelfReferencing = false;
2223
private $mapInverseRelation = true;
2324

2425
abstract public function isOwning(): bool;
@@ -59,6 +60,18 @@ public function setTargetPropertyName($targetPropertyName)
5960
return $this;
6061
}
6162

63+
public function isSelfReferencing(): bool
64+
{
65+
return $this->isSelfReferencing;
66+
}
67+
68+
public function setIsSelfReferencing(bool $isSelfReferencing)
69+
{
70+
$this->isSelfReferencing = $isSelfReferencing;
71+
72+
return $this;
73+
}
74+
6275
public function getMapInverseRelation(): bool
6376
{
6477
return $this->mapInverseRelation;

src/Doctrine/EntityRelation.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ final class EntityRelation
3333

3434
private $isNullable = false;
3535

36+
private $isSelfReferencing = false;
37+
3638
private $orphanRemoval = false;
3739

3840
private $mapInverseRelation = true;
@@ -50,6 +52,7 @@ public function __construct(string $type, string $owningClass, string $inverseCl
5052
$this->type = $type;
5153
$this->owningClass = $owningClass;
5254
$this->inverseClass = $inverseClass;
55+
$this->isSelfReferencing = $owningClass === $inverseClass;
5356
}
5457

5558
public function setOwningProperty(string $owningProperty)
@@ -95,6 +98,7 @@ public function getOwningRelation()
9598
->setTargetClassName($this->inverseClass)
9699
->setTargetPropertyName($this->inverseProperty)
97100
->setIsNullable($this->isNullable)
101+
->setIsSelfReferencing($this->isSelfReferencing)
98102
->setMapInverseRelation($this->mapInverseRelation)
99103
;
100104
break;
@@ -104,6 +108,7 @@ public function getOwningRelation()
104108
->setTargetClassName($this->inverseClass)
105109
->setTargetPropertyName($this->inverseProperty)
106110
->setIsOwning(true)->setMapInverseRelation($this->mapInverseRelation)
111+
->setIsSelfReferencing($this->isSelfReferencing)
107112
;
108113
break;
109114
case self::ONE_TO_ONE:
@@ -113,6 +118,7 @@ public function getOwningRelation()
113118
->setTargetPropertyName($this->inverseProperty)
114119
->setIsNullable($this->isNullable)
115120
->setIsOwning(true)
121+
->setIsSelfReferencing($this->isSelfReferencing)
116122
->setMapInverseRelation($this->mapInverseRelation)
117123
;
118124
break;
@@ -130,6 +136,7 @@ public function getInverseRelation()
130136
->setTargetClassName($this->owningClass)
131137
->setTargetPropertyName($this->owningProperty)
132138
->setOrphanRemoval($this->orphanRemoval)
139+
->setIsSelfReferencing($this->isSelfReferencing)
133140
;
134141
break;
135142
case self::MANY_TO_MANY:
@@ -138,6 +145,7 @@ public function getInverseRelation()
138145
->setTargetClassName($this->owningClass)
139146
->setTargetPropertyName($this->owningProperty)
140147
->setIsOwning(false)
148+
->setIsSelfReferencing($this->isSelfReferencing)
141149
;
142150
break;
143151
case self::ONE_TO_ONE:
@@ -147,6 +155,7 @@ public function getInverseRelation()
147155
->setTargetPropertyName($this->owningProperty)
148156
->setIsNullable($this->isNullable)
149157
->setIsOwning(false)
158+
->setIsSelfReferencing($this->isSelfReferencing)
150159
;
151160
break;
152161
default:
@@ -184,6 +193,11 @@ public function isNullable(): bool
184193
return $this->isNullable;
185194
}
186195

196+
public function isSelfReferencing(): bool
197+
{
198+
return $this->isSelfReferencing;
199+
}
200+
187201
public function getMapInverseRelation(): bool
188202
{
189203
return $this->mapInverseRelation;

src/Maker/MakeEntity.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,13 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
201201
} elseif ($newField instanceof EntityRelation) {
202202
// both overridden below for OneToMany
203203
$newFieldName = $newField->getOwningProperty();
204-
$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
205-
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
204+
if ($newField->isSelfReferencing()) {
205+
$otherManipulatorFilename = $entityPath;
206+
$otherManipulator = $manipulator;
207+
} else {
208+
$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
209+
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
210+
}
206211
switch ($newField->getType()) {
207212
case EntityRelation::MANY_TO_ONE:
208213
if ($newField->getOwningClass() === $entityClassDetails->getFullName()) {

src/Util/ClassSourceManipulator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ private function addSingularRelation(BaseRelation $relation)
447447

448448
private function addCollectionRelation(BaseCollectionRelation $relation)
449449
{
450-
$typeHint = $this->addUseStatementIfNecessary($relation->getTargetClassName());
450+
$typeHint = $relation->isSelfReferencing() ? 'self' : $this->addUseStatementIfNecessary($relation->getTargetClassName());
451451

452452
$arrayCollectionTypeHint = $this->addUseStatementIfNecessary(ArrayCollection::class);
453453
$collectionTypeHint = $this->addUseStatementIfNecessary(Collection::class);

tests/Maker/FunctionalTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,36 @@ public function getCommandEntityTests()
805805
->setRequiredPhpVersion(70100)
806806
];
807807

808+
yield 'entity_many_to_one_self_referencing' => [MakerTestDetails::createTest(
809+
$this->getMakerInstance(MakeEntity::class),
810+
[
811+
// entity class name
812+
'User',
813+
// field name
814+
'guardian',
815+
// add a relationship field
816+
'relation',
817+
// the target entity
818+
'User',
819+
// relation type
820+
'ManyToOne',
821+
// nullable
822+
'y',
823+
// do you want to generate an inverse relation? (default to yes)
824+
'',
825+
// field name on opposite side
826+
'dependants',
827+
// orphanRemoval (default to no)
828+
'',
829+
// finish adding fields
830+
'',
831+
])
832+
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeEntitySelfReferencing')
833+
->configureDatabase()
834+
->updateSchemaAfterCommand()
835+
->setRequiredPhpVersion(70100)
836+
];
837+
808838
yield 'entity_one_to_many_simple' => [MakerTestDetails::createTest(
809839
$this->getMakerInstance(MakeEntity::class),
810840
[
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Entity;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
/**
8+
* @ORM\Entity()
9+
*/
10+
class User
11+
{
12+
/**
13+
* @ORM\Id
14+
* @ORM\GeneratedValue
15+
* @ORM\Column(type="integer")
16+
*/
17+
private $id;
18+
19+
/**
20+
* @ORM\Column(type="string", length=255, nullable=true)
21+
*/
22+
private $firstName;
23+
24+
/**
25+
* @ORM\Column(type="datetime", nullable=true)
26+
*/
27+
private $createdAt;
28+
29+
public function getId()
30+
{
31+
return $this->id;
32+
}
33+
34+
public function getFirstName()
35+
{
36+
return $this->firstName;
37+
}
38+
39+
public function setFirstName(?string $firstName)
40+
{
41+
$this->firstName = $firstName;
42+
}
43+
44+
public function getCreatedAt(): ?\DateTimeInterface
45+
{
46+
return $this->createdAt;
47+
}
48+
49+
public function setCreatedAt(?\DateTimeInterface $createdAt)
50+
{
51+
$this->createdAt = $createdAt;
52+
}
53+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Tests;
4+
5+
use Doctrine\Common\Collections\ArrayCollection;
6+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
7+
use Doctrine\ORM\EntityManager;
8+
use App\Entity\User;
9+
10+
class GeneratedEntityTest extends KernelTestCase
11+
{
12+
public function testGeneratedEntity()
13+
{
14+
self::bootKernel();
15+
/** @var EntityManager $em */
16+
$em = self::$kernel->getContainer()
17+
->get('doctrine')
18+
->getManager();
19+
20+
$em->createQuery('DELETE FROM App\\Entity\\User u')->execute();
21+
22+
$user = new User();
23+
// check that the constructor was instantiated properly
24+
$this->assertInstanceOf(ArrayCollection::class, $user->getDependants());
25+
// set existing field
26+
$user->setFirstName('Ryan');
27+
$em->persist($user);
28+
29+
$ward = new User();
30+
$ward->setFirstName('Tim');
31+
$ward->setGuardian($user);
32+
$em->persist($ward);
33+
34+
// set via the inverse side
35+
$ward2 = new User();
36+
$ward2->setFirstName('Fabien');
37+
$user->addDependant($ward2);
38+
$em->persist($ward2);
39+
40+
$em->flush();
41+
$em->refresh($user);
42+
43+
$actualUser = $em->getRepository(User::class)
44+
->findAll();
45+
46+
$this->assertCount(3, $actualUser);
47+
$this->assertCount(2, $actualUser[0]->getDependants());
48+
}
49+
}

0 commit comments

Comments
 (0)