Si vous ne l’avez pas fait, vous êtes invités à lire les précédents articles de cette série avant d’entamer celui-ci :

Petit rappel : Le sujet de l’internationalisation ne sera pas abordé dans les exemples. Mais il est recommandé d’utiliser des clés de traduction pour les labels des widgets ainsi que pour les messages qui seront affichés à l’utilisateur.

Thèmes de formulaires

Une fois que vous avez réalisé des classes de formulaire, Symfony se charge du rendu de celles-ci. Ce rendu passe par des templates Twig spécifiques qui contiennent des blocs.

Nous allons aborder l’utilisation de ces templates de rendu, leur surcharge ainsi que les mêmes problématiques mais appliquées aux blocs.

Nouveaux formulaires

Afin d’obtenir différents cas intéressants, nous allons modifier notre entité Article. Une entité Reference va lui être lié et chaque article devra contenir exactement deux références.

Nouvelle entité Reference et son formulaire

<?php

/**
 * @ORM\Entity
 */
class Reference
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     *
     * @ORM\Column
     *
     * @Assert\NotNull()
     */
    protected $name;

    /**
     * @var string
     *
     * @ORM\Column
     *
     * @Assert\Url()
     */
    protected $link;

    /**
     * @var Article
     *
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Article", inversedBy="references")
     *
     * @Assert\NotNull
     */
    protected $article;
    
    // [...]
}
<?php

class ReferenceType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('link')
        ;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefaults([
                'data_class' => Reference::class,
            ])
        ;
    }
}

Ancienne entité Article et son formulaire

<?php

/**
 * @ORM\Entity
 */
class Article
{
     // [...]

    /**
     * @var Reference[]
     *
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Reference", mappedBy="article", cascade={"persist", "remove"})
     *
     * @Assert\Valid
     */
    protected $references;

    public function __construct()
    {
        $this->references = new ArrayCollection();
    }
    
    // [...]

    /**
     * @return Reference[]
     */
    public function getReferences()
    {
        return $this->references;
    }

    /**
     * @param Reference $reference
     *
     * @return $this
     */
    public function addReference(Reference $reference)
    {
        $reference->setArticle($this);
        $this->references->add($reference);

        return $this;
    }

    /**
     * @param Reference $reference
     *
     * @return $this
     */
    public function removeReference(Reference $reference)
    {
        if ($this->references->contains($reference)) {
            $this->references->removeElement($reference);
        }

        return $this;
    }
}
<?php

class ArticleType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('content', TextareaType::class)
            ->add('references', CollectionType::class, [
                'entry_type' => ReferenceType::class,
                'entry_options' => [
                    'label' => false,
                ],
                'allow_add' => true,
                'allow_delete' => true,
            ])
        ;
    }
    
    // [...]
}

On modifie un peu le contrôleur de création, car on souhaite que notre article contienne exactement deux références.

<?php

/**
 * @Route("/article")
 */
class ArticleController extends Controller
{
    // [...]
    
    /**
     * @Route("/new", name="article_new", methods={"GET", "POST"})
     */
    public function newAction(Request $request)
    {
        $article = (new Article())
            ->addReference(new Reference())
            ->addReference(new Reference())
        ;

        $form = $this->createForm(ArticleType::class, $article)
            ->add('submit', SubmitType::class, ['label' => 'Create', 'attr' => ['class' => 'btn-primary pull-right']])
        ;
    
        // [...]
    }
    
    // [...]
}

Surcharge des templates

La plupart du temps, lors de la réalisation d’un site internet, une charte graphique est mise en place. Elle définit la mise en page des différents éléments qui vont composer le site internet à travers un guide de styles, exemple. Ainsi les formulaires du site auront le même aspect.

En tant que développeur cela nous simplifie la vie, car une fois que les éléments auront la bonne mise en page HTML / CSS, on peut les réutiliser à volonté.

Surcharge globale

Dans Symfony on peut obtenir ce comportement en créant un template de rendu qui sera utilisé par tous les formulaires. Pour cela, il est recommandé d’étendre un template déjà existant, bootstrap_3_layout.html.twig par exemple, et d’y ajouter les besoins spécifiques pour atteindre le guide de style.

Exemple ci-dessous, form.html.twig, dans le parent une ligne de formulaire est arrangée ainsi : label, widget puis errors, l’exemple change l’ordre pour label, errors puis widget :

{% extends 'bootstrap_3_layout.html.twig' %}

{% block form_row -%}
    <div class="form-group{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
        {{- form_label(form) -}}
        {{- form_errors(form) -}}
        {{- form_widget(form) -}}
    </div>
{%- endblock form_row %}

Reste à déclarer qu’il faut utiliser ce template pour tous les formulaires du site, histoire de garder des templates très simples ({{ form(form) }}), dans config.yml :

# Twig Configuration
twig:
    form_themes:
        - "@App/Form/form.html.twig"

Et le tour est joué !

Surcharge spécifique

Admettons que vous ayez un ou deux formulaires qui sortent du guide de style, vous pouvez également faire un template de rendu qui prend en compte les spécifications, en repartant de zéro, en étendant bootstrap_3_layout.html.twig ou en étendant votre template précédemment créé.

Ensuite, dans le template du formulaire, vous devez appliquer le nouveau template de rendu :

{% form_theme myform '@App/Article/form_specific.html.twig' %}

{{ form(myform) }}

Et voilà, pas besoin de jongler avec les fonctions form_label() et form_widget() dans le template où vous souhaitez seulement afficher le formulaire.

les blocs de rendu

Un template de rendu de formulaire est composé de blocs. On en trouve pour chaque élément du formulaire. Ils s’appellent entre eux, depuis l’élément qui englobe tout jusqu’à l’élément le plus spécifique.

Variables

Dans les blocs, dur de savoir à quoi l’on a le droit en terme de variables. Pour ça, le profiler va vous aider, une section est dédiée aux formulaires de la page. Vous pouvez déplier la partie correspondant à celui que vous souhaitez étudier et sélectionner un des éléments du formulaire. Vous trouverez alors un tableau View Variables qui contient les variables de la vue qui seront disponible dans le bloc.

Profiler
Variables de la vue dans le profiler

Mais avec l’inclusion des blocs, ça n’est pas forcément clair dans tous les cas. Vous pouvez alors utiliser ce que propose Twig : la variable _context. Vous pouvez insérer {{ dump(_context|keys) }} qui listera toutes les variables auxquelles vous avez accès dans le bloc.

Nom

Sur le même principe que les CSS avec leurs classes et leur ID, vous pouvez nommer les blocs pour appliquer un thème sur une catégorie d’élément ou bien sur un élément spécifique d’un formulaire spécifique.

Les blocs, les plus utilisé, sont nommés avec le nom du widget et suffixé par :

  • _row, qui représente une ligne de formulaire qui va contenir _label, _widget, _errors
  • _label, qui va afficher le nom du champ
  • _widget, qui va afficher l’élément HTML comme un champ input par exemple
  • _errors, si des erreurs sont associé au champ, elle seront affiché par ce bloc

Par exemple, pour changer le markup HTML de tous vos champs textarea, vous pouvez utiliser le bloc textarea_widget :

{%- block textarea_widget -%}
    All my textarea
    <textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
{%- endblock textarea_widget -%}

Problème, comment retrouver les noms de bloc qui me permettrait de changer le thème d’un widget spécifique ? Dans la vue il existe une variable block_prefixes qui est un tableau qui contient les différents noms qui vont être utilisés pour les noms de bloc, du plus générique au plus précis.

Exemple pour le champ texte de l’article :

array:4 [▼
  0 => "form"
  1 => "text"
  2 => "textarea"
  3 => "_article_content"
]

On peut voir que le dernier nom est formé à partir du nom du formulaire et du nom du champs. Si vous souhaitez faire un cas particulier pour ce widget spécifique, vous pouvez utiliser le nom _article_content_widget :

{%- block _article_content_widget -%}
    My specific article/content textarea
    <textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
{%- endblock _article_content_widget -%}

Pour les widgets Collection et Choice qui ajoutent des enfants dont nous n’avons pas une maitrise directe, la difficulté est un poil plus élevée.

Profiler et entry

Comme on peut le constater en sélectionnant un élément d’une collection, les blocs utilisent un nouveau suffixe : _entry.

Dans notre cas, admettons que l’on souhaite faire une présentation particulière pour les références quand elles sont incluses dans le formulaire des articles, on pourrait le faire soit avec _article_references_entry_widget, qui serait appliqué à toutes les lignes de la collection Reference, soit avec _article_references_entry_name_widget. Cela marche aussi avec _row à la place de _widget.

Avec tout ça, attention toutefois à ne pas avoir une complexité élevée entre surcharge de template et de bloc. Il faut rester le plus intuitif possible pour les autres.

Trucs et astuces

Si vous souhaitez ajouter une classe automatiquement dans les attributs d’un widget, attention à ne pas écraser celles qui ont pu être définies avant votre template :

{% set attr = attr|merge({'class': ('row ' ~ attr.class|default(''))|trim }) %}

Dans un formulaire qui a besoin de paramètres, il est pratique de les définir, avec leur type et leurs valeurs par défaut :

<?php

class ArticleType extends AbstractType
{
    // [...]

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired([
            'step',
            'vertical',
        ]);
        $resolver->setDefaults([
            'start' => 0,
            'end' => 100,
        ]);
    }
}

This is this end

Merci d’avoir suivi ces quelques articles, j’espère qu’ils vous auront inspiré et aidé.

La série