0
votes

I have a ZF2 form with form collection element. And I have doctrine 2 entity. I bind this entity to the form. Here is my code:

$form->bind($entity); // $entity->roles is not empty. It has two elements
$form->setData($someData); // $someData['roles'] is empty array
if ($form->isValid()) {
    $form->get('roles')->getCount(); // 2(!) it is not empty!
    saveToDb($entity);
}
return $form;

Form collection's name is "roles". You can see that I bind the entity before I set data to the form. When user wants to update entity, entity already has values. For example there is already two values in the form collection. For example user wants to clear roles and $someData array has empty roles. The problem is that $form->setData don't clear roles collection. How to clear this collection? If you will look at Collection::populateValues() method you will see that it does nothing if data is empty.

3

3 Answers

1
votes

It seems to be related to this bug: https://github.com/zendframework/zf2/issues/4492

And a possible temp solution can be:

if (empty($someData['roles'])) {
    $entity->setRoles(array());
}
$form->bind($entity);
$form->setData($someData);
if ($form->isValid()) {
    saveToDb($entity);
}
return $form;
0
votes

I have two entities (roles & permissions), my roles entity exists of a id, name and a ArrayCollections of permission entities:

object(Authorization\Entity\Role)#525 (3) {
  ["id":protected] => 1
  ["name":protected] => string(7) "admin"
  ["permissions":protected] => object(Doctrine\Common\Collections\ArrayCollection)#524 (1) {
    ["_elements":"Doctrine\Common\Collections\ArrayCollection":private] => array(4) {
      [1] => object(Authorization\Entity\Permission)#527 (2) {
        ["id":protected] => int(1)
        ["name":protected] => "createUser"
      }
      [2] => object(Authorization\Entity\Permission)#529 (2) {
        ["id":protected] => int(2)
        ["name":protected] => "updateUser"
      }
      [3] => object(Authorization\Entity\Permission)#526 (2) {
        ["id":protected] => int(3)
        ["name":protected] => "createRole"
      }
      [4] => object(Authorization\Entity\Permission)#528 (2) {
        ["id":protected] => int(4)
        ["name":protected] => "updateRole"
      }
    }
  }
}

To update / clear the permissions, I send the data from the form to my RoleDao:

public function update(RoleViewObject $roleVO) {
        // Find VO by using the getId() function to update
        // the right VO object
        $role = $this->getRoleById($roleVO->getId());

        // Delete all permissions from VO <--- I think this is what you need
        $role->getPermissions()->clear();

        // Add updated permissions to VO
        foreach($roleVO->getPermissions() as $permissionId => &$permissionVO) {
            $permissionName = $permissionVO->getName();
            if(empty($permissionName)) {
                $role->getPermissions()->set($permissionVO->getId(), $this->entityManager->getReference('Authorization\Entity\Permission', $permissionVO->getId()));
            }
        }

        $this->entityManager->persist($role);
        $this->entityManager->flush();
    }
0
votes

The actual problem is that Zend forms don't bother with values that have not been sent, and in case of emptying a collection you would simply not send any data, which is why the form ignores the collection, resulting in the form and/or its fieldsets not telling their hydrator to do any changes to the collection.

Ultimately, you can blame this function, which removes filtered data extracted from the form not represented in the array passed to "setData".

I managed to solve this problem by overriding the form's "setData" function to additionally process the passed data to include empty arrays for collections that are still in the fieldset, but not represented in the data array:

namespace Module\Form;

class Form extends \Zend\Form\Form
{
    /**
     * Fill the passed data array with placeholder arrays for collections
     * existing in the passed fieldset (and its sub-fieldsets) but not
     * represented in the data array.
     * 
     * @param \Zend\Form\FieldsetInterface $fieldset
     * @param array $data
     * @return array
     */
    protected static function assertCollectionPlaceholders(\Zend\Form\FieldsetInterface $fieldset, array $data)
    {
        foreach ($fieldset as $name => $elementOrFieldset) {
            if (!$elementOrFieldset instanceof \Zend\Form\FieldsetInterface) {
                continue;
            }

            if (array_key_exists($name, $data) && is_array($data[$name])) {
                $data[$name] = static::assertCollectionPlaceholders($elementOrFieldset, $data[$name]);
            } else if ($elementOrFieldset instanceof \Zend\Form\Element\Collection) {
                $data[$name] = array();
            }
        }

        return $data;
    }

    /**
     * Set data to validate and/or populate elements
     *
     * Typically, also passes data on to the composed input filter.
     *
     * @see \Zend\Form\Form
     * @param  array|\ArrayAccess|Traversable $data
     * @return self
     * @throws \Zend\Form\Exception\InvalidArgumentException
     */
    public function setData($data)
    {
        if ($data instanceof \Traversable) {
            $data = \Zend\Stdlib\ArrayUtils::iteratorToArray($data);
        }

        if (!is_array($data)) {
            throw new \Zend\Form\Exception\InvalidArgumentException(sprintf(
                '%s expects an array or Traversable argument; received "%s"',
                __METHOD__,
                (is_object($data) ? get_class($data) : gettype($data))
            ));
        }

        $data = static::assertCollectionPlaceholders($this, $data);

        $this->hasValidated = false;
        $this->data         = $data;
        $this->populateValues($data);

        return $this;
    }
}

By doing so, the form and/or its fieldsets tell their hydrators that the collection is empty and, in case of Doctrine's hydrator, prompting them to remove elements not in the collection.