5

The form consists of one question which has several answers, so that the answers can be dynamically created for each question. This stuff all works fine:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('question','textarea')
        ->add('answers', 'collection', array(
            'type'=>new AnswerType(),
            'allow_add'=>true,
            'allow_delete'=>true,
            'label' => false
        ))
    ;
}

Here is form code for AnswerType:

    $builder
        ->add('answer','text', array(
            'attr'=>array(
                'class'=>'form-control'
            ),
            'label'=>false
        ))
        ->add('isGoodAnswer', 'checkbox', array(
            'label'=>'Good?',
            'required'=>false
        ))
    ;

I am using prototype template to populate container via jquery.

Adding new answer objects to the question object works fine. Deleting answers is also not a problem.

However, if I go to update existing property on one of the collection form inputs it does not update the existing object. It is persisting the question object though as it will update the text of the question itself. I can only delete and create new to replace something currently and I am having a hard time figuring out why.

Here is snippet of code from template form that is submitted:

    <ul id="answer-fields-list" data-prototype="{{ form_widget(form.answers.vars.prototype)|e }}">
    {% for answer in form.answers %}
        <li>
            <div class='col-md-12'>                 
                {{ form_widget(answer) }}                   
                <div>
                    <a href='#' class='btn btn-sm btn-danger delete-this'><span class='glyphicon glyphicon-trash'></span></a>
                </div>
            </div>                  
        </li>
    {% endfor %}
    </ul>           
    <a href="#" id="add-answer" class='btn btn-sm btn-success'><span class='glyphicon glyphicon-plus-sign'></span> Add Answer</a>           

edit, here is full controller code for this update method:

    $question = $em->getRepository('ChecklistMainBundle:ChecklistQuestion')->findOneById($questionId);
    if(!$question) throw new NotFoundHttpException('Question not found');

    $form = $this->createForm(new QuestionAnswerType(), $question);

    $form->handleRequest($request);

    if($request->getMethod()=='POST' && $form->isValid())
    {
        if($form->get('attachment')->getData() != null) {
            $question->uploadAttachment();
        }
        $em->persist($question);
        $em->flush();

        $this->get('session')->getFlashBag()->add('success', 'Question was modified successfully!');

        return $this->redirect($this->generateUrl('admin_checklists_view', array('id'=>$id)));
    }
skrilled
  • 5,350
  • 2
  • 26
  • 48
  • You should share snippet of code where you handle input – Michal Wilkowski Dec 01 '14 at 23:27
  • Hey Michal, I'm just doing `$form->handleRequest($request);` then persisting the $question object. The form is created via symfony2 form builder `$form = $this->createForm(new QuestionAnswerType(), $question);` – skrilled Dec 02 '14 at 20:36
  • 1
    Ok, just to clarify that I understood correctly: your changes are persisted in database, however object is not changed in PHP code and form is rendered with old stuff, right? Your approach looks fine (by the book). I am asking for code snippet, because perhaps there is some small mistake of recreating object, rendering it once again, reading it from database instead of form etc. – Michal Wilkowski Dec 03 '14 at 08:11
  • Correct, if I update the question text it saves properly. If I add new answers or delete answers, it appends the collection or deletes these answer objects as expected. It's only when I modify the answer text on an existing answer object that it doesn't update as expected. – skrilled Dec 03 '14 at 20:34
  • I am afraid I cannot help you. I believe it could something in controller that needs further investigation – Michal Wilkowski Dec 04 '14 at 17:56
  • Can you please put Mapping for Question/Answer? Do you have any listeners for this actions? – origaminal Dec 04 '14 at 19:49
  • 1
    This is one of those questions which cannot be (or is _extremely_ hard to) answer without access to the application to do further debugging. I suggest you do that debugging yourself, at least to narrow the problem down. Once you have more information than just "entity isn't updated in this use-case" we can be of better help. – Jasper N. Brouwer Dec 05 '14 at 16:07
  • @JasperN.Brouwer Thank you for your time and in explaining to the current answers why they won't work, as well as your time in commenting above. I think you are right; will figure it out and report back. – skrilled Dec 05 '14 at 19:46
  • I suspect that the change(s) made in the form doesn't reach the answer entity in question. Either that or the question gets detached. Like I said, try to narrow it down :) – Jasper N. Brouwer Dec 05 '14 at 22:48
  • I looped through $question->getAnswers() after the form was handled, and all the new values are set. In further testing, I've found that I can edit an existing object if I'm adding a new object or deleting an object from answers at the same time... still can't edit an existing without doing one of these loopholes though. – skrilled Dec 08 '14 at 19:32
  • Okay figured it out via an existing question, posted answer. Thanks again for everyone's help :) – skrilled Dec 08 '14 at 20:32

5 Answers5

4

After searching through Google for hours I ran across a duplicate question that resembles mine.

How to force Doctrine to update array type fields?

I adjusted by setAnswers method as follows to reflect this answer:

 public function setAnswers($answers)
    {
        if(!empty($answers) && $answers === $this->answers) {
            reset($answers);
            $answers[key($answers)] = clone current($answers);
        }
        $this->answers = $answers;
        return $this;
    }

It now saves existing answers fine, no more issues :)

Community
  • 1
  • 1
skrilled
  • 5,350
  • 2
  • 26
  • 48
  • You didn't specified associations and all thought that we speak about `OneToMany` relation. The more elegant way is to call `$uow->propertyChanged($entity, $propertyName, $oldValue, $newValue)` (for example, in listener). – origaminal Dec 09 '14 at 04:09
  • 1
    Even if you prefer cloning this can be done with `by_reference => false` into `AnswerType` options. – origaminal Dec 09 '14 at 15:48
  • That's not the case origaminal. I tried those and it did nothing for me, that would work if I were working with managed entities instead of simple php objects. Because I'm working with a collection of objects instead of a collection of entities, the entity manager only recognizes that a change to the collection happened but not a change to the property of an object in said collection. – skrilled Dec 12 '14 at 22:31
2

Add both addAnswer() and removeanswer() to your Question entity/model.

Set the collection's by_reference option to false

$builder
        ->add('question','textarea')
        ->add('answers', 'collection', array(
            'type'=>new AnswerType(),
            'allow_add'=>true,
            'allow_delete'=>true,
            'label' => false,
            'by_reference' = > false
        ))
    ;
Waaghals
  • 2,029
  • 16
  • 30
1

I think you should be using merge to update the object:

$em->merge($question)
$em->flush();
devilcius
  • 1,764
  • 14
  • 18
  • 3
    The Question entity is found by the EntityManager, thus already managed. Read [the docs](http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-objects.html#merging-entities) on what `merge()` actually does. Also, the question is persisted just fine, it's an answer (in a specific use-case) that isn't. – Jasper N. Brouwer Dec 05 '14 at 16:13
1

Answers won't persist themselves.

Either:

foreach ($question->getAnswers() as $answer) {

    $em->persist($answer);
}

$em->persist($question);
$em->flush();

Or (in your question entity):

/**
 * @ORM\OneToMany(targetEntity="YourBundle\Etc\Entity\Answer",mappedBy="question",cascade={"persist"})
 */

More info.

Richard
  • 4,079
  • 1
  • 14
  • 17
  • 2
    Actually, if you haven't change the default change tracking policy, a managed entity _will_ update itself (or rather the unit-of-work will persist the changes, which are auto-detected with the default change tracking policy). – Jasper N. Brouwer Dec 05 '14 at 16:15
  • In the link I referenced it says: "By default, no operations are cascaded". Unless I'm missing something, that seems pretty cut and dried. The question asks why aren't changes to the answer entities persisted & my answer seems to be backed up by the documentation. – Richard Dec 08 '14 at 00:54
  • 1
    `cascade=persist` will make sure that _new_ entities added to the collection/association are persisted, that indeed won't be done by default. But we're talking about updating an existing entity here :) – Jasper N. Brouwer Dec 08 '14 at 10:17
1

You may want to compare the object graph before and after the handleRequest call.

I advise you to use \Doctrine\Common\Util\Debug::dump();, it will make output shorter.

Another thing to check is the raw request data itself, to see if the submitted data itself is correct: just use the form web debug toolbar tab (or request, depending).

Now why it behaves like that is difficult to answer. The collection type behaves very differently given you configure it with by_reference or not:

http://symfony.com/doc/current/reference/forms/types/collection.html

if you're using the collection form type where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the setter (e.g. setAuthors()) to be called.

and the https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php is definitly playing a big role here. As you can see, ordering is important.

Verify that the submitted data is in the expected order.

Last (or maybe check this first :) ), most of the time collection just works :) Verify twice that your getters and setters don't contain a tricky typo that messes up the whiole thing:

Try manually if needed, by mimicking what would do the form component.

Hope it helps!

Florian Klein
  • 8,692
  • 1
  • 32
  • 42
  • Tried all this, in the process of messing around discovered that I *can* edit existing objects if and only if I'm also adding or deleting another object at the same time. I.e. If i edit the text of an answer, but am also adding a new answer at the same time - it works. Still trying to figure out a means to do it without these loopholes though. – skrilled Dec 08 '14 at 19:38