Login to bookmark this video
Buy Access to Course
20.

Muchos-a-Muchos pero con Datos Extra

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Las relaciones ManyToMany son el único lugar de Doctrine donde tenemos una tabla en nuestra base de datos - starship_droid - pero ninguna entidad correspondiente en nuestra aplicación.

Pero hay un problema: no podemos añadir columnas adicionales a esa tabla de unión. Por ejemplo, ¿qué pasaría si quisiéramos saber cuándo se asigna un droide a una nave estelar? Para ello, nuestra tabla de unión necesitaría una columna assignedAt. Pero no podemos añadirla

Cuando muchos a muchos no es suficiente

La solución es arremangarnos y empezar a manejar las cosas de forma más manual.

Dejaremos de utilizar la relación muchos-a-muchos por completo. En su lugar, vamos a generar una nueva entidad que represente la tabla de unión. Primero, deshaz la relación muchos-a-muchos (pero preocúpate sólo de las propiedades, no de los métodos). En Starship, despídete de la propiedaddroids:

229 lines | src/Entity/Starship.php
// ... lines 1 - 15
class Starship
{
// ... lines 18 - 50
/**
* @var Collection<int, Droid>
*/
#[ORM\ManyToMany(targetEntity: Droid::class, inversedBy: 'starships')]
private Collection $droids;
// ... lines 56 - 227
}

Y en Droid, haz lo mismo con la propiedad muchos-a-muchos starships:

91 lines | src/Entity/Droid.php
// ... lines 1 - 10
class Droid
{
// ... lines 13 - 23
/**
* @var Collection<int, Starship>
*/
#[ORM\ManyToMany(targetEntity: Starship::class, mappedBy: 'droids')]
private Collection $starships;
// ... lines 29 - 89
}

Borra el constructor en ambos.

Busca tu terminal y ejecuta:

symfony console doctrine:schema:update --dump-sql

Esto te muestra el aspecto que tendría tu migración si la generaras ahora mismo. Es lo que esperamos: no más tabla starship_droid.

Crear una nueva entidad de unión

Pero ¡no generes todavía esa migración! Queremos la tabla de unión, pero ahora necesitamos crear una entidad que la represente. Ejecuta::

symfony console make:entity StarshipDroid

DroidAssignment podría ser un nombre más adecuado, pero StarshipDroidnos ayuda a visualizar lo que estamos haciendo: recrear exactamente la misma relación de base de datos mediante dos ManyToOnes

Añade assignedAt junto con dos propiedades más para crear relaciones desde esta tabla de unión a Starship y Droid.

Éstas serán relaciones ManyToOne, y conectaránStarshipDroid con Starship y Droid.

La migración que no hace nada

Ahora, genera esa migración:

symfony console make:migration

Y compruébalo. Puede parecer que hay muchos cambios, pero fíjate bien: sólo se eliminan las restricciones de clave foránea, se añade una clave primaria y se vuelve a crear la clave foránea:

46 lines | migrations/Version20250315183623.php
// ... lines 1 - 12
final class Version20250315183623 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT FK_1C7FBE889B24DF5');
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT FK_1C7FBE88AB064EF');
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT starship_droid_pkey');
$this->addSql('ALTER TABLE starship_droid ADD id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL');
$this->addSql('ALTER TABLE starship_droid ADD assigned_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL');
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE889B24DF5 FOREIGN KEY (starship_id) REFERENCES starship (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE88AB064EF FOREIGN KEY (droid_id) REFERENCES droid (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE starship_droid ADD PRIMARY KEY (id)');
}
// ... lines 32 - 44
}

Así que, al final, esta migración no cambia nada real en la base de datos.

Ejecútala con:

symfony console doctrine:migrations:migrate

Y ¡boom!

La columna assignedAt no puede contener valores null.

Doctrine está haciendo un berrinche debido a las filas existentes en la tablastarship_droid. Podemos apaciguarlo con un valor por defecto. Actualiza manualmente la migración a, por ejemplo, DEFAULT NOW() NOT NULL:

46 lines | migrations/Version20250315183623.php
// ... lines 1 - 12
final class Version20250315183623 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema): void
{
// ... lines 22 - 26
$this->addSql('ALTER TABLE starship_droid ADD assigned_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NOW() NOT NULL');
// ... lines 28 - 30
}
// ... lines 32 - 44
}

Los toques finales

Vamos a añadir un toque final a StarshipDroid:

68 lines | src/Entity/StarshipDroid.php
// ... lines 1 - 7
#[ORM\Entity(repositoryClass: StarshipDroidRepository::class)]
class StarshipDroid
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?\DateTimeImmutable $assignedAt = null;
#[ORM\ManyToOne(inversedBy: 'starshipDroids')]
#[ORM\JoinColumn(nullable: false)]
private ?Droid $droid = null;
#[ORM\ManyToOne(inversedBy: 'starshipDroids')]
#[ORM\JoinColumn(nullable: false)]
private ?Starship $starship = null;
// ... lines 26 - 66
}

Este assignedAt no es realmente algo de lo que debamos preocuparnos. Crea un constructor y configúralo automáticamente: $this->assignedAt = new \DateTimeImmutable();:

73 lines | src/Entity/StarshipDroid.php
// ... lines 1 - 8
class StarshipDroid
{
// ... lines 11 - 26
public function __construct()
{
$this->assignedAt = new \DateTimeImmutable();
}
// ... lines 31 - 71
}

Espera, ¡porque esto es enorme! Ahora tenemos exactamente la misma relación en la base de datos que antes. Pero como hemos tomado el control de la entidad join, podemos añadirle nuevos campos. A continuación, veremos cómo asignar droides a naves estelares con esta nueva configuración de entidades. Y, finalmente, ¡nos pondremos elegantes y ocultaremos por completo este detalle de la implementación!