diff --git a/src/Doctrine/BaseRelation.php b/src/Doctrine/BaseRelation.php index 5ab9a513f..702aa1e19 100644 --- a/src/Doctrine/BaseRelation.php +++ b/src/Doctrine/BaseRelation.php @@ -28,6 +28,7 @@ public function __construct( private bool $isOwning = false, private bool $orphanRemoval = false, private bool $isNullable = false, + private ?string $joinTableName = null, ) { } @@ -85,4 +86,9 @@ public function isNullable(): bool { return $this->isNullable; } + + public function getJoinTableName(): ?string + { + return $this->joinTableName; + } } diff --git a/src/Doctrine/EntityRelation.php b/src/Doctrine/EntityRelation.php index db41f8052..91db3effc 100644 --- a/src/Doctrine/EntityRelation.php +++ b/src/Doctrine/EntityRelation.php @@ -27,6 +27,7 @@ final class EntityRelation private bool $isSelfReferencing = false; private bool $orphanRemoval = false; private bool $mapInverseRelation = true; + private ?string $joinTableName = null; public function __construct( private string $type, @@ -97,6 +98,7 @@ public function getOwningRelation(): RelationManyToMany|RelationOneToOne|Relatio isSelfReferencing: $this->isSelfReferencing, mapInverseRelation: $this->mapInverseRelation, isOwning: true, + joinTableName: $this->joinTableName, )), self::ONE_TO_ONE => (new RelationOneToOne( propertyName: $this->owningProperty, @@ -186,4 +188,14 @@ public function setMapInverseRelation(bool $mapInverseRelation): void $this->mapInverseRelation = $mapInverseRelation; } + + public function getJoinTableName(): ?string + { + return $this->joinTableName; + } + + public function setJoinTableName(?string $joinTableName) + { + $this->joinTableName = $joinTableName; + } } diff --git a/src/Maker/MakeEntity.php b/src/Maker/MakeEntity.php index 07896c675..b67a868ba 100644 --- a/src/Maker/MakeEntity.php +++ b/src/Maker/MakeEntity.php @@ -619,6 +619,20 @@ function ($name) use ($targetClass) { $relation->setMapInverseRelation($mapInverse); }; + $askJoinTableName = function (EntityRelation $relation) use ($io) { + $joinTableDecision = $io->confirm( + 'Do you want to specify a join table? You may want to do this if you plan on having multiple many-to-many relations to the same entity.', + false + ); + + if ($joinTableDecision) { + $relation->setJoinTableName($io->ask( + 'What should the join table be named?', + lcfirst(Str::getShortClassName($relation->getOwningClass())).ucfirst($relation->getOwningProperty()) + )); + } + }; + switch ($type) { case EntityRelation::MANY_TO_ONE: $relation = new EntityRelation( @@ -708,6 +722,8 @@ function ($name) use ($targetClass) { )); } + $askJoinTableName($relation); + break; case EntityRelation::ONE_TO_ONE: $relation = new EntityRelation( diff --git a/src/Util/ClassSourceManipulator.php b/src/Util/ClassSourceManipulator.php index 6a9d86d93..6d95cdeed 100644 --- a/src/Util/ClassSourceManipulator.php +++ b/src/Util/ClassSourceManipulator.php @@ -17,6 +17,7 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Embedded; use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\JoinTable; use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; @@ -558,6 +559,11 @@ private function addCollectionRelation(BaseCollectionRelation $relation): void ), ]; + // Conditionally add joinTable attribute + if ($relation->getJoinTableName()) { + $attributes[] = $this->buildAttributeNode(JoinTable::class, ['name' => $relation->getJoinTableName()], 'ORM'); + } + $this->addProperty( name: $relation->getPropertyName(), attributes: $attributes, diff --git a/tests/Maker/MakeEntityTest.php b/tests/Maker/MakeEntityTest.php index 6e8d36f82..b1392a90c 100644 --- a/tests/Maker/MakeEntityTest.php +++ b/tests/Maker/MakeEntityTest.php @@ -239,6 +239,8 @@ public function getTestDetails(): \Generator 'y', // field name on opposite side - use default 'courses' '', + // join table? - use default 'n' + '', // finish adding fields '', ]); @@ -272,6 +274,8 @@ public function getTestDetails(): \Generator 'y', // field name on opposite side - use default 'courses' '', + // join table? - use default 'n' + '', // finish adding fields '', ]); @@ -280,6 +284,37 @@ public function getTestDetails(): \Generator }), ]; + yield 'it_adds_many_to_many_with_custom_join_table' => [$this->createMakeEntityTest() + ->run(function (MakerTestRunner $runner) { + $this->copyEntity($runner, 'User-basic.php'); + + $runner->runMaker([ + // entity class name + 'Course', + // field name + 'students', + // add a relationship field + 'relation', + // the target entity + 'User', + // relation type + 'ManyToMany', + // inverse side? + 'y', + // field name on opposite side - use default 'courses' + '', + // join table? + 'y', + // join table name - use default 'courseStudents' + '', + // finish adding fields + '', + ]); + + $this->runCustomTest($runner, 'it_adds_many_to_many_with_custom_join_table.php'); + }), + ]; + yield 'it_adds_one_to_one_simple' => [$this->createMakeEntityTest() ->run(function (MakerTestRunner $runner) { $this->copyEntity($runner, 'User-basic.php'); @@ -363,6 +398,8 @@ public function getTestDetails(): \Generator * normally, we ask for the field on the *other* side, but we * do not here, since the other side won't be mapped. */ + // join table? - use default 'n' + '', // finish adding fields '', ]);