I want to be able to remove an entity from a collection using a symfony2 Form.
I can add new entities to the collection, and remove them, as long as the entity being added or removed is at the end of the collection. As soon as I remove one from the start or middle I get the following error:
When I try to do this I get this error:
Neither the property "id" nor one of the methods "addId()"/"removeId()", "setId()", "id()", "__set()" or "__call()" exist and have public access in class "ApiBundle\Entity\Data\Column".
Here is all the relevant code.
Data
/**
* Data
*
* @ORM\Table(name="data__data")
* @ORM\Entity(repositoryClass="ApiBundle\Repository\Data\DataRepository")
*/
class Data
{
/**
* @var integer
*
* @ORM\Column(name="id", type="string")
* @ORM\Id
* @ORM\GeneratedValue(strategy="UUID")
*/
protected $id;
/**
* @var ArrayCollection
* @ORM\OneToMany(targetEntity="Column", mappedBy="parent", cascade={"all"}, orphanRemoval=true)
*/
protected $columns;
/**
* Initialise the array collections
*/
public function __construct()
{
$this->columns = new ArrayCollection();
}
/**
* @param mixed $columns
*/
public function setColumns($columns)
{
$this->columns = $columns;
}
/**
* @param Column $column
*/
public function addColumn($column)
{
$column->setParent($this);
$this->columns->add($column);
}
/**
* @param Column $column
*/
public function removeColumn($column)
{
$this->columns->removeElement($column);
}
}
Column
/**
* Data
*
* @ORM\Table(name="data__column")
* @ORM\Entity
*/
class Column
{
/**
* @var integer
*
* @ORM\Column(name="id", type="string")
* @ORM\Id
* @ORM\GeneratedValue(strategy="UUID")
*/
protected $id;
/**
* @var Data
* @ORM\ManyToOne(targetEntity="Data", inversedBy="columns")
*/
protected $parent;
/**
* @return Data
*/
public function getParent()
{
return $this->parent;
}
/**
* @param Data $parent
*/
public function setParent($parent)
{
$this->parent = $parent;
}
}
DataFormType
class DataFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id')
->add('columns', 'collection', array(
'type' => new ColumnFormType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ApiBundle\Entity\Data\Data',
'csrf_protection' => false
));
}
public function getName()
{
return 'data';
}
}
ColumnFormType
class ColumnFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ApiBundle\Entity\Data\Column',
'csrf_protection' => false
));
}
public function getName()
{
return 'data_column';
}
}
I have removed some code from these snippets for clarity
Like I say, I get no problems when adding or deleting from the end of the collection. But as soon as it is anywhere else it errors out.
Thanks for any help.
The error is caused by the lack of the collection key preserving.
CollectionType
is high tought with ResizeListener
. It fills the collection form with subforms:
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
...
// Then add all rows again in the correct order
foreach ($data as $name => $value) {
$form->add($name, $this->type, array_replace(array(
'property_path' => '['.$name.']',
), $this->options));
}
}
So the every subform is mapped to the collection object (underlying data) and has the name that applies to collection index, e.g. '[0]', '[1]'. When you delete elements from the collection ResizeListener
removes redundant subforms.
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
...
// Remove all empty rows
if ($this->allowDelete) {
foreach ($form as $name => $child) {
if (!isset($data[$name])) {
$form->remove($name);
}
}
}
}
Lets say there were data[columns][0][id]=1, data[columns][1][id]=2, data[columns][2][id]=3
.
When you remove an element from the end - all is fine. There comes data[columns][0][id]=1, data[columns][1][id]=2
with corresponding content. Then the subform [2]
will be deleted and then the element with index 2 will be deleted from the collection.
When you remove an element not at the end and you don't preserve keys - the error occurs. For example you send data[columns][0][id]=2, data[columns][1][id]=3
. ResizeListener
will delete the subform with index [2]
. Underlying data will be overrided for the rest of subforms ([0]
, [1]
) and their child (id
). Most nested subforms are processed first.
[0] (Column)
[id]
1 => 2
[1] (Column)
[id]
2 => 3
Then PropertyPathMapper
will detect that id
subform's data is not equals to Column's id
property value (this is underlying data of the [0]
):
public function mapFormsToData($forms, &$data)
{
...
if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
}
...
}
It will make PropertyAccessor
to set new id
value to Column
object. The last one will throw exception as there is no way to set new id
to Column (no setter, property is not public, etc).
Solution: To preserve key order. If you get data[columns][0][id]=1, data[columns][1][id]=2, data[columns][2][id]=3
and you delete first element you should send data[columns][1][id]=2, data[columns][2][id]=3
PS Key order preserving for Forms is good practice for all cases. It will prevent you from redundant UPDATE
queries and loops.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With