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.