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 User
en nuestro sistema. Para configurar esto, olvídate por un momento de la API y modelémoslo en la base de datos.
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
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
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 40DragonTreasure
s cada uno acaparado aleatoriamente por uno de estos 10 User
s.
¡Vamos a probarlo! Ejecuta:
symfony console doctrine:fixtures:load
Esta vez... ¡éxito!
Vale, ahora DragonTreasure
tiene una nueva propiedad de relación owner
... y User
tiene 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!
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}