Buy Access to Course
19.

Recursos relacionados

|

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

Tenemos un recurso queso y un recurso usuario. ¡Vamos a relacionarlos! Bien, el verdadero problema que tenemos que resolver es el siguiente: cada CheeseListing será "propiedad" de un único usuario, lo cual es algo que tenemos que configurar en la base de datos, pero también algo que tenemos que exponer en nuestra API: cuando miro un recurso CheeseListing, ¡necesito saber qué usuario lo ha publicado!

Crear la relación de la base de datos

Primero vamos a configurar la base de datos. Busca tu terminal y ejecuta:

php bin/console make:entity

Actualicemos la entidad CheeseListing y añadamos una nueva propiedad owner. Ésta será una relation a la entidad User... que será una relación ManyToOne: cada CheeseListing tiene una User. ¿Esta nueva propiedad debe ser anulable en la base de datos? Di que no: cada CheeseListing debe tener un owneren nuestro sistema.

A continuación, formula una pregunta superimportante: ¿queremos añadir una nueva propiedad a User para poder acceder y actualizar los listados de quesos en ella, como$user->getCheeseListings(). Hacer esto es opcional, y hay dos razones por las que podrías quererlo. En primer lugar, si crees que escribir $user->getCheeseListings()en tu código puede ser conveniente, ¡lo querrás! En segundo lugar, cuando obtengas unUser en nuestra API, si quieres ser capaz de ver qué listados de queso posee este usuario como una propiedad en el JSON, también querrás esto. Pronto hablaremos de ello.

En cualquier caso, di que sí, llama a la propiedad cheeseListings y di que no a orphanRemoval. Si no conoces esa opción... entonces no la necesitas. Y... ¡bono! Un poco más adelante en este tutorial, te mostraré por qué y cuándo es útil esta opción.

¡Pulsa intro para terminar! Como es habitual, esto hizo algunas cosas: añadió una propiedad $ownera CheeseListing junto con los métodos getOwner() y setOwner(). En User, añadió una propiedad $cheeseListings con un método getCheeseListings()... pero no un método setCheeseListings(). En su lugar, make:entity generó los métodosaddCheeseListing() y removeCheeseListing(). Estos serán útiles más adelante.

195 lines | src/Entity/CheeseListing.php
// ... lines 1 - 37
class CheeseListing
{
// ... lines 40 - 84
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="cheeseListings")
* @ORM\JoinColumn(nullable=false)
*/
private $owner;
// ... lines 90 - 182
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
}

185 lines | src/Entity/User.php
// ... lines 1 - 22
class User implements UserInterface
{
// ... lines 25 - 58
/**
* @ORM\OneToMany(targetEntity="App\Entity\CheeseListing", mappedBy="owner")
*/
private $cheeseListings;
// ... line 63
public function __construct()
{
$this->cheeseListings = new ArrayCollection();
}
// ... lines 68 - 156
public function getCheeseListings(): Collection
{
return $this->cheeseListings;
}
public function addCheeseListing(CheeseListing $cheeseListing): self
{
if (!$this->cheeseListings->contains($cheeseListing)) {
$this->cheeseListings[] = $cheeseListing;
$cheeseListing->setOwner($this);
}
return $this;
}
public function removeCheeseListing(CheeseListing $cheeseListing): self
{
if ($this->cheeseListings->contains($cheeseListing)) {
$this->cheeseListings->removeElement($cheeseListing);
// set the owning side to null (unless already changed)
if ($cheeseListing->getOwner() === $this) {
$cheeseListing->setOwner(null);
}
}
return $this;
}
}

Vamos a crear la migración:

php bin/console make:migration

Y abre eso... para asegurarte de que no contiene nada extra

40 lines | src/Migrations/Version20190509190403.php
// ... lines 1 - 12
final class Version20190509190403 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE cheese_listing ADD owner_id INT NOT NULL');
$this->addSql('ALTER TABLE cheese_listing ADD CONSTRAINT FK_356577D47E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)');
$this->addSql('CREATE INDEX IDX_356577D47E3C61F9 ON cheese_listing (owner_id)');
}
// ... lines 29 - 38
}

Se ve bien: alterando la tabla y configurando la clave foránea. Ejecuta eso:

php bin/console doctrine:migrations:migrate

¡Oh, no! ¡Ha explotado!

No se puede añadir o actualizar una fila hija, falla una restricción de clave foránea

... en la columna owner_id de cheese_listing. Por encima de la propiedad owner, ponemos nullable=false, lo que significa que la columna owner_id de la tabla no puede ser nula. Pero... como nuestra tabla cheese_listing ya tiene algunas filas, cuando intentamos añadir esa nueva columna... no sabe qué valor utilizar para las filas existentes y explota.

Es un clásico fallo de migración. Si nuestro sitio ya estuviera en producción, tendríamos que hacer esta migración más elegante añadiendo primero la nueva columna como anulable, establecer los valores y luego cambiarla a no anulable. Pero como aún no estamos allí... podemos simplemente eliminar todos nuestros datos e intentarlo de nuevo. Ejecuta:

php bin/console doctrine:schema:drop --help

... porque esto tiene una opción que no recuerdo. Ah, aquí está: --full-database nos aseguraremos de eliminar todas las tablas, incluida migration_versions. Ejecuta: :

php bin/console doctrine:schema:drop --full-database --force

Ahora podemos ejecutar todas las migraciones para crear nuestro esquema desde cero:

php bin/console doctrine:migrations:migrate

¡Bien!

Exponer la propiedad de la relación

¡De vuelta al trabajo! En CheeseListing, tenemos una nueva propiedad y un nuevo getter y setter. Pero como estamos utilizando grupos de normalización y desnormalización, esta novedad no está expuesta en nuestra API.

Para empezar, éste es el objetivo: cuando creamos un CheeseListing, un cliente de la API debe poder especificar quién es el propietario. Y cuando leamos un CheeseListing, deberíamos poder ver quién es su propietario. Esto puede parecer un poco extraño al principio: ¿realmente vamos a permitir que un cliente de la API cree un CheeseListing y elija libremente quién es su propietario? Por ahora, sí: establecer el propietario de un listado de queso no es diferente de establecer cualquier otro campo. Más adelante, cuando tengamos un verdadero sistema de seguridad, empezaremos a bloquear las cosas para que no pueda crear unCheeseListing y decir que otro es su propietario.

De todos modos, para que owner forme parte de nuestra API, copia los @Groups() de $price... y añádelos encima de $owner.

196 lines | src/Entity/CheeseListing.php
// ... lines 1 - 37
class CheeseListing
{
// ... lines 40 - 84
/**
// ... lines 86 - 87
* @Groups({"cheese_listing:read", "cheese_listing:write"})
*/
private $owner;
// ... lines 91 - 194
}

¡Vamos a probarlo! Muévete y refresca los documentos. Pero antes de ver CheeseListing, vamos a crear un User para tener algunos datos con los que jugar. Le daré un correo electrónico, una contraseña cualquiera, un nombre de usuario y... Ejecuta. Genial - 201 éxito. Ajusta los datos y crea un usuario más.

Ahora, el momento de la verdad: haz clic para crear un nuevo CheeseListing. Interesante... dice que owner es una "cadena"... lo que puede ser sorprendente... ¿no vamos a establecerlo como un id entero? Vamos a averiguarlo. Intenta vender un bloque de queso desconocido por 20$, y añade una descripción.

Para el propietario, ¿qué ponemos aquí? Veamos... los dos usuarios que acabamos de crear tenían los ids 2 y 1. ¡Bien! Establece el propietario en 1 y ¡ejecuta!

¡Woh! ¡Falla con un código de estado 400!

Se esperaba un IRI o un documento anidado para el atributo owner, se ha dado un entero.

¡Resulta que establecer owner al id no es correcto! A continuación, vamos a arreglar esto, a hablar más de los IRI y a añadir una nueva propiedad cheeseListings a nuestro recurso de la API User.