Assume that we have one-to-many relationship between Country and League entities. We can easily create dedicated form types for each entities to handle data input into database however, we may sometimes want to handle both entities in a single form type so that we can create Country and League in one go within a single form type. This example does exactly that.


Country entity


namespace Football\FrontendBundle\Entity;

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

/**
* @ORM\Table(name="country")
* @UniqueEntity(fields="code", message="Code is already taken.")
* @UniqueEntity(fields="name", message="Name is already taken.")
*/
class Country
{
/**
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @ORM\Column(type="string", length=2, unique=true)
* @Assert\NotBlank(message="Code is required.")
*/
protected $code;

/**
* @ORM\Column(name="name", type="string", length=100, unique=true)
* @Assert\NotBlank(message="Name is required.")
*/
protected $name;

/**
* @ORM\OneToMany(
* targetEntity="League",
* mappedBy="country",
* cascade={"persist", "remove"}
* )
*/
protected $league;
}

League entity


If we had add @Assert\NotBlank(message="Country is required.") to $country property, this would always invalidate our form submission because we're creating a Country at this stage so it doesn't exist yet.


namespace Football\FrontendBundle\Entity;

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

/**
* @ORM\Table(
* name="league",
* uniqueConstraints={@ORM\UniqueConstraint(columns={"name", "country_id"})}
* )
* @UniqueEntity(
* fields={"name","country"},
* message="League for given country already exists in database."
* )
*/
class League
{
/**
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @ORM\Column(name="name", type="string", length=100)
* @Assert\NotBlank(message="Name is required.")
*/
protected $name;

/**
* @ORM\ManyToOne(
* targetEntity="Country",
* inversedBy="league"
* )
* @ORM\JoinColumn(
* name="country_id",
* referencedColumnName="id",
* onDelete="CASCADE",
* nullable=false
* )
*/
protected $country;
}

Country FormType


namespace Football\FrontendBundle\Form\Type;

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

class CountryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('code', 'text', ['required' => false, 'error_bubbling' => true])
->add('name', 'text', ['required' => false, 'error_bubbling' => true]);
}

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

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\Country']
);
}
}

League FormType


namespace Football\FrontendBundle\Form\Type;

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

class LeagueType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('name', 'text', ['required' => false, 'error_bubbling' => true]);
}

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

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\League']
);
}
}


CountryLeague FormType


namespace Football\FrontendBundle\Form\Type;

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

class CountryLeagueType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('country', new CountryType())
->add('league', new LeagueType());
}

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

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => null]
);
}
}

Controller


/**
* @Route("/countryleague")
*/
class CountyLeagueController extends Controller
{
const ROUTER_PREFIX = 'football_frontend_countyleague_';

/**
* Create page.
*
* @Route("/create")
* @Method({"GET"})
*
* @return Response
*/
public function createAction()
{
$form = $this->getForm(
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);

return $this->getFormView('create', ['form' => $form->createView()]);
}

/**
* Creates processing.
*
* @param Request $request
*
* @Route("/create")
* @Method({"POST"})
*
* @return RedirectResponse|Response
* @throws CountryLeagueException
*/
public function createProcessAction(Request $request)
{
if ($request->getMethod() != 'POST') {
throw new CountryLeagueException('CountryLeague create: only POST method is allowed.');
}

$form = $this->getForm(
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);
$form->handleRequest($request);

if (!$form->isSubmitted()) {
throw new CountryLeagueException('CountryLeague create: form is not submitted.');
}

if ($form->isValid() !== true) {
return $this->getFormView('create', ['form' => $form->createView()]);
}

try {
$data = $form->getData();

$country = new Country();
$country->setCode($data['country']->getCode());
$country->setName($data['country']->getName());

$league = new League();
$league->setName($data['league']->getName());
$league->setCountry($country);

$em = $this->getDoctrine()->getManager();
$em->persist($country);
$em->persist($league);
$em->flush();
} catch (DBALException $e) {
$message = sprintf('DBALException [%s]: %s', $e->getCode(), $e->getMessage());
} catch (ORMException $e) {
$message = sprintf('ORMException [%s]: %s', $e->getCode(), $e->getMessage());
} catch (Exception $e) {
$message = sprintf('Exception [%s]: %s', $e->getCode(), $e->getMessage());
}

if (isset($message)) {
throw new CountryLeagueException($message);
}

return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'create'));
}

/**
* Creates form object.
*
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm($method, $action)
{
return $this->createForm(
new CountryLeagueType(),
null,
[
'method' => $method,
'action' => $action
]
);
}

/**
* Creates webform.
*
* @param string $template
* @param array $parameters
*
* @return Response
*/
private function getFormView($template, array $parameters = [])
{
return $this->render(
sprintf('FootballFrontendBundle:CountryLeague:%s.html.twig', $template),
$parameters
);
}
}

Twig template


{% extends 'FootballFrontendBundle:CountryLeague:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
COUNTRY LEAGUE - Create
<hr />
{{ form_start(form) }}
{% if form_errors(form) != '' %}

{{ form_errors(form) }}

{% endif %}

<p>COUNTRY CODE: {{ form_widget(form.country.code) }}</p>
<p>COUNTRY NAME: {{ form_widget(form.country.name) }}</p>
<p>LEAGUE NAME: {{ form_widget(form.league.name) }}</p>

{{ form_widget(form._token) }}

<p><button name="button">Submit</button></p>
{{ form_end(form, {'render_rest': false}) }}
{% endspaceless %}
{% endblock %}