Les bonnes pratiques de développement Symfony

Symfony est un Framework Français qui permet de faciliter le développement d’une application web. Framework signifiant “cadre de travail”, Symfony nous donne les outils pour développer rapidement, mais mal utilisés, cela peut vite devenir une usine à gaz. Le but de cet article est de vous donner quelques clés / bonnes pratiques à mettre en place lors du développement de votre application.

Se lancer dans le développement d’une nouvelle application n’est jamais une tâche facile.
Entre conception, modélisation et production de nombreuses étapes sont à mettre en place.
Les actions à entreprendre ne se limitent pas qu’à la chefferie de projet, certaines concernent le développement et notamment ses bonnes pratiques.

 

Symfony est un Framework Français qui permet de faciliter le développement d’une application web.
Framework signifiant “cadre de travail”, Symfony nous donne les outils pour développer rapidement, mais mal utilisés, cela peut vite devenir une usine à gaz.
Le but de cet article est de vous donner quelques clés / bonnes pratiques à mettre en place lors du développement de votre application.

 

Je n’aborderai dans mon cas que les bonnes pratiques que j’applique au quotidien, et qui me permettent de développer rapidement et efficacement.

1 – LES BONNES PRATIQUES, POURQUOI ?

Les bonnes pratiques sont à la base de tout développement. Elles permettent de faciliter la compréhension du code, de le rendre plus lisible et donc plus facilement maintenable.


Ces bonnes pratiques ont 3 avantages majeurs :

  • La qualité du code : elles permettent d’avoir un code “propre” et donc fonctionnel, réfléchit et structuré
  • La lisibilité du code : un code “bien écrit” permet de comprendre rapidement ce que fait une classe ou une fonction, sans avoir à scroller sur une fonction de 5000 lignes illisible …
  • La pérennité du code : Au-delà de les appliquer au moment du développement, les bonnes pratiques permettent de retravailler sur un projet qu’on n’a pas touché de plusieurs mois/années, fait par soi ou d’autres développeurs.

Au final, les bonnes pratiques s’appliquent à tout type de développement, que ce soit sur Symfony, Laravel, ou même du développement natif.

2 – LES BONNES PRATIQUES DE DÉVELOPPEMENT

Il est important de noter que ces bonnes pratiques sont à adapter en fonction de votre projet, de sa taille, de son envergure, de son équipe de développement, etc …

De nombreux articles existent déjà sur les bonnes pratiques Symfony, les Normes PSR ou la de modélisation de données.
Je vous laisserai le soin de les consulter car ils sont très complets et très bien écrits.

Ici ne seront abordées que les bonnes pratiques que appliquées au quotidien, et qui permettent de développer rapidement et efficacement.

2.1 – LES CONVENTIONS DE NOMMAGE

Les conventions de nommage sont très importantes dans un projet. Elles permettent de savoir rapidement à quoi correspond une variable, une fonction, une classe, etc …

 

On ne parlera pas des différents cas d’utilisation du CamelCase, du snake_case, du PascalCase, etc … mais plutôt du nom des variables que vous allez utiliser.


Dans nos exemples nous codons en anglais, mais il est tout à fait possible de coder en français (nous le faisons sur des projets très “métiers” ou l’on perd plus de temps à vérifier la traduction d’un champ qu’à coder…), tant que vous restez cohérent dans votre code.

 

L’un des points les plus importants est de choisir le nom de votre variable. Elle peut en dire long sur ce qu’elle est quand elle est bien choisie.

ce que nous évitons

				
					//variable temporaire
$tmp = $request->get('only_user', false);

//variable trop generique
$users = $this->em->getRepository(User::class)->findBy(['is_active' => true]);

//s'applique aussi aux nom de fonctions
function getUsers() : array
{
    return $this->em->getRepository(User::class)->findBy(['is_active' => true]);
}
				
			

ce que nous faisons

				
					//variable temporaire > nommez les quand même cela ne coute pas grand choses
$isOnlyUser = $request->get('only_user', false);

//essayer de donner un peu d'informations
$activeUsers = $this->em->getRepository(User::class)->findBy(['is_active' => true]);

//essayer de donner des infos moins génériques
function getAdminUserList() : array
{
return $this->em->getRepository(User::class)->findBy(['is_active' => true]);
}
				
			

En général, nous appliquons quelques règles simples :

				
					//pluriels pour les tableaux ou les ArrayCollection
$users = $this->em->getRepository(User::class)->findAll();

//singulier pour les objets (on essaye de reprendre le nom de la classe)
$user = $this->em->getRepository(User::class)->find(1);

//utiliser is, has, contains pour des booléens
$isOnlyUser = true;
$hasChildrens = = $nav->getChildrens()->count() > 0;
$containsElements = $order->getProducts()->count() > 0;
				
			

2.2 – LES EARLY RETURN

Les early return sont une pratique qui consiste à sortir d’une fonction le plus tôt possible.

 

Cela permet de réduire la complexité de la fonction, et de ne pas imbriquer trop de conditions.

ce que nous évitons

				
					/* UserManager.php */

public function activateUser(User $user) : void
{
    if (!$user->getIsActive()) {
    $isOnBlackList = false;
    
        foreach ($this->blackListedUsers as $blackListedUser) {
            if ($blackListedUser->getId() === $user->getId()) {
            $isOnBlackList = true;
            }
        }
    
        if(!$isOnBlackList) {
            $user->setIsActive(true);
            $user->removeRole('ROLE_INACTIVE');
            $this->em->flush();
        }
    }
}

// exemple générique
if (condition1) {
    if (condition2) {
        if (condition3) {
            // -- instructions
        }
    }
}
				
			

ce que nous faisons

				
					public function activateUser(User $user) : void
{
    if ($user->getIsActive()) {
    return;
    }

    //on en profite pour refactoriser pour un peu plus de lisibilité
    if($this->isUserBlackListed($user)){
        return;
    }

    $user->setIsActive(true);
    $user->removeRole('ROLE_INACTIVE');
    $this->em->flush();
}

// exemple générique (on peut refactoriser plus mais c'est pour l'exemple)

if (!condition1) {
    return;
}

if (!condition2) {
    return;
}

if (!condition3) {
    return;
}

// -- instructions
				
			

2.3 – AVOIR DES CONTROLLERS LISIBLES

Un Controller Symfony est censé être le plus léger possible. Son rôle est de récupérer les informations de la requête, et de rendre une réponse.

 

Tout ce qui se passe entre ces deux étapes doit être reporté dans une (ou plusieurs) classe, un service, ou dans un repository avec un nom clair.

ce que nous évitons

				
					/* UserController.php */

#[Route('/')]
public function indexAction(Request $request) : Response{
    $users = $this->em->getRepository(User::class)->findBy([
        'is_active' => true,
        'is_admin' => false,
        'role' => 'ROLE_USER'
    ]);

    return $this->render('user/index.html.twig', [
        'users' => $users
    ]);
}

#[Route('/activate/{id}')]
public function activateAction(Request $request, User $user) : Response
{
    $user->setIsActive(true);
    $user->removeRole('ROLE_INACTIVE');
    
    $this->getDoctrine()->getManager()->flush();
    
    $this->addFlash('success', 'User activated');
    
    return $this->render('user/show.html.twig', [
        'user' => $user
    ]);
}
				
			

ce que nous faisons

				
					/* UserController.php */

#[Route('/')]
public function indexAction(Request $request) : Response
{
    $users = $this->em->getRepository(User::class)->getAdminUserList();
    
    return $this->render('user/index.html.twig', [
        'users' => $users
    ]);
}

#[Route('/activate/{id}')]
public function activateAction(Request $request, User $user, UserManager $userManager) : Response
{
    $userManager->activateUser($user);
    
    $this->addFlash('success', 'User activated');
    
    return $this->render('user/show.html.twig', [
        'user' => $user
    ]);
}
				
			

2.4 – UTILISEZ AU MAXIMUM LES OUTILS SYMFONY

LES FORMTYPES

Les formulaires Symfony servent à rendre des formulaires html mais surtout à controller et valider les données. On les utilise facilement pour créer ou modifier en enregistrement, mais aussi pour faire des recherches, des filtres etc …

 

Voici ce que l’on voit souvent pour les recherches :

ce que nous évitons

Cela reste relativement propre, mais les données ne sont pas contrôlées et on travaille avec des tableaux pour les paramètres ce qui n’est pas très explicite.

				
					/* UserController.php */

#[Route('/')]
public function indexAction(Request $request) : Response
{
    $name = $request->get('name', null);
    $isActive = $request->get('is_active', null);
    
    $users = $this->em->getRepository(User::class)->search([
        'name' => $name,
        'is_active' => $isActive
    ]);
    
    return $this->render('user/index.html.twig', [
        'users' => $users
    ]);
}

/******************************/

/* UserRepository.php */

public function search($params): array
{
    $qb = $this->createQueryBuilder('u');
    
    if($params['name']) {
        $qb->andWhere('u.name = :name')
        ->setParameter('name', $params['name']);
    }
    
    if($params['is_active']) {
        $qb->andWhere('u.is_active = :is_active')
        ->setParameter('is_active', $params['is_active']);
    }
    
    return $qb->getQuery()->getResult();
}
				
			

ce que nous faisons

Ici on a des données validées (et limitées) et on retrouve bien les différentes étapes du
controller:

  • Récupération du formulaire
  • Validation du formulaire
  • Recherche
				
					/* model/UserSearch.php */

class UserSearch {

    #[Assert\Length(max: 100)]
    private ?string $name = null;
    private ?bool $isActive = null;
    public function getName(): ?string {
        return $this->name;
    }
    public function isActive(): ?bool {
        return $this->isActive;
    }
}

/******************************/

/* UserSearchType.php */

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
    ->add('name', TextType::class, [
        'required' => false,
        'label' => 'Nom',
    ])
    ->add('lastName', CheckboxType::class, [
        'required' => false,
        'label' => 'Actif',
    ]);
}
    
public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'data_class' => UserSearch::class,
    ]);
}

/******************************/

/* UserRepository.php */

public function search(UserSearch $userSearch): array
{
    $qb = $this->createQueryBuilder('u');
    
    if($userSearch->getName()) {
        $qb->andWhere('u.name = :name')
        ->setParameter('name', $userSearch->getName());
    }
    if($userSearch->isActive()) {
        $qb->andWhere('u.is_active = :is_active')
        ->setParameter('is_active', $userSearch->isActive());
    }
    
    return $qb->getQuery()->getResult();
}

/******************************/

/* UserController.php */

#[Route('/')]
public function indexAction(Request $request, UserFormBuilder $userFormBuilder, UserFormHandler $userFormHandler, UserRepository $userRepository
) : Response
{
    //un service pour récupérer le formulaire
    $searchForm = $userFormBuilder->getSearchForm();
    //un service pour handle et vérifier
    $userSearch = $userFormHandler->handleSearch($request, $searchForm);
    
    $users = $userRepository->search($userSearch);
    
    $users = $this->em->getRepository(User::class)->search([
        'name' => $name,
        'is_active' => $isActive
    ]);
    
    return $this->render('user/index.html.twig', [
        'users' => $users
    ]);
}
				
			

LE PROFILER

Le profiler Symfony est un outil très puissant qui permet de voir ce qui se passe dans votre application.
On l’utilise principalement pour :

  • Analyser le temps de rendu d’une page
  • Analyser le nombre de requêtes SQL dans la page

 

Notre article Pourquoi et comment accélérer la vitesse de son site en parle aussi !

LES EVENTLISTENER

Véritable équivalent des triggers SQL, les EventListener permettent de déclencher des actions à des moments précis.

Ils sont principalement utiles pour l’envoi de mail après un enregistrement ou des mises à jour de champs.

 

C’est la base d’une utilisation avancée de Symfony : Events and Event Listeners

LE LOGGER SYMFONY

Un des outils les plus utiles en terme de temps / efficacité.

  • Coder propre, c’est bien
  • Tester c’est génial
  • Mais être averti si votre site tombe et savoir pourquoi, c’est encore mieux !

Le Logger Symfony Monolog s’installe très facilement et permet de logger des informations dans des fichiers, des mails, des bases de données, des services externes etc …

Personnellement, j’utilise l’envoie par mail sur un site simple, pour avoir rapidement des retours sur les erreurs.
Sur nos projets en général, on passe sur Sentry qui est un outil de monitoring très puissant mais qui est plus long à mettre en place.

2.5 – LES JEUX DE DONNÉES / FIXTURES

On change un peu de registre, il ne s’agit pas de code en soit mais de méthode de travail.

 

Les jeux de données sont des données fictives qui permettent de tester votre application. Les prévoir d’entrée de jeu est un gain de temps considérable.

Elles permettent à la fois :

  • D’avoir un jeu de données prêt quand quelqu’un récupère l’application sur sa machine
  • De pouvoir travailler facilement l’intégration avec des données déjà en base pour les listes, la pagination, etc…
  • De saisir des données particulières pouvant être utilisés pour des tests fonctionnels
  • Créer un jeu de données de montée en charge pour voir comment réagit votre application avec des tables de 100 000 lignes (au besoin)

Vous pouvez les créer à la main, mais il existe des outils pour les générer automatiquement :

2.6 – TESTEZ VOTRE APPLICATION

Cela parait évident… mais il est important de le rappeler : tester son application permet de s’assurer que tout fonctionne correctement.

Une solution qui fonctionne bien chez nous, c’est de se challenger ou challenger les équipes. Bizarrement, on est plus inventif et précautionneux quand il s’agit de faire planter le code de quelqu’un d’autre plutôt que le sien !

 

De plus, quand vous testez votre application, pensez à le faire intelligemment, avec de vraies données : “Qsd” et “Aze” ne sont pas des noms / prénoms !


Si vous ne saisissez pas de données cohérentes, vous ne vous rendrez pas forcément compte des erreurs d’enregistrements ou de mauvais champs

 

Enfin, comme on peut le voir partout, les tests fonctionnels sont de très bons outils pour assurer la pérennité de votre application. Il va sans dire que les tests fonctionnels ont un coût, et que malheureusement, ils ne sont pas toujours rentables sur des projets de petite taille.

2.7 – REFACTORISEZ AU BON MOMENT !

Celui là est un peu plus subjectif, mais il est important de savoir quand refactoriser son code.

Quand on fait de la qualité de code, ou des tests fonctionnels, on aime bien imaginer tous les cas ! Faire les fonctions le plus générique possible etc… C’est top ! mais cela prend du temps.
Et surtout, on entend souvent “oui mais si jamais après on rajoute ça, au moins c’est prévu ….” et au final on n’a jamais rajouté ça.

Evidemment, il y a beaucoup de choses à dire sur les bonnes pratiques de développement, et tout n’a pas été abordé.

Mais comme dit en introduction, vous avez les clés pour bien démarrer un projet !