4

I have three entities, Block, BlockPlacement, BlockPosition:

class BlockEntity
{
    private $bid;
    /**
     * @ORM\OneToMany(
     *     targetEntity="BlockPlacementEntity",
     *     mappedBy="block",
     *     cascade={"remove"})
     */
    private $placements;
}

class BlockPlacementEntity
{
    /**
     * The id of the block postion
     *
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="BlockPositionEntity", inversedBy="placements")
     * @ORM\JoinColumn(name="pid", referencedColumnName="pid", nullable=false)
     */
    private $position;

    /**
     * The id of the block
     *
     * @var BlockEntity
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="BlockEntity", inversedBy="placements")
     * @ORM\JoinColumn(name="bid", referencedColumnName="bid", nullable=false)
     */
    private $block;

    private $sortorder;
}

class BlockPositionEntity
{
    private $pid;
    /**
     * @ORM\OneToMany(
     *     targetEntity="BlockPlacementEntity",
     *     mappedBy="position",
     *     cascade={"remove"})
     * @ORM\OrderBy({"sortorder" = "ASC"})
     */
    private $placements;
}

So, you can see the relationship: Block < OneToMany > Placement < ManyToOne > Position.

Now I am trying to construct a form to create/edit a block:

    $builder
        ->add($builder->create('placements', 'entity', [
            'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
            'choice_label' => 'name',
            'multiple' => true,
            'required' => false
        ]))
    ;

This gives me a good select box with multiple selections possible with a proper list of positions to choose from. But it does not show previous selections for placement (I am using existing data) e.g. marking positions as 'selected'. I have not tried creating a new Block yet, only editing existing data.

I suspect I will need to be using addModelTransformer() or addViewTransformer() but have tried some of this an cannot get it to work.

I've looked at the collection form type and I don't like that solution because it isn't a multi-select box. It requires JS and isn't as intuitive as a simple select element.

This seems like such a common issue for people. I've searched and found no common answer and nothing that helps.

Guite
  • 203
  • 1
  • 2
  • 11
craigh
  • 1,909
  • 1
  • 11
  • 24
  • Why do you think about DataTransformers for this trouble? They are used when you need to transform model data. And yes, the collection field is much more complicated for this case. I will add links to my answer below to show you the working code for the same case from my project – Stepan Yudin Nov 26 '15 at 08:01

2 Answers2

0

Update: please look at this example repo

Update 2: i've updated the repo.

I did it with form event listeners and unmapped choice field. Take a closer look at BlockType form type Feel free to ask any questions about it.

Stepan Yudin
  • 470
  • 3
  • 19
  • yes, can I please publish it so I can take a closer look? – craigh Nov 27 '15 at 13:05
  • @craigh, of course, see update block in this answer to get github link – Stepan Yudin Nov 27 '15 at 13:50
  • thank you. I'll take a closer look over the next couple days and get back. – craigh Nov 27 '15 at 17:51
  • OK - I want to thank you again for taking the time to put together a demonstration package - you've really gone out of your way and I appreciate it. – craigh Nov 28 '15 at 14:09
  • Unfortunately, this is not what I need either - you have the block using a select element showing the BlockPosition which is created separately, but what I need is a selected element showing only Position. And on the creation of a block, showing the list of available Positions. I have something locally that might work now. When I confirm it is working like I expect, I will post it here. THANK YOU again! – craigh Nov 28 '15 at 14:12
  • no - I don't think it is a coding mistake or Doctrine specifically. I think it is a problem in concept. Again, I will post what I have in a few days (I hope) when I have it working. :-) – craigh Nov 28 '15 at 14:13
  • God, I misunderstood the task... Of course the right logic is to select Position in the Block form. This is standard case, i'll update the repo and answer. – Stepan Yudin Nov 28 '15 at 14:16
  • I have the unsolved issue with many-to-many with additional field: http://stackoverflow.com/questions/33756301/symfony-and-doctrine-cannot-remove-item-from-collection-many-to-many-relation. And still can't find - what im doing wrong. Maybe you know the proper solution ) – Stepan Yudin Nov 28 '15 at 14:21
  • @craigh, i fixed my app according to your needs. Everything works as you need. I updated the repo and answer, please take a look at BlockType form – Stepan Yudin Nov 28 '15 at 15:54
  • thank you for the updated solution. It appears to be exactly what I need. I have an alternative (and simpler) solution that I may have discovered. I am still working on it and will share it with you once I have it complete. – craigh Nov 29 '15 at 01:01
  • thank you again SO MUCH for providing your answer and taking the extra time to create a bundle with demo code. I very much appreciate your time and effort in helping me! Please see my answer below - as you can see, I came up with a simpler answer. :-) – craigh Dec 02 '15 at 13:19
  • The link is broken and the answer without the link is useless. – nulll Jan 25 '23 at 11:28
0

OK - so in the end, I found a different way. @Stepan Yudin's answer worked, but is complicated (listeners, etc) and not quite like I was hoping.

So, I have the same three entities. BlockPlacement and BlockPosition remain the same (and so aren't reposted, see above) but I have made some changes to the BlockEntity:

class BlockEntity
{
    private $bid;
    /**
     * @ORM\OneToMany(
     *     targetEntity="BlockPlacementEntity",
     *     mappedBy="block",
     *     cascade={"remove", "persist"},
     *     orphanRemoval=true)
     */
    private $placements;

    /**
     * Get an ArrayCollection of BlockPositionEntity that are assigned to this Block
     * @return ArrayCollection
     */
    public function getPositions()
    {
        $positions = new ArrayCollection();
        foreach($this->getPlacements() as $placement) {
            $positions->add($placement->getPosition());
        }

        return $positions;
    }

    /**
     * Set BlockPlacementsEntity from provided ArrayCollection of positionEntity
     * requires
     *   cascade={"remove, "persist"}
     *   orphanRemoval=true
     *   on the association of $this->placements
     * @param ArrayCollection $positions
     */
    public function setPositions(ArrayCollection $positions)
    {
        // remove placements and skip existing placements.
        foreach ($this->placements as $placement) {
            if (!$positions->contains($placement->getPosition())) {
                $this->placements->removeElement($placement);
            } else {
                $positions->removeElement($placement->getPosition()); // remove from positions to add.
            }
        }

        // add new placements
        foreach ($positions as $position) {
            $placement = new BlockPlacementEntity();
            $placement->setPosition($position);
            // sortorder is irrelevant at this stage.
            $placement->setBlock($this); // auto-adds placement
        }
    }
}

So you can see that the BlockEntity is now handling a positions parameter which doesn't exist in the entity at all. Here is the relevant form component:

$builder
    ->add('positions', 'Symfony\Bridge\Doctrine\Form\Type\EntityType', [
        'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
        'choice_label' => 'name',
        'multiple' => true,
    ])

note that I have changed to Symfony 2.8 form style since my first post

This renders a multiple select element on the page which accepts any number of positions and converts them to an ArrayCollection on submit. This is then handled directly by the form's get/set position methods and these convert to/from the placement property. The cascade and orphanRemoval are important because they take care to 'clean up' the leftover entities.

because it is references above here is the BlockPlacement setBlock($block) method:

public function setBlock(BlockEntity $block = null)
{
    if ($this->block !== null) {
        $this->block->removePlacement($this);
    }

    if ($block !== null) {
        $block->addPlacement($this);
    }

    $this->block = $block;

    return $this;
}
craigh
  • 1,909
  • 1
  • 11
  • 24