Search code examples
phpsymfonysonata

Call to a member function removeElement() on a non-object


I'm creating a form to be able to send an email message to a group of students which are linked to a particular course. By default all students for a given course must be selected, but the sender of the message must be able to deselect students to exclude them from receiving the message. Sending to the whole group is no problem. The problem occurs when removing students from the select.

I'm using Sonata Admin's sonata_type_model with a custom query. On the resulting form, if I don't change the select options and submit the form everything works fine. When I remove an item from the list I get an error after submitting the form:

Error: Call to a member function removeElement() on a non-object in /xxx/xxx/xxx/vendor/sonata-project/doctrine-orm-admin-bundle/Model/ModelManager.php line 607

After two days of searching for an answer hopefully someone here can help me in the right direction. Here's some of the code I use:

Admin:

$em = $this->modelManager->getEntityManager('Stnu\EduBundle\Entity\DealItem');
    $query = $em->createQueryBuilder('d')
            ->select('d')
            ->from('StnuEduBundle:DealItem', 'd')
            ->innerJoin('d.deal', 'de')
            ->where('d.course = :course')
            ->andWhere('de.status = :status')
            ->setParameter('course',$course)
            ->setParameter('status','order');

    $defaults = $query->getQuery()->getResult();


    $formMapper
            ->with('Certificaten verzenden cursus \''. $title .'\'', array('description' => 'Begeleidende tekst e-mail'))
                ->add('dealItems', 'sonata_type_model', array(
                    'required' => true,
                    'expanded' => false,
                    'btn_add' => false,
                    'multiple' => true,
                    'label' => 'Verzenden aan',
                    'query' => $query,
                    'property' => 'deal.user',
                    'data' => $defaults,
                    'validation_groups' => false
                ))
                ->add('subject', 'text', array('required' => true, 'label' => 'Onderwerp', 'data' => $subject))
                ->add('body', 'textarea', array('label' => 'Bericht', 'required' => false, 'data' => $body, 'attr' => array('class' => 'tinymce', 'data-theme' => 'fullpage', 'style' => 'height: 350px')));

Controller:

/**
 * Create action
 *
 * @return Response
 *
 * @throws AccessDeniedException If access is not granted
 */
public function createAction()
{

    // the key used to lookup the template
    $templateKey = 'edit';

    if (false === $this->admin->isGranted('CREATE')) {
        throw new AccessDeniedException();
    }

    $object = $this->admin->getNewInstance();

    $this->admin->setSubject($object);


    /** @var $form \Symfony\Component\Form\Form */
    $form = $this->admin->getForm();
    $form->setData($object);


    if ($this->getRestMethod()== 'POST') {

        $object->setDealItems($object->getDealItems());

        $form->submit($this->get('request'));

The error appears right after this point.

Entity:

<?php

namespace Stnu\EduBundle\Entity;

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

/**
 * DocsEmail
 * 
 * @ORM\Entity
 */
class CertificateEmail {

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;


    /**
     * @ORM\ManyToMany(targetEntity="DealItem")
     * @ORM\JoinTable(name="certificateemails_dealitems",
     *      joinColumns={@ORM\JoinColumn(name="certificateEmail_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="dealItem_id", referencedColumnName="id")}
     *      )
     */
    private $dealItems;

    private $subject;

    private $body;

    private $extraEmailTo;

    public function __construct() {
        $this->dealItems = new ArrayCollection();
    }

    /**
     * Add dealItem
     *
     */
    public function addDealItem(\Stnu\EduBundle\Entity\DealItem $dealItem) {

        $this->dealItems->add($dealItem);
        //$this->dealItems[] = $dealItem;
        return $this;
    }

    /**
     * Remove dealItem
     */
    public function removeDealItem(\Stnu\EduBundle\Entity\DealItem $dealItem) {

        foreach ($this->dealItems as $item) {
            if ($dealItem === $item) {
                // manager of Stnu\EduBundle\Entity\DealItem
                $entityManager->remove($dealItem);
            }
        }

    }

    /**
     * Get dealItems
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getDealItems() {

        return $this->dealItems;
    }

    public function setDealItems($dealItems) {
        $this->dealItems = new ArrayCollection();

        if (count($dealItems) > 0) {
            foreach ($dealItems as $dealItem) {
                $this->addDealItem($dealItem);
            }
        }

        return $this;
    }


    /**
     * Set subject
     *
     * @param string $subject
     */
    public function setSubject($subject) {
        $this->subject = $subject;

        return $this;
    }

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

    /**
     * Set body
     *
     * @param string $body
     */
    public function setBody($body) {
        $this->body = $body;

        return $this;
    }

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

    /**
     * Set extraEmailTo
     *
     * @param string $extraEmailTo
     */
    public function setExtraEmailTo($extraEmailTo) {
        $this->extraEmailTo = $extraEmailTo;

        return $this;
    }

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

}

Hopefully someone can help me out!


Solution

  • I believe this is getting closer to an answer to your question. The problem is, you asked ENTIRELY the wrong question. The important stuff is below, under "EDIT - Custom Multiple Select Field".

    Your removeDealItem method is all wrong. Try this:

        public function removeDealItem(\Stnu\EduBundle\Entity\DealItem $dealItem)
        {
            $this->dealItems->removeElement( $dealItem );
    
            return $this;
        }
    

    You don't HAVE an $entityManager to call on here ... and you don't need one. Doctrine will check to see if the entity you want to remove exists, and remove it if it does. You don't need to iterate over the existing elements in your collection, and you certainly don't need to do anything on a database level.

        public function addDealItem(\Stnu\EduBundle\Entity\DealItem $dealItem)
        {
            // Getting fancy - check if the item exists before adding it
            if( !$this->dealItems->contains($dealItem) )
            {
                $this->dealItems->add($dealItem);
            }
            return $this;
        }
    

    Adding an item is just as easy ... we can even get fancy with it and use the Doctrine ArrayCollection::contains() method to check if an element exists before we add it. There's nothing wrong with your addDealItem() method - I just wanted to show you contains() as a more visible illustration of letting the ArrayCollection class do the work for you.

    EDIT - Custom Multiple Select Field

    OK - after reading your comment about not necessarily needing to persist the data, I thought I'd offer this simplified example of how to create a custom multiple select box. Please understand that this is a 'bare basics' example - but it SHOULD lead you in the right direction. Obviously, without knowing the structure of your DealItem Entity I'm only GUESSING about the specific fields you need to access in order get the data you need to send your email.

    So - in your Controller - first we get the data:

    $query = $em->createQueryBuilder('d')
                ->select('d')
                ->from('StnuEduBundle:DealItem', 'd')
                ->innerJoin('d.deal', 'de')
                ->where('d.course = :course')
                ->andWhere('de.status = :status')
                ->setParameter('course',$course)
                ->setParameter('status','order');
    
    $defaults = $query->getQuery()->getResult();
    
    $choices = array();
    
    foreach( $defaults as $dealItem )
    {
        $choices[ $dealItem->getEmailAddress() ] = $dealItem->getStudentName();
    }
    

    Now we need an object to receive the data. What I've gathered from your comments is that you don't want to persist the data, and that you only created an Entity for your CertificateEmail object so that you could build a form. Bad idea. You don't need an entity - so don't build one in the first place. To prove a point, I'll do it with a stdClass object:

    $certificateEmail = new \stdClass();
    
    $certificateEmail->dealItems = array();
    $certificateEmail->subject   = '';
    $certificateEmail->body      = '';
    

    Then we build the form:

    $form = $this->createFormBuilder( $certificateEmail )
                 ->add( 'dealItems', 'choice', array(
                             'choices'  => $choices,
                             'multiple' => true,
                             'required' => true,
                             'label'    => 'Verzenden aan' ) )
                 ->add( 'subject', 'text', array( 'required' => true, 'label' => 'Onderwerp' ) )
                 ->add( 'body', 'textarea', array( 'required' => false, 'label' => 'Bericht' ) )
                 ->getForm();
    

    Finally, throw it at a template:

    return $this->render( 'template.html.twig', array( 'form' => $form->createView() ) );
    

    And, hopefully, you can take it from there :)