Symfony The Fast Track : découvrir Symfony 5

Après avoir exploré ce que Docker pouvait m’apporter, je me remets à l’apprentissage de Symfony. Pour cela, je suis les instructions de « The Fast Track », un guide disponible sur le site de Symfony. Il est plutôt bien fait, mais, entre sa publication et maintenant, il y a déjà eu quelques changements comme EasyAdmin qui est passée de la v2 à la v3. J’ai rencontré également d’autres problèmes que je vais détailler.

Qu’est-ce que The Fast Track ?

Présentation

The Fast Track est un guide rédigé par Fabien Potencier – le créateur de Symfony – et distribué sous licence Creative Commons BY-NC-SA 4.0. Il est possible de le consulter gratuitement en ligne au travers de nombreuses traductions, comme le français. Il est également possible de payer pour d’autres éditions (imprimé, numérique ou les deux) afin de soutenir Symfony.

Couverture de The Fast Track

Le guide est composé de 30 étapes pour donner un aperçu du framework Symfony, en utilisant la version 5. Les premières étapes présentent essentiellement les prérequis et le projet qui servira de fil conducteur dans le guide. Ensuite, nous créons ce projet de A à Z en utilisant les contrôleurs, les entités, et d’autres choses encore. Le guide s’attarde également sur d’autres aspects d’un projet : les tests unitaires et fonctionnels, l’utilisation d’une API ou encore la gestion des performances.

Pourquoi utiliser ce guide ?

Si vous débutez avec Symfony ou si vous utilisez une version antérieure, le guide permet de comprendre le fonctionnement de cette nouvelle version.

Dans mon cas, l’architecture d’un projet Symfony ne m’est pas inconnue, mais je n’ai jamais développé de site avec ce framework. Je me suis dit, vu l’étendu du guide, que c’était une bonne approche pour découvrir Symfony. La seule chose que j’ai mis de côté c’est SymfonyCloud. Il existe une période d’essai gratuite, mais comme je n’avais pas envie de me presser, j’ai préféré me concentrer sur le reste.

Les problèmes rencontrés en suivant The Fast Track

Le guide semble complet et de qualité. Cependant, il existe quelques incohérences et, surtout, depuis l’écriture du guide certaines versions ont évolué et il n’est pas possible de suivre le guide à la lettre. Je profite donc de cet article pour les lister et pour expliquer comment je m’y suis pris pour régler ces problèmes.

Utiliser EasyAdmin v3 avec Symfony The Fast Track

Le premier problème survient à l’étape 9 : « Configurer une interface d’administration ».

Pourquoi installer EasyAdmin v3 plutôt que la v2 ?

Si je tente d’installer EasyAdmin en spécifiant à Composer que je souhaite la version 2 (comme dans le guide), l’installation échoue. La version 2 n’est pas compatible avec les versions des autres dépendances. J’ai bien tenté de modifier ces versions, mais il y a trop de changements à effectuer pour rendre installable la version 2 d’EasyAdmin ; j’ai abandonné cette approche.

J’ai également tenté de chercher sur Google si d’autres instructions étaient disponibles, ou s’il existait un fichier composer.json adapté, mais la recherche a été vaine. Bref, dernière solution (la moins rapide mais peut-être la plus formatrice) : se pencher sur la documentation d’EasyAdmin pour comprendre le fonctionnement de cette version 3.

Comment configurer EasyAdmin pour l’adapter à The Fast Track ?

Tout d’abord, il faut installer EasyAdmin :

symfony composer req admin

Ensuite, il faut créer un tableau de bord qui servira de point d’entrée à l’interface d’administration.

php bin/console make:admin:dashboard

Enfin, nous aurons besoins de CRUD controllers pour interagir avec les entités de l’ORM Doctrine. Il en faut deux :

  • l’un pour les commentaires
  • l’autre pour les conférences
php bin/console make:admin:crud

Choisissez le premier et répétez l’opération pour le deuxième. L’ordre n’a pas d’importance. Concernant les choix, vous pouvez garder ceux par défaut.

Il faut maintenant lier ces CRUD controllers à notre tableau de bord pour pouvoir y accéder. Pour cela, il faut modifier notre fichier DashboardController.php :

<?php

namespace App\Controller\Admin;

use App\Entity\Comment;
use App\Entity\Conference;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\User\UserInterface;

class DashboardController extends AbstractDashboardController
{
    /**
     * @Route("/admin", name="admin")
     */
    public function index(): Response
    {
        return parent::index();
    }

    public function configureDashboard(): Dashboard
    {
        return Dashboard::new()
            ->setTitle('Guestbook');
    }

    public function configureMenuItems(): iterable
    {
        yield MenuItem::linktoRoute('Back to the website', 'fa fa-home', 'homepage');
        yield MenuItem::linktoDashboard('Dashboard', 'fa fa-toolbox');
        yield MenuItem::section('Manage');
        yield MenuItem::linkToCrud('Conferences', 'fa fa-map-marker', Conference::class);
        yield MenuItem::linkToCrud('Comments', 'fa fa-comments', Comment::class);
        // yield MenuItem::linkToCrud('The Label', 'icon class', EntityClass::class);
    }

    public function configureUserMenu(UserInterface $user): UserMenu
    {
        return parent::configureUserMenu($user)->addMenuItems([
            MenuItem::linkToLogout('Logout', 'fa fa-sign-out'),
        ]);
    }
}

Il vous faudra également éditer les fichiers de chaque CRUD controller pour configurer les champs et les filtres.

Pour CommentCrudController.php :

<?php

namespace App\Controller\Admin;

use App\Entity\Comment;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;

class CommentCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Comment::class;
    }

    public function configureFilters(Filters $filters): Filters
    {
        return $filters->add('conference')->add('email');
    }

    public function configureFields(string $pageName): iterable
    {
        return [
            IdField::new('id')->hideOnForm(),
            TextField::new('author'),
            EmailField::new('email'),
            DateTimeField::new('createdAt'),
            TextareaField::new('text'),
            ImageField::new('photoFilename'),
            AssociationField::new('conference'),
        ];
    }
}

Pour ConferenceCrudController.php :

<?php

namespace App\Controller\Admin;

use App\Entity\Conference;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;

class ConferenceCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Conference::class;
    }

    public function configureFields(string $pageName): iterable
    {
        return [
            IdField::new('id'),
            Field::new('city'),
            Field::new('year'),
            BooleanField::new('isInternational'),
            AssociationField::new(('comments')),
        ];
    }
}

Gérer le cycle de vie des objets Doctrine

Si vous suivez les instructions de The Fast Track, à l’étape 13, dans la partie « 13.2 Ajouter des slugs aux conférences », vous allez rencontrer une erreur.

Attempted to load class « Constraint » from namespace « Symfony\Component\Validator ».

Did you forget a « use » statement for « Doctrine\DBAL\Schema\Constraint »?

Pour pallier à ce problème, nous avons besoin d’une nouvelle dépendance :

symfony composer req validator

Cette fois-ci, c’est tout ce qu’il y a à faire. Vous pouvez à nouveau utiliser la commande :

symfony console make:migration

Afficher les photos dans l’interface d’administration

Là encore, les instructions concernent EasyAdmin version 2. Voyons comment les adapter à la version 3. Il faut déclarer le base_path et le label.

Je n’ai rien vu dans la documentation d’EasyAdmin, mais VS Code me permet d’accéder rapidement à la définition avec F12.

Je vois que la fonction new peut comporter un label en plus du nom de la propriété, et je vois qu’il existe une fonction setBasePath. Dans le fichier CommentCrudController.php, je transforme donc :

ImageField::new('photoFilename')

// en

ImageField::new('photoFilename', 'Photo')→setBasePath('/uploads/photos/')

Malheureusement, ça ne suffit pas. Tout semble fonctionner, sauf si je souhaite éditer un commentaire qui possède une photo dans l’administration. Cette erreur apparaît :

The form’s view data is expected to be an instance of class Symfony\Component\HttpFoundation\File\File, but is a(n) string. You can avoid this error by setting the « data_class » option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\Component\HttpFoundation\File\File.

Le problème semble venir du type de champs. Je n’ai pas trouvé de solution sans passer par VichUploader et je préfère suivre le guide que de m’aventurer avec des dépendances supplémentaires.

Donc, j’ai pensé à une solution alternative : dans le formulaire d’administration, nous n’avons pas besoin d’afficher la photo, nous allons uniquement afficher le nom du fichier dans un champs texte. Par contre, dans la liste des commentaires, nous afficherons bien la photo.

Voici le fichier final :

<?php

namespace App\Controller\Admin;

use App\Entity\Comment;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;

class CommentCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Comment::class;
    }

    public function configureFilters(Filters $filters): Filters
    {
        return $filters->add('conference')->add('email');
    }

    public function configureFields(string $pageName): iterable
    {
        if (Crud::PAGE_EDIT === $pageName) {
            $imageField = TextField::new('photoFilename', 'Photo');
        } else {
            $imageField = ImageField::new('photoFilename', 'Photo')->setBasePath('/uploads/photos/');
        }

        return [
            IdField::new('id')->hideOnForm(),
            TextField::new('author'),
            EmailField::new('email'),
            TextField::new('state'),
            DateTimeField::new('createdAt'),
            TextareaField::new('text'),
            $imageField,
            AssociationField::new('conference'),
        ];
    }
}

Sécuriser l’interface d’administration

À l’étape 15, dans la partie « 15.1 Définir une entité User », il faut générer une migration puis migrer la base de données. Un nouveau problème apparaît.

An exception occurred while executing ‘CREATE UNIQUE INDEX UNIQ_911533C8989D9B62 ON conference (slug)’:

SQLSTATE[42P07]: Duplicate table: 7 ERROR: relation « uniq_911533c8989d9b62 » already exists

Je ne sais pas si j’ai fait une mauvaise manipulation quelque part ou si les instructions ne fonctionnent pas correctement… Symfony tente de placer une contrainte UNIQUE sur le champs slug, mais certaines données de la table ne respectent pas cette contrainte.

Ok, mais comment régler ça ? J’ai cherché un peu et une commande semblait revenir :

symfony console doctrine:schema:update –force

Aucune erreur, la table a été mise à jour. Si j’ai bien compris, il n’y a pas besoin de refaire la migration puisque cette commande crée automatiquement toutes les tables de base de données nécessaires pour chaque entité connue. Et, effectivement, si vous tenter de faire la migration, la précédente erreur est remplacée par une nouvelle : la table admin est déjà présente. Donc, tout semble ok.

Enfin presque. Lorsque vous voudrez faire une nouvelle migration, Symfony essaiera de faire à nouveau cette migration et vous obtiendrez donc des erreurs. Il faut donc lui dire d’ignorer cette migration :

symfony console doctrine:migrations:version 'DoctrineMigrations\Version20201015154752' –add

Version20201015154752 est à remplacer par le nom de votre migration.

Les tests fonctionnels

À l’étape 17, dans la partie concernant Akismet, j’obtiens toujours une erreur :

Failed asserting that exception of type « RuntimeException » is thrown.

Je n’ai pas trouvé comment la résoudre…

Par contre, dans la partie « 17.5 Parcourir un site web avec des tests fonctionnels », j’obtiens également des échecs, mais j’ai trouvé comment corriger le code pour que ces tests passent.

La ligne de code qui échoue :

$this->assertSelectorExists('div:contains("There are 1 comments")');

Je ne suis pas familier avec les tests, et je débute avec Symfony, je ne comprends pas pourquoi ça ne fonctionne pas. Cependant, j’ai trouvé un moyen de faire la même chose avec une autre fonction  :

$this->assertSelectorTextContains('div', 'There are 1 comments');

Donc, il suffit de remplacer assertSelectorExist par assertSelectorTextContains à chaque occurrence dans le guide.

Quelques incohérences dans le guide

Je chipote puisqu’il n’y a rien de bloquant, mais certaines parties ne correspondent pas tout à fait à ce que nous obtenons.

Par exemple, à l’étape 14, à la partie « 14.2 Afficher un formulaire », il est écrit :

Elle ajoute également enctype=multipart/form-data à la balise <form> comme l’exige le champ d’upload de fichier.

Le champ d’upload n’a toujours pas été configuré, il s’agit d’un champ texte. Donc, non, mon formulaire ne contient pas ça. Le champ est configuré à l’étape suivante ; cette instruction sera ajoutée à ce moment-là.

Dans la partie « 21.4 Purger le cache HTTP pour les tests » de l’étape 21, il faut définir une fonction purgeHttpCache sauf qu’à l’étape suivante la fonction s’appelle flushHttpCache. Là ça va, nous venons de de la déclarer ; il est facile de comprendre qu’il s’agit de la même fonction. Lorsque cette même fonction revient plusieurs étapes après, c’est moins évident. Ici ce n’est peut-être pas le cas. Je n’ai pas noté les précédentes incohérences pour lesquelles ça s’appliquait.

En somme, il faut rester attentif et comprendre ce que l’on fait. Même si c’est un peu frustrant de se retrouver avec des erreurs ou problèmes en suivant un guide, cela reste formateur pour peu qu’on cherche comment les contourner.

Bref, dans cette dernière partie, je chipote. Dans l’ensemble, The Fast Track reste un bon guide pour découvrir Symfony 5 et comprendre le fonctionnement du framework.

Je me suis arrêté à l’étape 22. J’ai regardé la suite du guide, mais je n’ai pas mis en application. Il y a encore quelques problèmes sans doute à cause de chemins qui diffèrent, je n’ai pas cherché. J’y reviendrai peut-être, mais, pour le moment, je vais essayer autre chose par moi-même en mettant le nez dans le documentation.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.