Annexe TP n°1 - Construction de l’appli Todo avec Symfony
Construction de l’application « from scratch »

Table des matières

1. Introduction

Cette annexe documente l’essentiel des étapes utilisées pour réaliser la version 2.x de l’application Todo telle que vue dans la séance de TP.

Inutile de tester cela, sauf si vous avez vraiment l’intention de repartir d’une feuille blanche pour un projet personnel.

Cette procédure est fournie sans garantie d’exhaustivité.

2. Création d’un projet Symfony grâce à Composer

Vous allez utiliser la commande new de l’outil Symfony en ligne de commande (Symfony CLI), qui va créer le répertoire de développement d’une application Symfony et y télécharger le code source d’un squelette d’application.

Cette commande s’appuie sur le gestionnaire de paquetages PHP Composer, en appelant la commande create-project de composer.

Nous avons choisi de privilégier la version LTS de Symfony pour les TP afin de stabiliser les contenus sur plusieurs années. Néanmoins vous devriez pouvoir exécuter à peu près les mêmes opérations sur la version stable de Symfony. Modifiez la version en choisissant plutôt --version=stable ci-dessous, si vous préférez tenter l’aventure.

Téléchargez et installez le squelette d’application standard de Symfony, sur lequel vous allez travailler :

cd $HOME/CSC4101/tp-01/
symfony --debug new --no-git mytodo --version=lts

Cette commande affiche plusieurs infos intéressantes pour comprendre ce qui se passe :

* Creating a new Symfony 6.4 project with Composer

...

[OK] Your project is now ready in /.../mytodo                                   

Composer a téléchargé dans un nouveau sous-répertoire mytodo/ le squelette d’un projet d’application Symfony, pour une version précise de Symfony, ici la 6.4. C’est celle qui correspond à la version LTS (Long-Term Support) de Symfony (au moment où on exécute la commande symfony new --version=lts).

3. Ajout des modules PHP utiles

Nous aurons besoin de différents modules pour permettre à l’application de fonctionner, ou pour nous aider dans le prototypage de cette application, notamment :

Doctrine
le composant d’accès à la base de données via des objets PHP
le « Maker bundle »
qui va nous permettre de générer du code PHP plutôt que d’avoir à tout coder à la main
les « DataFixtures »
qui permettent de charger automatiquement des données de test dans la base de données

Procédez à l’installation avec les commandes suivantes à l’intérieur du répertoire mytodo :

cd mytodo/
  • Monolog :

    symfony composer require symfony/monolog-bundle
    
  • ORM Doctrine :

    symfony composer require -n symfony/orm-pack
    
  • « Maker bundle » :

    symfony composer require --dev -n symfony/maker-bundle
    
  • DataFixtures :

    symfony composer require --dev doctrine/doctrine-fixtures-bundle
    

Composer a téléchargé et extrait dans vendor/ les différentes bibliothèques PHP. On peut donc les utiliser dans le code de l’application.

4. Ajout de l’entité principale : « Todo »

Si vous êtes comme la plupart des programmeurs, vous aimez modérément copier-coller du code qui pourrait être généré automatiquement.

Ça tombe bien, Symfony propose différent assistants qu’on ne va pas se priver d’utiliser pour générer la base du code de nos applications, plutôt que d’écrire nous-même du code buggé.

Lancez l’assistant make:entity, dans le terminal, depuis l’intérieur du projet Symfony. Il va nous servir à générer le code de classes PHP pour gérer notre entité « Todo » du modèle de données.

Répondez aux questions de l’assistant pour obtenir une interaction similaire à la trace présentée ci-dessous :

symfony console make:entity
 Class name of the entity to create or update (e.g. VictoriousPuppy):
 > Todo

 created: src/Entity/Todo.php
 created: src/Repository/TodoRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press <return> to stop adding fields):
 > title

 Field type (enter ? to see all types) [string]:
 > 

 Field length [255]:
 > 

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Todo.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > completed

 Field type (enter ? to see all types) [string]:
 > ?

Main Types
  * string
  * text
  * boolean
  * integer (or smallint, bigint)
  * float

Relationships/Associations
  * relation (a wizard 🧙 will help you build the relation)
  * ManyToOne
  * OneToMany
  * ManyToMany
  * OneToOne

Array/Object Types
  * array (or simple_array)
  * json
  * object
  * binary
  * blob

Date/Time Types
  * datetime (or datetime_immutable)
  * datetimetz (or datetimetz_immutable)
  * date (or date_immutable)
  * time (or time_immutable)
  * dateinterval

Other Types
  * ascii_string
  * decimal
  * guid


 Field type (enter ? to see all types) [string]:
 > boolean

 Can this field be null in the database (nullable) (yes/no) [no]:
 > 

 updated: src/Entity/Todo.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > created

 Field type (enter ? to see all types) [string]:
 > ?

Main Types
  * string
  * text
  * boolean
  * integer (or smallint, bigint)
  * float

Relationships/Associations
  * relation (a wizard 🧙 will help you build the relation)
  * ManyToOne
  * OneToMany
  * ManyToMany
  * OneToOne

Array/Object Types
  * array (or simple_array)
  * json
  * object
  * binary
  * blob

Date/Time Types
  * datetime (or datetime_immutable)
  * datetimetz (or datetimetz_immutable)
  * date (or date_immutable)
  * time (or time_immutable)
  * dateinterval

Other Types
  * ascii_string
  * decimal
  * guid


 Field type (enter ? to see all types) [string]:
 > datetime

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Todo.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > updated

 Field type (enter ? to see all types) [string]:
 > datetime

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Todo.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > 


         
  Success! 
         

 Next: When you're ready, create a migration with symfony console make:migration

L’assistant a créé pour nous deux classes PHP qui utilisent Doctrine :

  • src/Entity/Todo.php : gère les instances en mémoire des tâches
  • src/Repository/TodoRepository.php : gère le chargement des tâches depuis la base de données

Chargez le projet dans votre IDE, pour voir apparaître le code de ces deux classes.

Attention à lire attentivement les questions (ainsi que les messages en réponse), et à éviter le copier/coller un peu rapide.

5. Configuration de la base de données de tests dans l’environnement de développement

Dans l’environnement de développement, on va effectuer des tests sur le SGBD SQLite. Il faut donc configurer cela?

  1. Chargez le le fichier .env dans l’IDE

    Attention, c’est un fichier caché… il faut éventuellement le faire apparaître dans l’IDE.

    Pour cela, dans Eclipse :

    • cliquez sur le bouton avec trois petits points verticaux, en haut à droite de la colonne du « Project Explorer »;
    • puis sur « Filters and Customizations »;
    • dans la liste des « Pre-Set filters », dé-cochez « .* ressources », puis « OK ».

    Les fichiers cachés apparaissent dans le projet, dont le fichier .env.

  2. Ouvrez .env, et vérifiez la valeur de la variable DATABASE_URL.

    Elle doit typiquement prendre la valeur sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db, afin d’utiliser SQLite pour les besoins du développeur qui teste en local :

    DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
    

    Ainsi, la base de données SQLite utilisée pour les tests sera créée dans $HOME/CSC4101/tp-01/mytodo/var/data_dev.db.

  3. Créez un fichier de base de données SQLite (vide), en exécutant la commande suivante :

    symfony console doctrine:database:create
    
  4. Créez le schéma de la base SQLite (tables, index, etc.), en utilisant la sous-commande doctrine:schema:create :

    symfony console doctrine:schema:create
    
  5. Ajoutez des données de test :

    symfony console dbal:run-sql "INSERT INTO todo (TITLE, COMPLETED)  VALUES ('Se passer de ChatGPT', false)"
    

6. Ajout de l’interface console

Cette étape va permettre d’ajouter une couche de présentation (dans la console d’un terminal) dans notre application.

6.1. Ajout de la commande app:list-todos

Ajoutons une interface en ligne de commande à notre application PHP, pour disposer d’une commande accessible pour le développeur via l’interface principale offerte par bin/console.

Cette fois encore, utilisons un assistant générateur de code pour nous faciliter le travail :

symfony console make:command
Choose a command name (e.g. app:victorious-popsicle):
> app:list-todos

created: src/Command/ListTodosCommand.php

        
 Success! 
        

Next: open your new command class and customize it!
Find the documentation at https://symfony.com/doc/current/console.html

Consultez le résultat généré dans src/Command/ListTodosCommand.php

Vérifiez que la commande est bien disponible via :

symfony console list app

et :

symfony console app:list-todos --help

Et enfin qu’elle répond quand on l’invoque :

symfony console app:list-todos

                                                                                                                        
 [OK] You have a new command! Now make it your own! Pass --help to see your options.                                    
                                                                                                                        

Le squelette de code de commande est là, prêt à recevoir notre code applicatif.

6.2. Branchement de l’interface à la base de données

Pour que notre commande puisse afficher la liste des tâches on va devoir réaliser quelques branchements entre notre code et celui de Doctrine, qui gère l’accès aux données.

On va modifier le code de src/Command/ListTodosCommand.php, pour ajouter un attribut todoRepository dans la classe ListTodosCommand, permettant de gérer l’accès à la base de données via le composant Doctrine.

Copiez-collez les éléments ci-dessous pour ajouter l’attribut todoRepository, et son initialisation dans le constructeur de ListTodosCommand (attention à ne pas dupliquer la déclaration de la classe) :

// ...

use App\Entity\Todo;
use App\Repository\TodoRepository;
use Doctrine\Persistence\ManagerRegistry;

use Symfony\Component\Console\Attribute\AsCommand;
// ...


class ListTodosCommand extends Command
{
        /**
         *  @var TodoRepository data access repository
         */
        private $todoRepository;

        /**
         * Plugs the database to the command
         * 
         * @param ManagerRegistry $doctrineManager
         */
        public function __construct(ManagerRegistry $doctrineManager)
        {
                $this->todoRepository = $doctrineManager->getRepository(Todo::class);

                parent::__construct();
        }

        // ...

7. Ajout de l’affichage de la liste des tâches

Ajoutons maintenant au code de la commande, qui a été généré par l’asssistant, les instructions permettant de charger toutes les tâches présentes dans la base de données.

On utilise la méthode findAll() du repository Doctrine des tâches TodoRepository, et qui renvoie un tableau de tâches. On peut manipuler ce tableau comme un tableau PHP ordinaire, par exemple avec foreach.

Modifiez la méthode execute() de ListTodosCommand :

protected function execute(InputInterface $input, OutputInterface $output): int
{
         $io = new SymfonyStyle($input, $output);

         // récupère une liste toutes les instances de la classe Todo
         $todos = $this->todoRepository->findAll();
         //dump($todos);
         if(!empty($todos)) {
                 $io->title('list of todos:');

                 $io->listing($todos);
         } else {
                 $io->error('no todos found!');
                 return Command::FAILURE;
         }
         return Command::SUCCESS;
}

Testez avec :

symfony console app:list-todos

Le programme échoue avec une erreur (attendue) du type :

symfony console app:list-todos 

list of todos:
==============


In SymfonyStyle.php line 107:
                                                                    
  Object of class App\Entity\Todo could not be converted to string  
                                                                    

app:list-todos [--option1] [--] [<arg1>]

Ce genre de chose devrait vous paraître cohérent avec ce qui a été vu l’année passée en programmation orientée objet.

La méthode listing() de SymfonyStyle essaie d’afficher une chaîne de caractères sur la console… mais une instance de la classe Todo peut-elle être convertie en chaîne de caractères ? Pas de base, en PHP.

Réglons cela.

8. Ajout de Todo::__toString()

Ajoutez, dans src/Entity/Todo.php, la méthode de conversion de tâche en chaîne de caractère qui nous manque : Todo::__toString().

Par exemple :

public function __toString() {
        $s = '';
        $s .= $this->getId() .' '. $this->getTitle() .' ';
        $s .= $this->isCompleted() ? '(completed)': '(not complete)';
        return $s;
}

Réessayez le lancement de notre commande…

Bravo, vous avez une application PHP objet qui effectue des requêtes en base de données et affiche le résultat sur la console. La classe !

9. Ajout d’une interface EasyAdmin avec un contrôleurs CRUD pour Todo

Vous allez ajouter un tableau de bord d’administration fonctionnant grâce à EasyAdmin.

9.1. Ajout du contrôleur d’administration principal

Pour cela, déroulez les instructions suivantes (en vous référant à la documentation https://symfony.com/bundles/EasyAdminBundle/current/index.html pour plus de détails) :

  1. ajout du bundle EasyAdmin au projet, avec Composer :

    cd $HOME/CSC4101/projet/[nom-code]
    symfony composer req admin
    
  2. création du controleur du tableau de bord d’administration src/Controller/Admin/DashboardController.php :

    symfony console make:admin:dashboard
    
  3. nettoyez le cache du code précompilé de Symfony

    symfony console cache:clear
    
  4. lancez le serveur Web de l’environnement de développement Symfony :

    symfony server:start
    
  5. Testez dans le navigateur qu’une page « Welcome to EasyAdmin 4 » s’affiche bien sur l’URL /admin (sur http://locahost:8000/admin par exemple).

9.2. Ajout du contrôleur CRUD pour Todo

Vous allez ajouter au tableau de bord d’administration un premier contrôleur « CRUD » d’EasyAdmin qui servira à afficher et modifier les entités Todo.

  1. création du contrôleur de gestion CRUD des Todo src/Controller/Admin/TodoCrudController.php :

    symfony console make:admin:crud
    
  2. réalisez ensuite le « câblage » de TodoCrudController dans l’affichage de la page principale

    Modifiez le code de la méthode DashboardController::index() pour générer une redirection vers la route du CRUD de Todo :

    public function index(): Response
    {
        // Option 1. You can make your dashboard redirect to some common page of your backend
        //
        // 1.1) If you have enabled the "pretty URLs" feature:
        return $this->redirectToRoute('admin_todo_index');
    
        //[...]
    }
    

    Testez que ça fonctionne : le chargement de http://localhost:8000/admin/ doit rediriger vers le tableau de bord CRUD de Todo, sur http://localhost:8000/admin/todo

  3. Testez que ce tableau de bord CRUD fonctionne et que les modifications sont possibles en base de données pour des tâches.

10. Customisation de EasyAdmin dans ToDo

Une fois qu’on a effectué les opérations de base pour ajouter EasyAdmin dans le code, il pourrait être intéressant de modifier certains paramètres pour « customiser » l’affichage des Todo.

Pour cela, on devra probablement surcharger certains gabarits d’affichage.

Nous ne détaillons pas toutes les étapes éventuellement requises, car elles ont un intérêt padagogique limité à ce stade.

Pour info, voici un exemple de TodoCrudController::configureFields() avec quelques commentaires pour expliciter le rôle des différents paramètres.

On trouvera le code des gabarits qui n’a pas été reproduit ici dans les sources du projet Todo v2.* finalisé (dans templates/admin/).

public function configureFields(string $pageName): iterable
{
          return [
                  // Id shouldn't be modified
                  IdField::new('id')->hideOnForm(),
                  // Completed will be rendered as a toggle only in edit
                  BooleanField::new('completed')
                          ->onlyOnForms()
                          ->hideWhenCreating(),
                  // otherwise it may be displayed on index as a string (as an Integer since Text won't be able to convert bool to string)
                  IntegerField::new('completed')
                          ->onlyOnIndex()
                          ->formatValue(function ($value) {
                                  return ($value ? 'COMPLETED' : 'ACTIVE');
                          }),
                  // Title will be rendered so as to include a link, and be striked whenever completed
                  TextField::new('title')
                          ->setTemplatePath('admin/fields/todo_index_title.html.twig'),
                  DateField::new('created'),
                  DateField::new('updated'),
          ];
}

public function configureActions(Actions $actions): Actions
{
          // For whatever reason show isn't in the menu, bu default
          return $actions
                  ->add(Crud::PAGE_INDEX, Action::DETAIL)
          ;
}

public function configureCrud(Crud $crud): Crud
{
          // Customize the rendering of the row to grey-out the completed Todos
          return $crud
                  ->overrideTemplate('crud/index', 'admin/crud/todo_index.html.twig')
          ;
}

D’autres paramétrages un peu spéciaux ont dû être faits pour éviter certains problèmes de mise au point (ou contourner des bugs) :

Dans config/packages/framework.yaml, modifier la valeur de cookie_secure à false :

framework:
    # ...
    session:
        # ...

        # make EasyAdmin happy, otherwise CSRF fails for weird reason
        cookie_secure: false

Author: Olivier Berger (TSP)

Date: 2025-07-16 Wed 11:07

Emacs (Org mode)