pusle
pusle

Reputation: 1523

Symfony 3 One To Many: how to add inverse side to owning side when persisting inverse side

I have these entities with one to many relationships:

freightOrder has many shipments

shipment has many shipmentLines

The freightOrder entity looks like this:

<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ExclusionPolicy("all")
 */
class FreightOrder
{
    /**
     * @Expose
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @Expose
     *
     * @ORM\OneToMany(
     *     targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\Shipment",
     *     mappedBy="freightOrder",
     *     cascade={"persist","remove"}
     * )
     */
    private $shipments;

    /**
     * @Expose
     *
     * @ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
     * @ORM\JoinColumn(name="freighter_id", referencedColumnName="id")
     */
    private $freighter;



    /**
     * Order constructor.
     */
    public function __construct()
    {
        $this->shipments = new ArrayCollection();
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getShipments()
    {
        return $this->shipments;
    }

    /**
     * @param mixed $shipment
     */
    public function addShipment(Shipment $shipment)
    {
        $shipment->setFreightOrder($this);
        $this->shipments->add($shipment);
    }

    public function removeShipment(Shipment $shipment)
    {
        $this->shipments->removeElement($shipment);
    }

    /**
     * @return mixed
     */
    public function getFreighter()
    {
        return $this->freighter;
    }

    /**
     * @param mixed $freighter
     */
    public function setFreighter($freighter)
    {
        $this->freighter = $freighter;
    }
}

The shipment entity:

<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Fraktportalen\Bundle\AccountBundle\Entity\Account;
use Fraktportalen\Bundle\AddressBundle\Entity\Address;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ExclusionPolicy("all")
 */
class Shipment
{
    /**
     * @Expose
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @Expose
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $pickup;

    /**
     * @Expose
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $delivery;

    /**
     * @Expose
     *
     * @ORM\OneToMany(
     *     targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\ShipmentLine",
     *     mappedBy="shipment",
     *     fetch="EAGER",
     *     cascade={"persist"}
     * )
     */
    private $shipmentLines;

    /**
     * @Expose
     *
     * @ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\ShipmentBundle\Entity\FreightOrder", inversedBy="shipments")
     * @ORM\JoinColumn(name="freight_order_id", referencedColumnName="id", nullable=false)
     */
    private $freightOrder;

    /**
     * @Expose
     *
     * @ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
     * @ORM\JoinColumn(name="sender_id", referencedColumnName="id")
     */
    private $sender;

    /**
     * @Expose
     *
     * @ORM\ManyToOne(targetEntity="Fraktportalen\Bundle\AccountBundle\Entity\Account")
     * @ORM\JoinColumn(name="receiver_id", referencedColumnName="id")
     */
    private $receiver;



    /**
     * Shipment constructor.
     */
    public function __construct()
    {
        $this->shipmentLines = new ArrayCollection();
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getFreightOrder() {
        return $this->freightOrder;
    }

    /**
     * @param mixed $freightOrder
     *
     * @return Shipment
     */
    public function setFreightOrder($freightOrder) {
        $this->freightOrder = $freightOrder;
        $freightOrder->addShipment($this);
        return $this;
    }

    /**
     * @return mixed
     */
    public function getPickup()
    {
        return $this->pickup;
    }

    /**
     * @param mixed $pickup
     */
    public function setPickup(\DateTime $pickup = null)
    {
        $this->pickup = $pickup;
    }

    /**
     * @return mixed
     */
    public function getDelivery()
    {
        return $this->delivery;
    }

    /**
     * @param mixed $delivery
     */
    public function setDelivery(\DateTime $delivery = null)
    {
        $this->delivery = $delivery;
    }

    /**
     * @return mixed
     */
    public function getShipmentLines()
    {
        return $this->shipmentLines;
    }

    /**
     * @param mixed $shipmentLine
     */
    public function addShipmentLine(ShipmentLine $shipmentLine)
    {
        $shipmentLine->setShipment($this);
        $this->shipmentLines->add($shipmentLine);
    }

    public function removeShipmentLine(ShipmentLine $shipmentLine)
    {
        $this->shipmentLines->removeElement($shipmentLine);
    }

    /**
     * @return mixed
     */
    public function getSender()
    {
        return $this->sender;
    }

    /**
     * @param mixed $sender
     */
    public function setSender(Account $sender)
    {
        $this->sender = $sender;
    }

    /**
     * @return mixed
     */
    public function getReceiver()
    {
        return $this->receiver;
    }

    /**
     * @param mixed $receiver
     */
    public function setReceiver(Account $receiver)
    {
        $this->receiver = $receiver;
    }
}

The freightOrderType next:

<?php
namespace Fraktportalen\Bundle\ShipmentBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class FreightOrderType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('freighter', EntityType::class, array(
                'class' => 'AccountBundle:Account',
            ))
            ->add('shipments', CollectionType::class, array(
                'entry_type' => ShipmentType::class,
                'allow_add' => true
            ))
        ;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Fraktportalen\Bundle\ShipmentBundle\Entity\FreightOrder',
            'csrf_protection' => false
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'freightOrder';
    }
}

When saving a freightOrder from an Ember app, it sends this json:

{
    "freightOrder": {
        "freighter": "3",
        "shipments": [{
            "pickup": "2017-03-22 12:32:00",
            "delivery": "2017-03-23 12:32:00",
            "sender": "1",
            "receiver": "2",
            "shipment_lines": [{
                "package_weight": 45,
                "package_length": 240,
                "package_width": 120,
                "package_height": 240,
                "package_type": "3"
            }]
        }]
    }
}

Everything works as expected except that freightOrder on shipments is not saved. Even though I add it on the freightOrder entity addShipment function:

/**
 * @param mixed $shipment
 */
public function addShipment(Shipment $shipment)
{
    $shipment->setFreightOrder($this);
    $this->shipments->add($shipment);
}

As you can see in the shipment entity, I have even tried to add shipment to freightOrder there. But nothing works.

Shipment lines are added to shipment though, and that is also a one to many relationship.

Will I have to add a form event listener to the freightOrderType and manually add freightOrder to all shipments? I was under the impression that adding freightOrder to shipments when adding said shipments to freightOrder would solve it. At least that is what I have found when searching SO.

Thanks for your time, Tommy

Upvotes: 2

Views: 675

Answers (1)

DevDonkey
DevDonkey

Reputation: 4880

I feel your pain, I fight with this problem quite often.

It sounds like the setter isnt getting called. You can sometimes fix this by declaring by_reference to false.

try this..

    $builder
        ->add('freighter', EntityType::class, array(
            'class' => 'AccountBundle:Account',
        ))
        ->add('shipments', CollectionType::class, array(
            'entry_type' => ShipmentType::class,
            'allow_add' => true,
            'by_reference' => false,
        ))
    ;

reading the docs:

Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

Personally, I still think by_reference is some kind of hacky fix for a bug they had once.. I guess we'll never know..

Upvotes: 4

Related Questions