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.