Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Relacionar recursos

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

En nuestra aplicación, cada DragonTreasure debe pertenecer a un único dragón... o Useren nuestro sistema. Para configurar esto, olvídate por un momento de la API y modelémoslo en la base de datos.

Añadir la relación ManyToOne

Dirígete a tu terminal y ejecuta:

php bin/console make:entity

Modifiquemos la entidad DragonTreasure para añadir una propiedad owner... y entonces ésta será una relación ManyToOne. Si no estás seguro de qué relación necesitas, siempre puedes escribir relation y obtendrás un pequeño asistente.

Será una relación con User... y te preguntará si la nueva propiedad owner puede ser nula en la base de datos. Cada DragonTreasure debe tener un propietario... así que di "no". A continuación: ¿queremos mapear el otro lado de la relación? Básicamente, ¿queremos tener la posibilidad de decir $user->getDragonTreasures() en nuestro código? Voy a decir "sí" a esto. Y puede que respondas "sí" por dos razones. O bien porque poder decir $user->getDragonTreasures() sería útil en tu código o, como veremos un poco más adelante, porque quieres poder obtener unUser en tu API y ver al instante qué tesoros tiene.

De todos modos, la propiedad - dragonTreasures dentro de User es fine.... y, por último, para orphanRemoval, di que no. También hablaremos de eso más adelante.

Y... ¡listo! Pulsa intro para salir.

Así que esto no tiene nada que ver con la API Platform. Nuestra entidad DragonTreasure tiene ahora una nueva propiedad owner con los métodos getOwner() y setOwner().

... lines 1 - 51
class DragonTreasure
{
... lines 54 - 93
#[ORM\ManyToOne(inversedBy: 'dragonTreasures')]
#[ORM\JoinColumn(nullable: false)]
private ?User $owner = null;
... lines 97 - 197
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
}

Y en User tenemos una nueva propiedad dragonTreasures, que es un OneToMany de vuelta aDragonTreasure. En la parte inferior, se ha generado getDragonTreasures(),addDragonTreasure(), y removeDragonTreasure(). Cosas muy estándar.

... lines 1 - 6
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
... lines 9 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 25 - 50
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: DragonTreasure::class)]
private Collection $dragonTreasures;
public function __construct()
{
$this->dragonTreasures = new ArrayCollection();
}
... lines 58 - 140
/**
* @return Collection<int, DragonTreasure>
*/
public function getDragonTreasures(): Collection
{
return $this->dragonTreasures;
}
public function addDragonTreasure(DragonTreasure $treasure): self
{
if (!$this->dragonTreasures->contains($treasure)) {
$this->dragonTreasures->add($treasure);
$treasure->setOwner($this);
}
return $this;
}
public function removeDragonTreasure(DragonTreasure $treasure): self
{
if ($this->dragonTreasures->removeElement($treasure)) {
// set the owning side to null (unless already changed)
if ($treasure->getOwner() === $this) {
$treasure->setOwner(null);
}
}
return $this;
}
}

Vamos a crear una migración para esto:

symfony console make:migration

Haremos nuestra doble comprobación estándar para asegurarnos de que la migración no está intentando minar bitcoin. Sí, aquí todo son aburridas consultas SQL.

... lines 1 - 12
final class Version20230104200643 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 dragon_treasure ADD owner_id INT NOT NULL');
$this->addSql('ALTER TABLE dragon_treasure ADD CONSTRAINT FK_9E31BF5F7E3C61F9 FOREIGN KEY (owner_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_9E31BF5F7E3C61F9 ON dragon_treasure (owner_id)');
}
... lines 27 - 35
}

Ejecútala con:

symfony console doctrine:migrations:migrate

Reiniciar la base de datos

Y nos explota en la cara. ¡Grosero! Pero... no debería sorprenderte demasiado. Ya tenemos unos 40 registros DragonTreasure en nuestra base de datos. Así que cuando la migración intenta añadir la columna owner_id a la tabla -que no permite nulos-, nuestra base de datos se queda perpleja: no tiene ni idea de qué valor poner para esos tesoros existentes.

Si nuestra aplicación ya estuviera en producción, tendríamos que trabajar un poco más para solucionar esto, de lo que hablamos en nuestro tutorial de Doctrine. Pero como esto no está en producción, podemos hacer trampas y simplemente apagar y volver a encender la base de datos. Para ello ejecuta:

symfony console doctrine:database:drop --force

Luego:

symfony console doctrine:database:create

Y la migración, que debería funcionar ahora que nuestra base de datos está vacía.

symfony console doctrine:migrations:migrate

Configurar las Fijaciones

Por último, vuelve a añadir algunos datos con:

symfony console doctrine:fixtures:load

Y oh, ¡esto falla por la misma razón! Está intentando crear Tesoros Dragón sin propietario. Para solucionarlo, hay dos opciones. En DragonTreasureFactory, añade un nuevo campo owner a getDefaults() configurado como UserFactory::new().

... lines 1 - 29
final class DragonTreasureFactory extends ModelFactory
{
... lines 32 - 46
protected function getDefaults(): array
{
return [
... lines 50 - 55
'owner' => UserFactory::new(),
];
}
... lines 59 - 73
}

No voy a entrar en los detalles de Foundry -y Foundry tiene una documentación estupenda sobre cómo trabajar con relaciones-, pero esto creará un nuevo User cada vez que cree un nuevo DragonTreasure... y luego los relacionará. Así que está bien tenerlo por defecto.

Pero en AppFixtures, anulemos eso para hacer algo más guay. Desplaza la llamada aDragonTreasureFactory después de UserFactory... y pasa un segundo argumento, que es una forma de anular los valores por defecto. Pasando una llamada de retorno, cada vez que se cree unDragonTreasure -es decir, 40 veces- se llamará a este método y podremos devolver datos únicos que utilizaremos para anular los valores por defecto de ese tesoro. Devuelveowner ajustado a User::factory()->random().

... lines 1 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
UserFactory::createMany(10);
DragonTreasureFactory::createMany(40, function () {
return [
'owner' => UserFactory::random(),
];
});
}
}

Eso encontrará un objeto User aleatorio y lo establecerá como owner. Así tendremos 40DragonTreasures cada uno acaparado aleatoriamente por uno de estos 10 Users.

¡Vamos a probarlo! Ejecuta:

symfony console doctrine:fixtures:load

Esta vez... ¡éxito!

Exponer el "propietario" en la API

Vale, ahora DragonTreasure tiene una nueva propiedad de relación owner... y Usertiene una nueva propiedad de relación dragonTreasures.

¿Aparecerá... esa nueva propiedad owner en la API? Prueba con la ruta GET del tesoro. Y... ¡el nuevo campo no aparece! Eso tiene sentido! La propiedad owner no está dentro del grupo de normalización.

Así que si queremos exponer la propiedad owner en la API, como cualquier otro campo, tenemos que añadirle grupos. Copia los grupos de coolFactor... y pégalos aquí.

... lines 1 - 51
class DragonTreasure
{
... lines 54 - 95
#[Groups(['treasure:read', 'treasure:write'])]
private ?User $owner = null;
... lines 98 - 209
}

Esto hace que la propiedad sea legible y escribible. Y sí, más adelante aprenderemos a establecer la propiedad owner automáticamente para que el usuario de la API no tenga que enviarlo manualmente. Pero por ahora, hacer que el cliente de la API envíe el campo owner funcionará de maravilla.

En cualquier caso, ¿qué aspecto tiene esta nueva propiedad owner? Pulsa "Ejecutar" y... ¡guau! ¡La propiedad owner se establece en una URL! Bueno, en realidad, el IRI de User.

Esto me encanta. Cuando empecé a trabajar con la API Platform, pensaba que las propiedades de relación utilizarían simplemente el id del objeto. Como owner: 1. Pero esto es mucho más útil... porque le dice a nuestro cliente API exactamente cómo puede obtener más información sobre este usuario: ¡sólo tiene que seguir la URL!

Escribir una propiedad de relación

Así que, por defecto, una relación se devuelve como una URL. Pero, ¿qué aspecto tiene establecer un campo de relación? Actualiza la página, abre la ruta POST, inténtalo, y pegaré todos los campos excepto owner. ¿Qué utilizamos para owner? ¡No lo sé! Probemos a ponerle un id, como 1.

Momento de la verdad. Pulsa ejecutar. Veamos... ¡un código de estado 400! Y comprueba el error:

IRI esperado o documento anidado para el atributo owner, entero dado.

Así que pasé el ID del propietario y... eso no le gusta. ¿Qué debemos poner aquí? Pues el IRI, ¡por supuesto! Averigüemos más sobre eso a continuación.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice