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 ! 😎
» 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
## —— 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
#[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
*/
final readonly class AbstractApi
{
public function __construct(
private HttpClientInterface $abstractApiClient,
private string $abstractApiKey,
) {
}
3.2 » Un service utilisant des paramètres nommés.
Contrôleurs
/**
* 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
{% 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>
<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
<?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)
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é
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
/**
* 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
<?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 ?
Just blogged: "Is not writing tests unprofessional?" https://t.co/Vrr0a1qNQW
— Matthias Noback (@matthiasnoback) September 30, 2019
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) À bientô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. 👍
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 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 ! 😉
[🇫🇷] 11ème article de l'année. Nous passerons en revue les meilleures pratiques Symfony ! https://t.co/qySNDFC57S Retours, likes et retweets sont les bienvenus ! 😉 Objectif annuel : 11/12 (91%) #symfony #php #strangebuzz #blog #blogging ☃️ 🎄 🎁
— [SB] COil (@C0il) December 27, 2019