Juan Etxenike
Juan Etxenike

Reputation: 69

Symfony: Persisting in a "ManyToMany with extra data" after implementing bridge entity

I am trying to persist a "collection" of objects that are related between them through an entity that is in a ManyToOne relation with two other entities in order to provide a kind of "ManyToMany" relation but with extrafields.

I have followed the only strategy proposed at symfonycasts and also here in stackoverflow by different users which is to create an entity that will contain as properties the two other entities plus one extra field.

Users make reservations to a travel on a given date, but such reservations offer different options depending on the travel type.

I make part of the reservation process using javascript and ajax, but yet the problem I have is that my controller will only persist the data from the last item from the array.

The controller's lines where I am encountering the trouble are these:

$options = $request->request->get('options') ? $request->request->get('options') : [];
        $dateId = $request->request->get('dateId');
        $em = $this->getDoctrine()->getManager();
        $datesRepository = $em->getRepository(Dates::class);
        $date = $datesRepository->find($dateId);

        $reservation = new Reservation();
        $now = new \DateTime();
        $reservation->setDateAjout($now);
        $reservation->setStatus('initialized');
        $reservation->setDate($date);
        /** INTRODUCING OPTIONS */
        if(count($options) > 0) {
            $reservationOptions = new ReservationOptions();
            $optionsRepository = $em->getRepository(Options::class);
            foreach ( $options as $key => $value ){
                if($value > 0) {
                    $option = $optionsRepository->find($key);
                    $reservationOptions->setOptions($option);
                    $reservationOptions->setAmount($value);
                    $reservation->addReservationOption($reservationOptions);
//This only persist on the last loop
                }
            }
        }
        $em->persist($reservation);
        $em->flush();

The entity that relates both entities in a for a "ManyToMany + extra fields" relation:

<?php

namespace App\Entity;

use App\Repository\ReservationOptionsRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ReservationOptionsRepository::class)
 */
class ReservationOptions
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(
     *                  targetEntity=Reservation::class, 
     *                  inversedBy="reservationOptions", 
     *                  cascade={"persist"}
     * )
     * @ORM\JoinColumn(nullable=false)
     */
    private $reservation;

    /**
     * @ORM\ManyToOne(
     *                  targetEntity=Options::class, 
     *                  inversedBy="reservationOptions", 
     *                  cascade={"persist"}
     * )
     * @ORM\JoinColumn(nullable=false)
     */
    private $options;

    /**
     * @ORM\Column(type="integer")
     */
    private $amount;

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

    public function getReservation(): ?Reservation
    {
        return $this->reservation;
    }

    public function setReservation(?Reservation $reservation): self
    {
        $this->reservation = $reservation;

        return $this;
    }

    public function getOptions(): ?Options
    {
        return $this->options;
    }

    public function setOptions(?Options $options): self
    {
        $this->options = $options;

        return $this;
    }

    public function getAmount(): ?int
    {
        return $this->amount;
    }

    public function setAmount(int $amount): self
    {
        $this->amount = $amount;

        return $this;
    }

    public function __toString()
    {
        return $this->id;
    }
}

The reservation entity looks like this:

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\ReservationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource()
 * @ORM\Entity(repositoryClass=ReservationRepository::class)
 */
class Reservation
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Dates::class, inversedBy="reservations")
     */
    private $date;

    /**
     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="reservations")
     */
    private $user;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $status;

    /**
     * @ORM\OneToMany(targetEntity=ReservationOptions::class, mappedBy="reservation", orphanRemoval=true, cascade={"persist"})
     */
    private $reservationOptions;


    public function __construct()
    {
        $this->travellers = new ArrayCollection();
        $this->reservationOptions = new ArrayCollection();
    }

    public function __toString():string
    {
        return $this->getStatus();
    }

    public function getStatus(): ?string
    {
        return $this->status;
    }

    public function setStatus(?string $status): self
    {
        $this->status = $status;

        return $this;
    }

    /**
     * @return Collection|ReservationOptions[]
     */
    public function getReservationOptions(): Collection
    {
        return $this->reservationOptions;
    }

    public function addReservationOption(ReservationOptions $reservationOption): self
    {
        if (!$this->reservationOptions->contains($reservationOption)) {
            $this->reservationOptions[] = $reservationOption;
            $reservationOption->setReservation($this);
        }

        return $this;
    }

    public function removeReservationOption(ReservationOptions $reservationOption): self
    {
        if ($this->reservationOptions->removeElement($reservationOption)) {
            // set the owning side to null (unless already changed)
            if ($reservationOption->getReservation() === $this) {
                $reservationOption->setReservation(null);
            }
        }

        return $this;
    }

}

The options file looks like this:

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * Options
 *
 * @ORM\Table(name="options")
 * @ORM\Entity(repositoryClass="App\Repository\OptionsRepository")
 */
class Options
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned"=true})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;



    /**
     * @var string
     *
     * @ORM\Column(name="prix", type="decimal", precision=10, scale=2, nullable=false)
     */
    private $price;

    /**
     * @ORM\ManyToOne(targetEntity=Travel::class, inversedBy="options")
     */
    private $travel;

    /**
     * @ORM\OneToMany(targetEntity=OptionsTranslations::class, mappedBy="options", orphanRemoval=true, cascade={"persist"})
     */
    private $optionsTranslations;

    /**
     * @ORM\OneToMany(targetEntity=ReservationOptions::class, mappedBy="options", orphanRemoval=true, cascade={"persist"})
     */
    private $reservationOptions;

    public function __construct()
    {
        $this->optionsTranslations = new ArrayCollection();
        $this->reservationOptions = new ArrayCollection();
    }

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

    public function getPrice(): ?string
    {
        return $this->price;
    }

    public function setPrice(string $price): self
    {
        $this->price = $price;

        return $this;
    }

    public function getTravel(): ?Travel
    {
        return $this->travel;
    }

    public function setTravel(?Travel $travel): self
    {
        $this->travel = $travel;

        return $this;
    }

    /**
     * @return Collection|OptionsTranslations[]
     */
    public function getOptionsTranslations(): Collection
    {
        return $this->optionsTranslations;
    }

    public function addOptionsTranslation(OptionsTranslations $optionsTranslation): self
    {
        if (!$this->optionsTranslations->contains($optionsTranslation)) {
            $this->optionsTranslations[] = $optionsTranslation;
            $optionsTranslation->setOptions($this);
        }

        return $this;
    }

    public function removeOptionsTranslation(OptionsTranslations $optionsTranslation): self
    {
        if ($this->optionsTranslations->removeElement($optionsTranslation)) {
            // set the owning side to null (unless already changed)
            if ($optionsTranslation->getOptions() === $this) {
                $optionsTranslation->setOptions(null);
            }
        }

        return $this;
    }
    public function __toString():string
    {
        return $this->travel->getMainTitle();
    }

    /**
     * @return Collection|ReservationOptions[]
     */
    public function getReservationOptions(): Collection
    {
        return $this->reservationOptions;
    }

    public function addReservationOption(ReservationOptions $reservationOption): self
    {
        if (!$this->reservationOptions->contains($reservationOption)) {
            $this->reservationOptions[] = $reservationOption;
            $reservationOption->setOptions($this);
        }

        return $this;
    }

    public function removeReservationOption(ReservationOptions $reservationOption): self
    {
        if ($this->reservationOptions->removeElement($reservationOption)) {
            // set the owning side to null (unless already changed)
            if ($reservationOption->getOptions() === $this) {
                $reservationOption->setOptions(null);
            }
        }

        return $this;
    }

}

Upvotes: 1

Views: 706

Answers (1)

Arleigh Hix
Arleigh Hix

Reputation: 10897

You are reusing the same ReservationOptions entity, changing the option and value on each iteration of the loop. So, you end up with only one ReservationOptions having the values from the last item.
You need to create a new ReservationOptions entity for each option/value.

    /** INTRODUCING OPTIONS */
    if(count($options) > 0) {
        $optionsRepository = $em->getRepository(Options::class);
        foreach ( $options as $key => $value ){
            if($value > 0) {
                $reservationOptions = new ReservationOptions();
                $option = $optionsRepository->find($key);
                $reservationOptions->setOptions($option);
                $reservationOptions->setAmount($value);
                $reservation->addReservationOption($reservationOptions);
            }
        }
    }
    $em->persist($reservation);
    $em->flush();

Upvotes: 1

Related Questions