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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEn 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 | |
| (['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.
8 Comments
Under Setting up the Fixtures the sentence
should be:
Hey Alex,
You're right, thanks! Well, we still say it like
User::factory()... but that's a misspell and we clearly useUserFactory::random()on the video, so I fixed it https://github.com/SymfonyCasts/api-platform3/commit/71f42603109698c1ebad04bce8fe08636013e780Cheers!
Hi mate, I have really weird behavior when I tested the relationship with ApiTestCase. From the swagger I'm able to debug my processor state, but when I run the test, I have the following exceptio: ""Failed to denormalize attribute "protocol" value for class "App\Entity\Tramit": Expected argument of type "?App\Entity\Protocol", "Symfony\Component\HttpFoundation\Response" given at property path "protocol"."
The swagger POST request which is running fine
https://ibb.co/WvksJFk
The test which is getting error
https://ibb.co/TrcVVmv
I noticed that when I execute the request from the swagger, the content looks like this:
https://ibb.co/sPSqdtm
But when I executed the test, the same content looks a little different:
https://ibb.co/VB107mm
Is a little weird because I have another tests running well, the only different thing is that on this entities (Tramit and Protocol) I have inverseBy field in Protocol:
https://ibb.co/ZxtXk8w
What I'm trying to achieve is the capability to run a processor in the POST action, this capability is working fine from the swagger but no in the unit test, any help will be ver helpful.
Thanks in advance
Hey @Zahit,
First, have you tried to debug the
$irivariable? does it look like expected?My second thought is about the test data, it maybe not the same as your local copy, I mean probably some relations are not set, that's why it's failing to denormalize objects
Cheers!
Hi!
Something really strange happened to me.
The migration was failing, so I looked at the error and went to the migration files to find the statements causing them.
To my surprise, in the first document, there were references to a table from a previous tutorial interspersed with these.
Any idea how this could have happened?
Maybe something in the Symfony binary cache?
Thanks!
`
`
Yo @Oleguer-C!
Hmm. I don't see those extra
vinyl_mixlines in the migration files we have in the download code for the project, but I CAN think of a general workflow that could add these... it just depends on what actually happened :). A flow might be:A) You previously work on the Doctrine tutorial (with
vinyl_mix)B) You boot up this project, but point it to the same database.
C) Run
make:migrationDoctrine will see the
vinyl_mixstuff and think "that shouldn't be there" and try to drop it. Does this sound feasible? If I'm off the mark, it's still probably some silly detail - unless we have something wrong in our code and I'm not seeing it (definitely possible).Cheers!
Thanks for the reply.
That's what i thought, but in principle shouldn't the dbs of each project be isolated?
Maybe i hadn't downloaded the last container and since the port was exposed i connected to the old container the first time. Who knows.
Thanks
If you're using the Docker integration, then yes, the dbs should, in theory, be in their own container. However, the names of the containers, by default, come from your directory name (not the full directory name - just the last, top-level directory name). So if you put both projects into a directory called
training, for example, then they would share container names and thus share containers :). I have a lot of projects locally, so I hit this sometimes!"Houston: no signs of life"
Start the conversation!