Quelles sont vos meilleures pratiques Symfony ?

Publié le 22/12/2019 • Mis à jour le 22/12/2019

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 🇬🇧

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

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 très important 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.
## —— Symfony binary 💻 ————————————————————————————————————————————————————————
bin-install: ## Download and install the binary in the project (file is ignored)
	curl -sS https://get.symfony.com/cli/installer | bash
	mv ~/.symfony/bin/symfony .

cert-install: symfony ## Install the local HTTPS certificates
	$(SYMFONY_BIN) server:ca:install

serve: symfony ## Serve the application with HTTPS support
	$(SYMFONY_BIN) serve --daemon

unserve: symfony ## Stop the web server
	$(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.
Utilisez les paramètres pour la configuration de l'application (2.2) -
Utilisez un préfixe court pour vos noms de paramètres (2.3) 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.4) 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("/{_locale}/snippets", name="snippet_", requirements={"_locale"="%locales_requirements%"})
 */
class SnippetController extends AbstractController
{
    public const TD = 'snippet';

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

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 les annotations pour définir le mapping des entités Doctrine (3.4) 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://ipstack.com/quickstart
 */
class Ipstack
{
    private $apiKey;
    private $translator;

    public function __construct(string $ipStackApiKey, TranslatorInterface $translator)
    {
        $this->apiKey = $ipStackApiKey;
        $this->translator = $translator;

3.2 » Un service utilisant, d'une part un paramètre nommé. Et d'autre part, le service de traduction en spécifiant l'interface relative (TranslatorInterface).

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 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
{
    /**
     * @Route("/", name="root")
     */
    public function root(Request $request): Response
    {
        $locale = $request->getPreferredLanguage($this->getParameter('activated_locales'));

        return $this->redirectToRoute('homepage', ['_locale' => $locale]);
    }

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="q">{{ 'keyword'|trans({}, 'search') }}</label>
                        <input required="required" id="q" name="q" value="{{ app.request.query.get('q') }}" type="text" class="form-control" aria-describedby="qHelp" placeholder="{{ 'enter_one_or_several_keywords'|trans({}, 'search') }}"/>
                    </div>

                    <div class="card-footer justify-content-center">
                        <button type="submit" class="btn btn-primary"><i class="fab fa-searchengin"></i> {{ 'search'|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/Type/NewsletterType.php

namespace App\Type;

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 $b, array $options): void
    {
        $b->add(self::EMAIL_FIELD_NAME, EmailType::class, [
            'required' => true,
            'constraints' => [
                new NotBlank(),
                new Email(['mode' => 'strict']),
            ],
        ]);
        $b->add(self::HONEYPOT_FIELD_NAME, TextType::class, ['required' => false]);
        $b->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 !
  This is a fake newsletter form I am using in the post <a href="/en/blog/implementing-a-honeypot-in-a-symfony-form">
  "Implementing a honeypot in a Symfony form"</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: >
  Definitely! Using a "string" as the reference is just a pain and prone errors.
  l'article <a href="/fr/blog/implementer-un-leurre-anti-spam-dans-un-formulaire-symfony">
  "Implémenter un leurre anti-spam dans un formulaire Symfony"</a>.

h2_7: Internationalisation (i18n)

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

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:
    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: auto

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        in_memory:
            memory:
                users:
                    coil:
                        password: '%env(PASSWORD)%'
                        roles: 'ROLE_ADMIN'

8.3) L'algo de hashage est en mode "auto". 8.x) Le mot de passe en mémoire provient d'une variable d'environnement qui n'est pas commité.

Si vous ne prenez pas la sécurité au sérieux, c'est juste une question de temps avant que vous vous fassiez hacker. Et... s'il vous plait... Ne commitez jamais de mots de passe ni de tokens d'API ! Veuillez noter que Symfony 4.4 introduit un nouveau composant concernant la gestion des secrets, il semble prometteur. Je ne l'ai pas encore testé, mais il est à parier qu'il sera bientôt cité dans cette rubrique.

Ressource web

Meilleure pratique Mon opinion Commentaires
Utilisez Webpack Encore pour construire vos ressources web (9.1) Sur ce blog, je ne construis pas mes ressources web (oui c'est mal ! 🙃). Je dois prendre un peu de temps pour le faire. Je pense que je testerai tout ceci dans un nouveau projet personnel que je vais lancer en 2020. 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.

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\Controller;

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

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

    /**
     * @covers ErrorHandler::handleException
     */
    public function test404(): void
    {
        $client = static::createClient();
        $client->request('GET', '/404');
        $this->assertSame($client->getResponse()->getStatusCode(), 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).

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) À la revoyure ! 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 license 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. 👍


» A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (utilisez la boîte ci-dessus pour commenter ou 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.

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

COil