So basically, creating a from in Symfony is simple overall, all you have to do is define the entity, let’s assume the following Company entity, wich has multiple relationships with the CompanySize, Country and Employee entities :

// App/Entity/Company.php

<?php

namespace App\Entity;

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

/**
 * @ORM\Entity(repositoryClass="App\Repository\CompanyRepository")
 */
class Company
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="text", length=100)
     */
    protected $name;

    /**
     * @ORM\Column(type="text", length=200)
     */
    protected $website;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\CompanySize", inversedBy="companies")
     * @ORM\JoinColumn(name="size_id", referencedColumnName="id")
     */
    protected $size;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Country", inversedBy="companies")
     * @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=true)
     */
    protected $country;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Employee", mappedBy="company")
     */
    protected $employees;

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

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

    /**
     * @param mixed $id
     */
    public function setId($id): void {
        $this->id = $id;
    }

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

    /**
     * @param mixed $name
     */
    public function setName($name): void {
        $this->name = $name;
    }

    /**
     * @return mixed
     */
    public function getWebsite() {
        return $this->website;
    }

    /**
     * @param mixed $website
     */
    public function setWebsite($website): void {
        $this->website = $website;
    }

    public function getCountry()
    {
        return $this->country;
    }

    public function setCountry(Country $country)
    {
        $this->country = $country;
    }

    public function getSize()
    {
        return $this->size;
    }

    public function setSize(CompanySize $size)
    {
        $this->size = $size;
    }

    public function getEmployees() : ArrayCollection
    {
        return $this->employees;
    }
}

The next step would be adding a Form :

// App/Entity/CompanyType.php

<?php
/**
 * Title
 *
 * Description
 *
 * @package        Form guide
 * @subpackage     xxx Bundle
 * @category
 * @author         Achraf Soltani <contact@achrafsoltani.com>
 * @link           http://www.achrafsoltani.com
 * @file           CompanyType.php
 */

namespace App\Form;

use App\Entity\Company;
use App\Entity\CompanySize;
use App\Entity\Country;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CompanyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array(
                'label' =>"Name",
                'translation_domain' => 'company'
            ))
            ->add('website', TextType::class, array(
                'label' =>"Website",
                'translation_domain' => 'company'
            ))
            ->add('size', EntityType::class,
                array(
                    'label' =>"Size",
                    'class' => CompanySize::class,
                    'choice_label' => 'label',
                    'choice_value' => 'id',
                    'translation_domain' => 'company'
                ))
            ->add('country', EntityType::class,
                array(
                    'label' =>"Country",
                    'class' => Country::class,
                    'choice_label' => 'name',
                    'choice_value' => 'id',
                    'translation_domain' => 'company'
                ))
            ->add('save', SubmitType::class, array(
                'label' =>"Save",
                'translation_domain' => 'company'
            ))
        ;
    }

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

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Company::class,
        ));
    }
}

Notice that I have added the ‘translation_domain’ param in the array of options, that would use the file in App/translations/company.{LANG}.yml where {LANG} is the equivalent of the user’s locale.

I have also specified the value/id fields for the related entities which will be displayed in the HTML select/options (choice_label and choice_value)

Next, the Controller should be used to build the Form, validate it’s submission and render the view, the following is a simple implementation of the controller :

<?php

namespace App\Controller;

use App\Entity\Company;
use App\Form\CompanyType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CompanyController extends Controller
{
    /**
     * @Route("{_locale}/company/new", name="_companies_new")
     */
    public function newAction(Request $request)
    {
        $company = new Company();

        $form = $this->createForm(CompanyType::class, $company);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // $form->getData() holds the submitted values
            // but, the original `$company` variable has also been updated
            $company = $form->getData();

            // ... perform some action, such as saving the task to the database
            // for example, if Task is a Doctrine entity, save it!
            $em = $this->getDoctrine()->getManager();
            $em->persist($company);
            $em->flush();

            return $this->redirectToRoute('_companies_listing');
        }

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

Now, we should render the From, in a twig template, then discover how we can theme it up

{% extends 'App/layout.html.twig' %}
{% trans_default_domain "company" %}
{% form_theme form 'App/Form/fields.html.twig' %}

{% block content %}

 {{ form_start(form) }}
     {{ form_widget(form.name, {'attr': {'class': 'form-control', 'placeholder': 'Name'}}) }}
     <br/>
     {{ form_widget(form.website, {'attr': {'class': 'form-control', 'placeholder': 'Website'}}) }}
     <br/>
     {{ form_widget(form.size, {'attr': {'class': 'form-control'}, 'placeholder': 'Size', 'required': true}) }}
     <br/>
     {{ form_widget(form.country, {'attr': {'class': 'form-control'}, 'placeholder': 'Country', 'required': true}) }}
     <br/>
     {{ form_widget(form.save, {'attr': {'class': 'btn btn-default'}}) }}
     <br/>
 {{ form_end(form) }}
{% endblock %}

Notice that here again, I have explicitly specified the ‘trans_default_demain’ for the form’s localization, and the ‘form_theme’ param which defines the theme’s location. As you would guess, now we will have to customize the Form inputs rendering, So as I have specified in the ‘from_theme’ param :

{% block form_widget_simple %}
    {% set type = type|default('text') %}
    <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{% endblock form_widget_simple %}

<!-- Both integer widget and text wigets extend the form_widget_simple --> 

{% block integer_widget %}
    <div class="integer_widget">
        {% set type = type|default('number') %}
        {{ block('form_widget_simple') }}
    </div>
{% endblock %}

{% block text_widget %}
        {% set type = type|default('text') %}
        {{ block('form_widget_simple') }}
{% endblock %}

<!-- Choice wiget implementation if you wish to override anything in here, like adding container divs or so --> 

{% block choice_widget %}
    {% if expanded %}
        {{ block('choice_widget_expanded') }}
    {% else %}
        {{ block('choice_widget_collapsed') }}
    {% endif %}
{% endblock choice_widget %}

{% block choice_widget_expanded %}
        {% for child in form %}
            {{ form_widget(child) }}
        {% endfor %}
{% endblock choice_widget_expanded %}

{% block choice_widget_collapsed %}
    {% if required and placeholder is none and not placeholder_in_choices and not multiple and (attr.size is not defined or attr.size <= 1) %}
        {% set required = false %}
    {% endif %}
    <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
        {% if placeholder is not none %}
            <option value=""{% if required and value is empty %} selected="selected"{% endif %}>{{ placeholder != '' ? (translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain)) }}</option>
        {% endif %}
        {% if preferred_choices|length > 0 %}
            {% set options = preferred_choices %}
            {{ block('choice_widget_options') }}
            {% if choices|length > 0 and separator is not none %}
                <option disabled="disabled">{{ separator }}</option>
            {% endif %}
        {% endif %}
        {% set options = choices %}
        {{ block('choice_widget_options') }}
    </select>
{% endblock choice_widget_collapsed %}

{% block choice_widget_options %}
    {% for group_label, choice in options %}
        {% if choice is iterable %}
            <optgroup label="{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
                {% set options = choice %}
                {{ block('choice_widget_options') }}
            </optgroup>
        {% else %}
            <option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
        {% endif %}
    {% endfor %}
{% endblock choice_widget_options %}

As you can see, if you navigate to http://localhost:8000/fr/company/new for example, the rendered form now looks exactly as we want, using the css classes we specified in the template and using the related entities’ data (Size and Country)