8000 [Form] Collection Type with addXxx and removeXxx · Issue #3732 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Collection Type with addXxx and removeXxx #3732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
pocallaghan opened this issue Mar 30, 2012 · 2 comments
Closed

[Form] Collection Type with addXxx and removeXxx #3732

pocallaghan opened this issue Mar 30, 2012 · 2 comments

Comments

@pocallaghan
Copy link

I recently tried to set up a one-to-many relationship using the collection type to allow adding and removing elements. I had tremendous trouble getting it working, during the process of which I came across several issues, some of which are already reported, the last of which I don't believe is. I've include some information on all of it, for anybody attempting the same task.

The very first problem I had was that the addXxx and removeXxx functions never seemed to be called, I eventually tracked this down to a bug/ version incompatibility that had already been reported (Doctrines PersistentCollection didn't have __clone, meaning when the dataSnapshot is taken in MergeCollectionListener, the inner collection isn't cloned, thus changes are made to the dataSnapshot), I fixed this by updating composer.json to use doctrine/orm v2.2.1.

The second issue I had was that whilst addXxx was now being called, it was passing an array rather than an instance of my entity, I tracked this down to another bug report [https://github.com//issues/3354] , I used the suggest work around of adding the data_class to the options in the parent type.

At this point I am using symfony/symfony v2.1.* with doctrine/orm v2.2.1

After these steps the addXxx function seemed to work, but removeXxx was never getting triggered. After looking through the code I identified where I think the issue is as being MergeCollectionListener::onBindNormData. The logic for populating $itemsToDelete doesn't seem to make much sense to me. I have managed to get my test code up and running by changing the code....

          // Item not found, remember for deletion
            foreach ($originalData as $key => $item) {
                if ($item === $originalItem) {
                    $itemsToDelete[$key] = $item;
                    continue 2;
                }
            }

...to...

            foreach($originalData as $key => $item) {
                if ($item === $originalItem) {
                    continue 2;
                }
            }
            $itemsToDelete[] = $originalItem;

Here are the relevant Entities and Types, the controller / twig templates in use are the ones generated by the crud generator (with some JS added to add / remove fields).

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Table(name="band")
 * @ORM\Entity
 */
class Band
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string $number
     *
     * @ORM\Column(name="name", type="string", length=128)
     */
    private $name;

    /**
     * @var Doctrine\Common\Collections\ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="BandDetail", mappedBy="band", cascade={"all"})
     */
    private $details;

    public function __construct()
    {
        $this->details = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Band
     */
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Add details
     *
     * @param Acme\DemoBundle\Entity\BandDetail $details
     */
    public function addDetail(\Acme\DemoBundle\Entity\BandDetail $detail)
    {
        $detail->setBand($this);
        $this->details->add($detail);
    }

    public function removeDetail(\Acme\DemoBundle\Entity\BandDetail $detail)
    {
        $detail->setBand(null);
        $this->details->removeElement($detail);
    }

    public function setDetails($details)
    {
        foreach($details as $detail) {
            $detail->setBand($this);
        }

        $this->details = $details;
    }

    /**
     * Get details
     *
     * @return Doctrine\Common\Collections\Collection
     */
    public function getDetails()
    {
        return $this->details;
    }
}

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Table(name="band_details")
 * @ORM\Entity
 */
class BandDetail
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Band", inversedBy="details")
     * @ORM\JoinColumn(name="band_id", referencedColumnName="id")
     */
    private $band;

    /**
     * @var string $number
     *
     * @ORM\Column(name="text", type="string", length=128)
     */
    private $text;

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set text
     *
     * @param string $text
     * @return BandDetail
     */
    public function setText($text)
    {
        $this->text = $text;
        return $this;
    }

    /**
     * Get text
     *
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set band
     *
     * @param Acme\DemoBundle\Entity\Band $band
     * @return BandDetail
     */
    public function setBand(\Acme\DemoBundle\Entity\Band $band = null)
    {
        $this->band = $band;
        return $this;
    }

    /**
     * Get band
     *
     * @return Acme\DemoBundle\Entity\Band
     */
    public function getBand()
    {
        return $this->band;
    }
}

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class BandDetailType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('text')
        ;
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Acme\DemoBundle\Entity\BandDetail',
        );
    }

    public function getName()
    {
        return 'banddetail';
    }
}

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class BandType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('details', 'collection', array(
                'type' => new BandDetailType(),
                'allow_add' => true,
                'allow_delete' => true,
                'prototype' => true,
                'by_reference' => true,
                'options' => array('data_class' => 'Acme\DemoBundle\Entity\BandDetail'),
            ))
        ;
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Acme\DemoBundle\Entity\Band',
        );
    }

    public function getName()
    {
        return 'acme_demobundle_bandtype';
    }
}

Apologies for the length of the post, I was just trying to be thorough. I am a newcomer to both Symfony and Doctrine, but I'm relatively confident there is a bug here. Can anybody shed some light on to whether this is a valid fix to the issue, or whether I'm just doing something wrong?

Cheers.

@stof
Copy link
Member
stof commented Mar 30, 2012

@bschussek ping

@webmozart
Copy link
Contributor

Confirmed. Working on this.

fabpot added a commit that referenced this issue Apr 10, 2012
Commits
-------

c4e68a3 [Form] Moved logic of addXxx()/removeXxx() methods to the PropertyPath class

Discussion
----------

[Form] Moved logic of addXxx()/removeXxx() methods to the PropertyPath class

Bug fix: yes
Feature addition: no
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: #3732
Todo: -

![Travis Build Status](https://secure.travis-ci.org/bschussek/symfony.png?branch=issue3732)

The addXxx()/removeXxx() methods should now be called correctly in ChoiceType and CollectionType.

PropertyPath now favors addXxx()/removeXxx() over setXxx() for collections. For example:

```
$propertyPath = new PropertyPath('article.tags');

// Tries to use addTag()/removeTag() and only uses setTags() (et al.)
// if not found
$propertyPath->setValue($article, $tags);
```

For other languages than English or very irregular plurals, a custom singular can be set by separating it with a pipe:

```
$propertyPath = new PropertyPath('article.genera|genus');
```

---------------------------------------------------------------------------

by bschussek at 2012-04-07T12:40:39Z

Again, the failing build is not my fault.
@fabpot fabpot closed this as completed Apr 10, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants
0