Search code examples
symfonydoctrine-ormmany-to-many

Doctrine Manytomany with additional field


I searchin everywhere something can help me to resolve this issue and did not find.

I have relation:

TABLE A --- one-to-many --- TABLE AB --- many-to-one --- TABLE B

ERROR what i get while try to call newAction method:

Expected value of type "Doctrine\Common\Collections\Collection|array" for association field "AppBundle\Entity\Line#$buttons", got "AppBundle\Entity\Button" instead.

I think I have tried everything and get still same error.

Line entity

    namespace AppBundle\Entity;

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

    /**
     * Line
     *
     * @ORM\Table(name="lines")
     * @ORM\Entity(repositoryClass="AppBundle\Repository\LineRepository")
     */
    class Line
    {
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;

        /**
         * @var string
         *
         * @ORM\Column(name="line_name", type="string", length=255)
         */
        private $lineName;

        /**
         * @ORM\OneToMany(targetEntity="AppBundle\Entity\ButtonsState", mappedBy="line", cascade={"all"})
         */
        private $buttons;

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

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

        /**
         * Set lineName
         *
         * @param string $lineName
         *
         * @return Line
         */
        public function setLineName($lineName)
        {
            $this->lineName = $lineName;

            return $this;
        }

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

        /**
         * Get buttons
         *
         * @return int
         */
        public function getButtons()
        {
            return $this->buttons;
        }

        public function addButton(Button $button)
        {
            if ($this->buttons->contains($button)) {
                return;
            }

            $this->buttons->add($button);
        }

    }

Button entity

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Button
 *
 * @ORM\Table(name="buttons")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ButtonRepository")
 */
class Button
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="button_name", type="string", length=255)
     */
    private $buttonName;

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\ButtonsState", mappedBy="button", cascade={"all"})
     */
    private $lines;


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

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

    /**
     * Set buttonName
     *
     * @param string $buttonName
     *
     * @return Button
     */
    public function setButtonName($buttonName)
    {
        $this->buttonName = $buttonName;
    }

}

ButtonsState entity

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * ButtonsState
 *
 * @ORM\Table(name="lines_buttons")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ButtonStateRepository")
 */
class ButtonsState
{
    /**
     * @ORM\Id()
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Line", inversedBy="buttons", cascade={"PERSIST"})
     * @ORM\JoinColumn(name="line_id", referencedColumnName="id")
     */
    private $line;

    /**
     * @ORM\Id()
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Button", inversedBy="line", cascade={"PERSIST"})
     * @ORM\JoinColumn(name="button_id", referencedColumnName="id")
     */
    private $button;

    /**
     * @var int
     *
     * @ORM\Column(name="state", type="integer")
     */
    private $state;

    /**
     * @return int
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * @param int $state
     */
    public function setState($state)
    {
        $this->state = $state;
    }  

}

LineController

/**
     * Creates a new line entity.
     *
     * @Route("/new", name="line_new")
     * @Method({"GET", "POST"})
     */
    public function newAction(Request $request)
    {
        $line = new Line();
        $form = $this->createForm('AppBundle\Form\LineType', $line);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            $em = $this->getDoctrine()->getManager();

            foreach ($line->getButtons() as $button) {
                $line->addButton($button);
            }

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

            return $this->redirectToRoute('line_index', array('id' => $line->getId()));
        }

        return $this->render('line/new.html.twig', array(
            'line' => $line,
            'form' => $form->createView(),
        ));
    }

LineType

namespace AppBundle\Form;

use AppBundle\Entity\Button;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class LineType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('lineName', null, array(
                'label' => 'Line name',
            ))
            ->add('buttons', EntityType::class, array(
                'class' => Button::class,
                'choice_label' => 'buttonName',
                'multiple' => true,
                'expanded' => true,
            ));
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Line'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_line';
    }


}

Solution

  • On line 51 of your Controller you are calling the method addButtons.

    foreach ($line->getButtons() as $button) {
        $line->addButtons($button);
    }
    

    Try changing it to addButton since this is the method defined in the entity.

    If this does not helps please provide the corresponding stack trace.

    Edit:

    Having a look at it again I've seen what the problem is.

    You're treating the ButtonsState Entity as the table between an m to m relationship what it isn't since there's an extra property on it.

    In the form type your'e saying that behind the buttons property of the Line entity there are Buttons. This is wrong. Behind this property there are ButtonsState entities.

    So change your code as follows:

    Form type:

    // ...
    ->add('buttonsToAdd', EntityType::class, array(
        'class' => Button::class,
        'choice_label' => 'buttonName',
        'multiple' => true,
        'expanded' => true,
        'mapped' => false,
    ))
    // ...
    

    Controller:

    // ...
    $em->persist($line);
    foreach ($form->get('buttonsToAdd')->getData() as $button) {
        $state = new \UserBundle\Entity\ButtonsState();
        $state->setLine($line);
        $state->setButton($button);
        $state->setState(0);
    
        $line->addButton($state);
    }
    
    $em->flush();
    // ...
    

    Like this you have an extra (unmapped) property whit the buttons on the form that you can convert to ButtonsState instances in the controller.

    One more hint: use the command bin/console doctrine:generate:entities AppBundle:ButtonsState to generate the getters and setters in your entities since they are wronc/incomplete at some places.