Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Añadir elementos a una propiedad de la colección

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

Utiliza los documentos para consultar la página User con id=2. Cuando leemos un recurso, podemos decidir exponer cualquier propiedad, y una propiedad que contiene una colección, como cheeseListings, no es diferente. Exponemos esa propiedad añadiendo@Groups("user:read") sobre ella. Y como ésta contiene una colección de objetos relacionados, también podemos decidir si la propiedad cheeseListings debe exponerse como una matriz de cadenas IRI o como una matriz de objetos incrustados, añadiendo este mismo grupo a al menos una propiedad dentro de la propia CheeseListing.

Genial. ¡Nuevo reto! Podemos leer la propiedad cheeseListings en User... pero ¿podríamos también modificar esta propiedad?

Por ejemplo, bueno, es un ejemplo un poco extraño, pero supongamos que un administrador quiere poder editar un User y convertirlo en propietario de algunos objetosCheeseListing existentes en el sistema. Esto ya se puede hacer editando unCheeseListing y cambiando su owner. Pero, ¿podríamos hacerlo también editando unUser y pasándole una propiedad cheeseListings?

En realidad, ¡vamos a ponernos aún más locos! Quiero poder crear un nuevoUser y especificar uno o más listados de queso que este User debería poseer... todo en una sola petición.

Hacer que los listados de queso sean modificables

Ahora mismo, la propiedad cheeseListings no es modificable. La razón es sencilla: esa propiedad sólo tiene el grupo de lectura. ¡Genial! Haré que ese grupo sea un array y añadiré user:write.

... lines 1 - 22
class User implements UserInterface
{
... lines 25 - 58
/**
... line 60
* @Groups({"user:read", "user:write"})
*/
private $cheeseListings;
... lines 64 - 184
}

Ahora, vuelve, refresca los documentos y mira la operación POST: sí tenemos una propiedad cheeseListings. ¡Vamos a hacerlo! Empieza con la aburrida información del usuario: correo electrónico, la contraseña no importa y el nombre de usuario. Para cheeseListings, esto tiene que ser un array... porque esta propiedad contiene un array. Dentro, añade sólo un elemento - un IRI - /api/cheeses/1.

En un mundo perfecto, esto creará un nuevo User y luego irá a buscar elCheeseListing con el id 1 y lo cambiará para que sea propiedad de este usuario. Respira hondo. ¡Ejecuta!

¿Ha funcionado? Es decir, ¡ha funcionado! Un código de estado 201: ¡ha creado el nuevo User y ese User es ahora el propietario de este CheeseListing! Espera un segundo... ¿cómo ha funcionado?

Métodos de adición y eliminación de colecciones

Compruébalo: entendemos cómo se manejan email, password y username: cuando hacemos un POST, el serializador llamará a setEmail(). En este caso, estamos enviando un campocheeseListings... pero si vamos a buscar setCheeseListings(), ¡no existe!

En su lugar, busca addCheeseListing(). Ahhh. El comando make:entity es inteligente: cuando genera una relación de colección como ésta, en lugar de generar un métodosetCheeseListings(), genera addCheeseListing() yremoveCheeseListing(). Y el serializador es lo suficientemente inteligente como para utilizarlos Ve el único IRI de CheeseListing que estamos enviando, consulta la base de datos en busca de ese objeto, llama a addCheeseListing() y lo pasa como argumento.

La razón por la que make:entity genera el sumador -en lugar de sólosetCheeseListings() - es que nos permite hacer cosas cuando se añade o elimina un listado de quesos. ¡Y eso es clave! Compruébalo: dentro del código generado, llama a $cheeseListing->setOwner($this). Por eso el propietario cambió al nuevo usuario, para este CheeseListing con id=1. Entonces... ¡todo se guarda!

Siguiente: cuando estemos creando o editando un usuario, en lugar de reasignar unCheeseListing existente a un nuevo propietario, vamos a hacer posible la creación de listados de quesos totalmente nuevos. Sí, ¡nos estamos volviendo locos! Pero esto nos permitirá aprender aún más sobre cómo piensa y funciona el serializador.

Leave a comment!

4
Login or Register to join the conversation

Hi, is there a way to disable removing in that case, without throwing any exception/validation ? kind of patching my array collection with new data, but i dont want old data be deleted, if I don't provide them inside my PUT payload ?

Reply

Hi Ahmedbhs!

Apologies for the slow reply - vacation last week, and your question was left for me personally! :)

Yes, I think you CAN do this, but it's entirely up to YOUR code to do it. Behind the scenes, iirc, ApiPlatform (via the serializer I believe) looks at the current collection and the submitted collection and finds the "difference". It calls addCheeseListing() for any new items and removeCheeseListing() for any removed items. So, in theory, you could make your removeCheeseListing() do nothing ;) That feels... a bit odd and potentially dangerous to me, but I think that IS the path... I can't think of another way.

Cheers!

Reply
Sung L. Avatar

Hi,

I have a question about adding items to a collection property in many-to-many relations with extra property in bridge table.
Example is sports players and teams. A player can be in multiple teams and teams can have multiple players. Also team can have player(s) as captains.

Here is the table schema:

Players
--
id
name
teams

Teams
--
id
name
players

PlayersTeams
--
player
team
is_captain

And here is the shortened Entities:

// App/Entity/Players.php

/**
* @var \Teams
* @ORM\OneToMany(targetEntity="PlayersTeams", mappedBy="player", fetch="EAGER")
* @Groups({"players:read", "players:write"})
* @Assert\Valid()
*/
private $teams

/**
* @return Collection|Teams[]
*/
public function getTeams(): Collection {
return $this->teams;
}

public function addTeam(Teams $team): self {
if (!$this->teams->contains($team)) {
$this->teams[] = $team;
$playerTeam = new PlayersTeams();
$playerTeam->setPlayer($this);
}

return $this;
}

public function removeTeam(Teams $team): self {
if ($this->teams->contains($team)) {
$this->teams->removeElement($team);
if ($team->getPlayer() === $this) {
$team->setPlayer(null);
}
}

return $this;
}

// App/Entity/PlayersTeams.php

/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @var \Teams
* @ORM\ManyToOne(targetEntity="Teams", inversedBy="players")
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", nullable=false)
* @Groups({"players:read", "players:write", "teams:read", "teams:write"})
*/
private $team;

/**
* @var \Players
* @ORM\ManyToOne(targetEntity="Players", inversedBy="teams")
* @ORM\JoinColumn(name="player_id", referencedColumnName="id", nullable=false)
* @Groups({"players:read", "players:write", "teams:read", "teams:write"})
*/
private $player;

/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"players:read", "players:write", "teams:read", "teams:write"})
*/
private $isCaptain = false;

public function getId(): ?int {
return $this->id;
}

public function getTeam(): Teams {
return $this->team;
}

public function setTeam(Teams $team): self {
$this->team = $team;
return $this;
}

public function getPlayer(): Players {
return $this->player;
}

public function setPlayer(Players $player): self {
$this->player = $player;
return $this;
}

public function getIsCaptain(): bool {
return $this->isCaptain;
}

public function setIsCaptain(bool $isCaptain): self {
$this->isCaptain = $isCaptain;
return $this;
}

When I call /player/1 PUT API, I get the following response: "Expected value of type \"App\\Entity\\PlayersTeams\" for association field \"App\\Entity\\Players#$teams\", got \"App\\Entity\\Teams\" instead."

How can I add items to the embedded objects in this entity relations?

Thank you for your tips!

Reply

Hey Sung,

It sounds like you're trying to set a Teams entity instead of PlayersTeams entity to the Players::$teams property. It sounds like you have an invalid annotation mapping for this property, shouldn't it be "App\Entity\Teams" instead? Otherwise, you should set PlayersTeams entity instead of Teams.

Btw, I'd recommend you also to check your Doctrine mapping configuration with the command:

$ bin/console doctrine:schema:validate

Make sure you have a valid mapping in your application. If you do - most probably you have a logic mistake somewhere.

I hope this helps!

Cheers!

Reply
Cat in space

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

Este tutorial funciona muy bien para Symfony 5 y la Plataforma API 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}