Quelles sont vos meilleures pratiques Symfony ?

Publié le 22/12/2019 • Actualisé le 24/12/2021

Dans cet article, nous allons passer en revue toutes les "meilleures pratiques" Symfony présentes dans la documentation officielle. Pour chacune, je dirai si je suis d'accord ou pas et pourquoi. C'est parti ! 😎


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

» Publié dans "Une semaine Symfonique 679" (du 30 décembre 2019 au 5 janvier 2020).

[Maj 24/12/2021] : Mise à jour pour Symfony 5.4

Introduction

Les "meilleurs pratiques" sont très importantes. Ce sont des règles que l'on peut suivre (ou pas) permettant d'avoir un code cohérent et homogène. C'est crucial quand on travaille en équipe (et c'est pratiquement toujours le cas) dans un contexte professionnel. Ce n'est pas le cas sur ce blog même si certaines personnes m'aident pour l'orthographe et la grammaire. Regardons chaque règle de la documentation officielle. Si je suis entièrement d'accord , je mettrai un symbole . Si je la trouve plutôt bonne mais que je ne l'applique pas dans tous les cas je mettrai un simple ️ et finalement si je ne suis pas d'accord du tout ou plutôt en désaccord, je mettrai une croix . J'ajouterai à chaque fois quelques commentaires pour justifier mes choix.

Qu'est-ce qui fait d'une pratique une "meilleure pratique"?

Rien ! Une pratique est considérée comme telle seulement si vous pensez qu'elle l'est. Rappelez-vous que vous ne devriez pas suivre ces règles aveuglément. Soyez toujours pragmatiques. Testez, essayez, construisez votre propre expérience et élaborez votre propre ensemble de meilleurs pratiques. Pour chaque section, j'ajouterai un exemple de code extrait de ce site web.

Création d'un projet

Meilleure pratique Mon opinion Commentaires
Utilisez le binaire Symfony pour créer des applications (1.1) Quand on utilise le binaire Symfony, si une version est plus récente, il vous demandera s'il doit se mettre à jour. Cela veut dire qu'il créera toujours un projet correspondant à la dernière version stable et aussi qu'il respectera toutes les meilleures pratiques, surtout celles concernant la structure des répertoires (voir 1.2).
Utilisez la structure de répertoire par défaut (1.2) Évitez d'utiliser des choses exotiques (pour un projet DDD, on ne pas suivre cette règle).
## —— Symfony binary 💻 ————————————————————————————————————————————————————————
cert-install: ## Install the local HTTPS certificates
	@$(SYMFONY_BIN) server:ca:install

serve: ## Serve the application with HTTPS support (add "--no-tls" to disable https)
	@$(SYMFONY_BIN) serve --daemon --port=$(HTTP_PORT)

unserve: ## Stop the webserver
	@$(SYMFONY_BIN) server:stop

1.1 » Le binaire Symfony peut aussi servir les applications localement avec support HTTPS (vous trouverez mon Makefile complet ici).

Configuration

Meilleure pratique Mon opinion Commentaires
Utilisez des variables d'environnement pour la configuration de l'infrastructure (2.1) Les fichiers .env sont faciles à utiliser. Je préfère en avoir un unique pour garder l'ensemble des paramètres à un seul endroit.
Utiliser les secrets pour les informations confidentielles (2.2) Quand j'ai écrit cet article, cette "meilleure pratique" n'était pas encore proposée. Le composant secret a été introduit dans Symfony 4.4 et c'est maintenant la bonne manière de gérer vos mots de passe et clés d'API.
Utilisez les paramètres pour la configuration de l'application (2.3) -
Utilisez un préfixe court pour vos noms de paramètres (2.4) C'est quelque chose que j'utilise en partie à l'heure actuelle mais je n'ai jamais eu de conflit entre un bundle tiers et l'application.
Utilisez des constantes pour des paramètres qui changent peu (2.5) Le nombre d'éléments à afficher par la pagination est un bon exemple. Jetez un coup d'œil au code extrait de mon contrôleur dédié à l'affichage des snippets.
#[Route(path: '/{_locale}/blog', name: 'blog_', requirements: ['_locale' => '%locales_requirements%'])]
final class BlogController extends AbstractController
{
    // Snippets
    // - templates/blog/posts/_21.html.twig (L23->L23+2)
    // - templates/snippet/code/_35.html.twig (L23->L23+2)
    // - templates/blog/posts/_64.html.twig (L23->L23+15)
    use Post\Post13Trait;
    use Post\Post26Trait;
    use Post\Post51Trait;
    use Post\Post59Trait;
    use Post\Post90Trait;
    use Post\Post138Trait;
    use Post\Post165Trait;
    use Post\Post216Trait;
    use Post\Post254Trait;

2.5 » J'affiche dix éléments par page sur la liste principale des article.

Logique métier

Meilleure pratique Mon opinion Commentaires
Ne créez pas de bundle pour organiser votre logique métier (3.1) Je ne suis pas d'accord avec cette règle. Pour l'une des applications sur laquelle je travaille, nous avons eu à livrer un gros projet concernant un domaine fonctionnel. Nous avons décidé de mettre tout ce qui concerne ce projet dans un nouveau bundle. Cela nous a évité des casse-têtes de merge puisque toutes les nouvelles ressources concernant ce projet étaient dans un nouveau répertoire.
Utilisez l'autowiring pour automatiser la configuration des services (3.2) L'autowiring est fantastique. J'étais sceptique au début. Puis, quand on l'utilise, vous avez ces moments où vous vous dîtes, "Comment est-ce que je faisais avant ?". C'était tellement rébarbatif de déclarer chaque service individuellement. Le fichier services.yml devenait rapidement énorme pour aucun bénéfice dans la majeure partie des cas.
Les services devraient être privés si possible (3.3) Dans ce projet, j'ai uniquement un service public car le persister Elastica oblige les services fournisseurs de données à l'être. Jetez un coup d'œil à la classe ArticleProvider du tutoriel Elasticsearch.
Utilisez le YAML pour vos propres services (3.4) Même si la tendance actuelle est de plus en plus d'utiliser PHP pour les configurations (probablement pour Symfony 6). Utiliser le YAML pour déclarer des choses spécifiques dans le fichier services.yml reste très pratique.
Utilisez les attributs ou les annotations pour définir le mapping des entités Doctrine (3.5) Je n'ai jamais aimé avoir un schéma YAML séparé des entités Doctrine. C'est mieux d'avoir les propriétés et le schéma relatif dans le même fichier pour pouvoir faire des modifications facilement.
/**
 * @see https://app.abstractapi.com/api/ip-geolocation/documentation
 */
class AbstractApi
{
    public function __construct(
        private HttpClientInterface $abstractApiClient,
        private string $abstractApiKey)
    {
    }

3.2 » Un service utilisant des paramètres nommés.

Contrôleurs

Meilleure pratique Mon opinion Commentaires
Faites vos contrôleurs étendre le controlleur de base "AbstractController" (4.1) Le "contrôleur abstrait" fournit les fonctionnalités les plus communes dont on a besoin : render(), redirect()... mais empêche la récupération de services à la volée comme on pouvait le faire auparavant.
Utilisez les attributs ou les annotations pour configurer le routage, le cache et la sécurité (4.2) Il m'est arrivé de travailler sur de gros projets où toutes les informations de routage étaient dans un même fichier YAML. Le fichier grossit de manière chaotique et ça devient vite un enfer. Aussi bien pour s'y retrouver que pour les merges ! Quand on utilise une annotation de routage, on a le nom de la route, le chemin, mais aussi le code relatif à la même place. C'est beaucoup plus facile pour développer.
N'utilisez pas les annotations pour configurer le template du contrôleur (4.3) J'ai testé l'annotation @template sur quelques projets. Ça fonctionne mais parfois on a à chercher le nom de template à utiliser. Quand on utilise explicitement la fonction render() avec PHPStorm, on peut cliquer sur le nom de template pour l'ouvrir dans l'éditeur ! Évitez la magie. De plus un contrôleur doit toujours retourner un objet Response.
Utilisez l'injection de dépendances pour utiliser les services (4.4) Comment nous l'avons vu dans le point 4.1, nous devons éviter d'utiliser des services publics. La bonne pratique est d'utiliser l'injection de dépendances dans le constructeur ou les paramètres de méthodes.
Utilisez les convertisseurs de paramètres, ils sont pratiques (4.5) J'ai rarement utilisé cette fonctionnalité. Ça introduit de la magie, c'est ce que nous voulons éviter. Je préfère avoir toute la logique dans les contrôleurs et lever les exceptions en accord avec la requête reçue.

/**
 * App generic actions and locales root handling.
 */
final class AppController extends AbstractController
{
    public function __construct(
        public readonly ArticleRepository $articleRepository,
    ) {
    }

    #[Route(path: '/', name: 'root')]

4.1 » Mon contrôleur "home" qui étend le contrôleur abstrait de Symfony.

Templates

Meilleure pratique Mon opinion Commentaires
Utilisez la casse snakecase pour les nom de templates et de variables (5.1) Pourquoi cette convention ? Parce qu'il fait bien en choisir une ! 🐍
Préfixez les fragments de template avec un underscore (5.2) C'est une règle que j'aime particulièrement. Elle vient de symfony1 ™ où de tels templates étaient appelées "partiels". Utiliser cette stratégie de nommage permet de différencier rapidement les templates principaux associés aux contrôleurs, de ceux qui peuvent être inclus ou utilisés par ces premiers.
{% trans_default_domain 'search' %}

{% set route = route is defined ? route : 'search_main' %}

<div class="row">
    <div class="col-lg-6 col-md-6 col-sm-8 ml-auto mr-auto">
        <div class="card">
            <div class="card-body">
                <form action="{{ path(route) }}" method="get">
                    <div class="form-group">
                        <label for="post-q">{{ 'keyword'|trans({}, 'search') }}</label>
                        {% include 'search/_autocomplete.html.twig' with {route: 'search_main'} %}
                    </div>

                    <div class="card-footer justify-content-center">
                        <button type="submit" class="btn btn-primary"><i class="fab fa-searchengin"></i> {{ 'search'|trans }}</button>&nbsp;&nbsp;
                        <button type="reset" class="btn"><i class="fad fa-minus-octagon"></i> {{ 'reset'|trans }}</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

5.2,6.2 » Mon partiel _form.html.twig qui est inclus à la fois dans le template de recherche et dans les articles du tutoriel Elasticsearch.

Les formulaires

Meilleure pratique Mon opinion Commentaires
Déclarez vos formulaires avec des classes PHP (6.1) Un formulaire créé à la volée dans un contrôleur ne peut pas être réutilisé. À proscrire.
Ajouter les boutons de formulaire dans les templates (6.2) Généralement, je déclare les boutons de soumission dans la classe de formulaire s'ils sont plusieurs, pour qu'il soit facile de tester lequel a été cliqué dans le contrôleur ($form->get('saveAndAdd')->isClicked()). Sinon je les mets effectivement dans le template.
Définissez les contraintes de validation sur l'objet sous-jacent (6.3) -
Utilisez une seule action pour rendre et traiter le formulaire (6.4) Évitez la duplication de code ! Si vous soumettez votre formulaire vers une autre action, vous devrez aussi préparer les données pour cette dernière en cas d'erreur de validation. C'est quelque chose que nous voulons éviter même si en théorie on pourrait utiliser un service pour ce faire. Gardez le code simple !
<?php

declare(strict_types=1);

// src/Form/NewsletterType.php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;

/**
 * Newsletter subscribe form with honeypot.
 */
final class NewsletterType extends AbstractType
{
    public const HONEYPOT_FIELD_NAME = 'email';
    public const EMAIL_FIELD_NAME = 'information';

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add(self::EMAIL_FIELD_NAME, EmailType::class, [
            'required' => true,
            'constraints' => [
                new NotBlank(),
                new Email(['mode' => 'strict']),
            ],
        ]);
        $builder->add(self::HONEYPOT_FIELD_NAME, TextType::class, ['required' => false]);
        $builder->setMethod(Request::METHOD_POST);
    }
}

6.1 » Ceci est un formulaire factice d'inscription à une newsletter que j'utilise dans l'article "Implémenter un leurre anti-spam dans un formulaire Symfony".

Internationalisation (i18n)

Meilleure pratique Mon opinion Commentaires
Utilisez le format XLIFF pour vos fichiers de traduction (7.1) Je trouve le format YAML beaucoup plus facile à utiliser. Comme je n'utilise pas d'outil de traduction professionnel, c'est suffisant pour mes besoins.
Utilisez des clés pour les traductions au lieu de chaînes (7.2) Absolument ! Utiliser une chaîne comme référence est juste laborieux et source d'erreurs. Ne faites pas ça ! ⛔
h2_7: Internationalization (i18n)

rule_7_1: Use the XLIFF Format for Your Translation Files
rule_7_1_notes: >
  I find the YAML files much easier to use. As I don't use a "professional" tool
  for translations, that's enough for my needs.

rule_7_2: Use Keys for Translations Instead of Content Strings
rule_7_2_notes: >
  Using a "label" as the reference is just a pain and prone errors. Don't do this! ⛔

p7: The i18n blocks I am using to translate the seventh section of this blog post. 🙂
rule_7_1: Utilisez le format XLIFF pour vos fichiers de traduction
rule_7_1_notes: >
  Je trouve le format YAML beaucoup plus facile à utiliser. Comme je n'utilise
  pas d'outil de traduction professionnel, c'est suffisant pour mes besoins.

rule_7_2: Utilisez des clés pour les traductions au lieu de chaînes
rule_7_2_notes: >
  Absolument ! Utiliser une chaîne comme référence est juste laborieux et source
  d'erreurs. Ne faites pas ça ! ⛔

p7: Les blocs i18n que j'utilise pour traduire la section sept de cet article 🙂.

7.2 » Les blocs i18n que j'utilise pour traduire la section sept de cet article 🙂.

Sécurité

Meilleure pratique Mon opinion Commentaires
Définir un pare-feu unique (8.1) -
Utilisez le hashage automatique de mots de passe (8.2) -
Utilisez les voteurs pour implémenter une restriction fine des droits (8.3) -
security:
  
    password_hashers:
        App\Entity\User:
            algorithm: auto

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

8.3) L'algo de hashage est en mode "auto". 8.x) Le paramètre enable_authenticator_manager: true indique l'utilisation du nouveau système de sécurité introduit dans Symfony 5.1.

S'il vous plait, ne commitez jamais de mots de passe ni de clés d'API !

Ressource web

Meilleure pratique Mon opinion Commentaires
Utilisez Webpack Encore pour construire vos ressources web (9.1) [Maj 20/04/2020] Et voilà. Je gère désormais mes assets avec Webpack, ça m'a pris un peu de temps mais c'est top. J'ai migré tout le code JavaScript dans des mixins Vue.js. Le code est vérifié avec eslint, c'est beaucoup plus propre que ce que je faisais avant avec des tags <script> isolés et intégrés dans le code HTML.
Si vous commencez un projet depuis zéro vous devriez utiliser Webpack Encore et faire les choses proprement tout de suite. Plus vous attendrez et plus il sera difficile de faire la transition.
/**
 * J'utilise un module JavaScript afin d'isoler chaque snippet dans un fichier.
 * C'est en fait un mixin Vue.js. Utilisez le code appelé par la fonction mounted()
 * ou snippetXX().
 */
export default {
  data: {
    snippetFeedback: ''
  },
  methods: {
    snippet28: function () {
      if (!this.$refs.myForm.checkValidity()) {
        this.$refs.myFormSubmit.click()
        this.snippetFeedback = 'Form is NOT valid. Enter a value.'
      } else {
        this.snippetFeedback = 'Form is valid.'
      }
      // That's it! 😁
    }
  }
}

Le mixin Vue.js pour le snippet "Vérifier la validité d'un formulaire avant sa soumission avec JavaScript". Chaque snippet est désormais isolé dans un module qui lui est propre.

Tests

Meilleure pratique Mon opinion Commentaires
Smoke testez vos URLs (10.1) Difficile de traduire "smoke test" ! Dans l'idée, si on voit de la fumée quelque part, on est à coup sûr qu'il y a du feu (et donc un gros problème). (j'me comprends, j'me comprends...) 🤔 Ces tests sont si faciles à écrire, ça ne serait pas professionnel de ne pas les écrire (je crois que j'ai vu un Tweet passer à ce propos 😁).
Mettez les URLs en dur dans les tests fonctionnels (10.2) Cette règle peut paraître étrange pour les débutants car une des premières choses que l'on apprend quand on débute avec Symfony est : "Ne jamais mettre en dur des URLs dans les templates !". Mais ici, on veut découpler au maximum le code des tests.
<?php

declare(strict_types=1);

namespace App\Tests\Functional\Controller;

use App\Subscriber\NotFoundExceptionSubscriber;
use App\Tests\WebTestCase;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\HttpFoundation\Response;

final class AppControllerTest extends WebTestCase
{
    /**
     * @see AppController::root
     */
    public function testRootAction(): void
    {
        $client = self::createClient();
        $this->assertUrlIsRedirectedTo($client, '/', '/en');
    }

    /**
     * @see AppController::homepage
     */
    public function testHomepage(): void
    {
        $client = self::createClient();
        $this->assertResponseIsOk($client, '/en');
        $this->assertResponseIsOk($client, '/fr');
    }

    /**
     * @see NotFoundExceptionSubscriber
     */
    public function testNotFoundExceptionSubscriber(): void
    {
        $client = self::createClient();
        $client->request('GET', '/404');
        self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
        self::assertSelectorTextContains('body', '404 found');
    }

    /**
     * @see ErrorHandler::handleException
     */
    public function test404(): void
    {
        $client = self::createClient();
        $client->request('GET', '/not-found');
        self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
    }
}

Dans ces tests fonctionnels, je vérifie une redirection et qu'on a bien un code de réponse 404 quand on accède à un chemin qui n'est pas géré par l'application (et aussi que le template d'erreur 404 personnalisé ne provoque pas d'erreur 500).

» Jetez un coup d'œil à mon article "Organisation des tests de votre projet Symfony".

Est-ce de l'amateurisme que de ne pas écrire de tests ?

Conclusion

Comment vous pouvez le voir, je ne suis pas toutes les "MP" mais la plupart. Je pense que tout le monde aura un point de vue différent. C'est bien sûr très bien de toutes les suivre, mais il ne faut pas les prendre comme un dogme. Pensez y plutôt comme une ligne directrice. N'en suivre aucune, par contre, sera plus problématique puisqu'une personne arrivant sur un tel projet pourrait se sentir perdue et ne pas retrouver ce qu'elle a l'habitude de trouver dans un projet Symfony. Si vous n'êtes pas d'accord avec moi, faites le moi savoir sur Twitter ou Slack et essayez de me faire changer d'avis !
Et vous, quelles sont vos meilleures pratiques Symfony ? 🤔

Et voilà ! J'espère que vous avez aimé. Découvrez d'autres informations en rapport à cet article avec les liens ci-dessous. Comme toujours, retours, likes et retweets sont les bienvenus. (voir la boîte ci-dessous) À tantôt ! COil. 😊

  Lire la doc  Plus sur le web  Plus sur Stackoverflow

Ce travail, qui inclut les exemples de code est sous licence Creative Commons BY-SA 3.0.

J'ai mis cette licence pour cet article car j'ai extrait les règles de la documentation Symfony officielle.

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : jmsche, danabrey, keversc. 👍

  Travaillez avec moi !


A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (cf le tweet à droite pour me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.
  • Cliquez sur les boutons Plus sur Stackoverflow pour me faire gagner des badges "annonceur" 🏅.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil