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.
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;
}