2

I'm building a form generator using Symfony 2.2 with Doctrine. The base principle is that the user create a new form by filling its name and selecting the widgets he'd likes to have in a select menu.

We can think of WidgetInputText, WidgetSelect, WidgetFile, etc.

Here is an example of my model:

<?php

namespace Ineat\FormGeneratorBundle\Entity\Widget;
use Symfony\Component\Validator\Constraints as Assert;


use Doctrine\ORM\Mapping as ORM;

/**
 * Widget
 *
 * @ORM\Table(name="widget")
 * @ORM\Entity
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap({"widget_text" = "WidgetText", "widget_input_text" = "WidgetInputText", "widget_select" = "WidgetSelect"})
 */
abstract class Widget
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Form", inversedBy="widgets")
     */
    private $form;

    /**
     * @var integer
     *
     * @ORM\OneToOne(targetEntity="Question")
     */
    private $question;

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

    /**
     * Set form
     *
     * @param \Ineat\FormGeneratorBundle\Entity\Form $form
     * @return Widget
     */
    public function setForm(\Ineat\FormGeneratorBundle\Entity\Form $form = null)
    {
        $this->form = $form;

        return $this;
    }

    /**
     * Get form
     *
     * @return \Ineat\FormGeneratorBundle\Entity\Form 
     */
    public function getForm()
    {
        return $this->form;
    }

    /**
     * Set question
     *
     * @param \Ineat\FormGeneratorBundle\Entity\Question $question
     * @return Widget
     */
    public function setQuestion(\Ineat\FormGeneratorBundle\Entity\Question $question = null)
    {
        $this->question = $question;

        return $this;
    }

    /**
     * Get question
     *
     * @return \Ineat\FormGeneratorBundle\Entity\Question 
     */
    public function getQuestion()
    {
        return $this->question;
    }
}

<?php

namespace Ineat\FormGeneratorBundle\Entity\Widget;

use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;

/**
 * Widget
 *
 * @ORM\Entity
 * @ORM\Table(name="widget_text")
 */
class WidgetText extends Widget
{
    /**
     * @var string
     *
     * @ORM\Column(type="text")
     */
    private $text;

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

        return $this;
    }

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

<?php

namespace Ineat\FormGeneratorBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Form
 *
 * @ORM\Table(name="form")
 * @ORM\Entity(repositoryClass="Ineat\FormGeneratorBundle\Entity\FormRepository")
 * @UniqueEntity("name")
 * @UniqueEntity("slug")
 */
class Form
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Widget", mappedBy="form", cascade={"persist"})
     */
    private $widgets;


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

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

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

        return $this;
    }

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

    /**
     * Set slug
     *
     * @param string $slug
     * @return Form
     */
    public function setSlug($slug)
    {
        $this->slug = $slug;

        return $this;
    }

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

    /**
     * Add widgets
     *
     * @param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
     * @return Form
     */
    public function addWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
    {
        $this->widgets[] = $widget;

        return $this;
    }

    /**
     * Remove widgets
     *
     * @param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
     */
    public function removeWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
    {
        $this->widgets->removeElement($widget);
    }

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

    /**
     * Set widgets
     *
     * @param \Doctrine\Common\Collections\Collection $widgets
     */
    public function setWidget(\Doctrine\Common\Collections\Collection $widgets)
    {
        $this->widgets = $widgets;
    }

    public function __set($name, $obj)
    {
        if (is_a($obj, '\Ineat\FormGeneratorBundle\Entity\Widget\Widget')) {
            $this->addWidget($obj);
        }
    }
}

As you can see a form can have multiple widgets attached to him.

I've made an abstract class Widget because all widgets have common fields and are of type Widget and because in the Form entity it seems really really bad to had one collection per Widget type (bad and boring).

This model works, I've unit tested it and I'm able to attach a WidgetText to a form and then retrieve it.

The issue comes when I try to use forms with it.

<?php

namespace Ineat\FormGeneratorBundle\Form;

use Ineat\FormGeneratorBundle\Entity\Widget\WidgetText;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class FormType extends AbstractType
{
    protected $widgets;

    public function __construct(array $widgets = array())
    {
        $this->widgets = $widgets;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text')
            ->add('slug', 'text')
            ->add('WidgetText', 'collection', array(
                'type'         => new WidgetTextType(),
                'allow_add'    => true,
                'attr'         => array('class' => 'widget-text'),
                'by_reference' => false
            ))
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Ineat\FormGeneratorBundle\Entity\Form',
        ));
    }

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

<?php

namespace Ineat\FormGeneratorBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

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

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

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Ineat\FormGeneratorBundle\Entity\Widget\WidgetText',
        ));
    }
}

When I try to display the form I've got the following error:

Neither property "WidgetText" nor method "getWidgetText()" nor method "isWidgetText()" exists in class "Ineat\FormGeneratorBundle\Entity\Form"

It's like Symfony doesn't know that my WidgetText is of type Widget too.

If in the controller (generated by Symfony) I change this line:

$this->createForm(new FormType(), new Form())

To:

$this->createForm(new FormType())

The form displays well but when submitting I've got no data binded.

I'm completly stuck there, from an OOP point of view I think this should work but I'm not sure if Symfony allows me to do what I want.

DevAntoine
  • 1,932
  • 19
  • 24
  • do i understand it right that you just want to have a collection of subforms ( of type WidgetTextType ) in your Ineat\FormGeneratorBundle\Form\FormType which you can add and remove? – Nicolai Fröhlich May 23 '13 at 10:04
  • Have you tried using `Widget` instead of `WidgetText` when adding a field to your form ? – Manse May 23 '13 at 10:05
  • your entity doent has `WidgetText` field, it only has 'widgets'. So try to `->add('widgets', 'collection', array(...)` – Alexey B. May 26 '13 at 05:42
  • This is what I did, and I bind this field to WidgetType containing allmy widgets' collections, but I still have binding issues. – DevAntoine May 27 '13 at 08:01
  • Did you figure it out? Trying to do the same but unsure if it's me doing something wrong or an undocumented limitation of symfony. – aabreu May 12 '22 at 21:03
  • @aabreu sorry but more than 9 years later I can't remember. – DevAntoine Oct 31 '22 at 14:37

1 Answers1

0

As mentioned in the comments of your question, you should change the name of the "WidgetText" field to "widgets". The reason behind that is that the names of the fields should match the accessors in your model (i.e. "name" for (set|get)Name(), "widgets" for (set|get)Widgets() etc.)

If you really want the name of a field to differ from the accessor in the model, you can also use the "property_path" option (which is set to the field's name by default):

$builder->add('WidgetText', ..., array(
    ...
    'property_path' => 'widgets',
));
Bernhard Schussek
  • 4,823
  • 26
  • 33