Logos des technos utilisées

Gestion électronique de documents :

Lors de ma formation DWWM, j'ai éffectué un stage dans la société 6xpos qui développe et commercialise des Caisses enregistreuses, des Bornes et pads de commandes tactiles pour les restaurateurs. Pour faciliter l'échange de fichiers avec ses clients j'ai eu à réaliser une gestion électronique de documents.

Maquette graphique

Pour la réalisation de mon projet, on m'a donné une maquette graphique que j'ai étudié avec le donneur d'ordre afin d'avoir une idée précise de ce qui était attendu

  • Diapo01
  • Diapo02
  • Diapo03
  • Diapo04
  • Diapo05
  • Diapo06
  • Diapo07
  • Diapo08
  • Diapo09
  • Diapo10
  • Diapo11
  • Diapo12
  • Diapo13
  • Diapo14

La GED comprend :

Une page d'accueil, une section administrateur et 4 sections utilisateur.

On retrouve sur le site différents formulaires (connexion, changement de mot de passe, création de compte client, formulaires destinés à la transmission d'informations administratives) qui apparaissent sous forme de modales.

Un système de cartes dont l'apparence change si le document a été envoyé ou non (affichage des formats images, génération d'un logo récupérant l'extension du fichier si ce n'est pas un format d'image).

Au clic sur une carte vide, une modale unique d'upload se modifie en fonction du bouton cliqué ( les inputs, noms et tokens CSRF du formulaire ) et apparaît.

Au clic sur une carte remplie, on affiche un aperçu zoomable des formats images ou on télécharge les autres formats.

Des boutons pour le CRUD sont disponibles pour chaque cartes ( Variables en fonction du rôle de l'utilisateur ).

Un système de signature électronique qui enregistre une signature faite sur un canva en format image et l'ajoute à un document.

Des modales de confirmation sont affichée lors de la suppression d'éléments ou lors de la souscription à des éléments payants.

Des messages de succès ou d'erreur sont affichés lors des différentes actions.

Des requêtes en AJAX ont été implémentés afin de ne rafraîchir qu'une partie ciblée de la page et d'obtenir un rendu plus rapide et fluide.

Utilisation des media queries en CSS et Javascript pour adapter le site sur les différents types d'écrans.

L'affichage de certaines sections de l'utilisateur ne s'affichent que si certaines checkbox sont cochées dans le formulaire de création de compte client.

La création d'un compte client envoie un mail à l'adresse renseignée dans le formulaire afin que l'utilisateur puisse définir son mot de passe.

Architecture

Ayant choisi Symfony, mon projet suit une architecture MVC (Modèle, vue, contrôleur).
Une requête est envoyée à un contrôleur frontal (index.php) qui redirige vers la route demandée.
Les controleurs utilisent les modèles pour interagir avec la base de données et récupérer les données à transmettre à la vue.
La vue construit le html et l'envoi en réponse de la requête.

shcema architecture MVC

Base de données / Modèles

Dans la conception, j'ai commencé par réaliser la base de données pour poser les fondations de l'application, puis les contrôleurs et les vues.

Modèle conceptuel des données

Avec une table user, un utilisateur peut avoir une table company qui contient l'information des entreprises et tout le reste est lié à cette table company. En vert les tables qui correspondent aux différentes cartes acceptant les envois multiples (0,n - 1,1) en bleu les cartes n'acceptant qu'un seul exemplaire dans la table documents et les différents formulaires (0,1 - 1,1).

Entity

Une fois le MCD terminé, j'ai du construire ma base de données.
J'ai commencé par renseigner les informations de connexion dans le fichier .env.
Puis j'ai créé mes différentes classes Entity en m'aidant du terminal (php bin/console make:entity" puis on renseigne les différentes colonnes et leur types (un type relation est meme disponible pour configurer les clefs primaires et étrangères).
Cela nous donne un fichier préconstruit ou on retrouve différentes choses :
Des annotations pour Doctrine qui est l'ORM (Object-Relational Mapping) par défaut de symfony, l'ORM utilise ces annotations pour faire le lien entre une base de données et des objets utilisés par l'application. On peut voir des colonnes de la table ou des relations (ManyToOne / JoinColumn)
Chaque annotation doctrine est suivie par une propriété de la classe contenant le type de donnée attendue.
On peut également voir des annotations pour VichUploader que j'ai utilisé pour la gestion des fichiers uploadés avec un mapping qui défini le chemin vers le dossier ou doit être enregistré le fichier et qui contient une méthode de nommage de fichiers pour avoir des noms uniques, il est lié à une propriété avec fileNameProperty.
Des getters et setters sont également générés par la commande make:entity

<?php

namespace App\Entity;

use App\Repository\GraphicelementsRepository;
use Doctrine\ORM\Mapping as ORM;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Symfony\Component\HttpFoundation\File\File;

#[ORM\Entity(repositoryClass: GraphicelementsRepository::class)]
#[Vich\Uploadable]
class Graphicelements
{
     #[ORM\Id]
     #[ORM\GeneratedValue]
     #[ORM\Column]
     private ?int $id = null;

     #[ORM\Column(length: 255, nullable: true)]
     private ?string $graphicelement = null;

     #[Vich\UploadableField(mapping: 'graphicelements', fileNameProperty: 'graphicelement')]
     private ?File $graphicelementFile =null;

     #[ORM\Column(length: 32, nullable: true)]
     private ?string $type = null;

     #[ORM\ManyToOne(inversedBy: 'graphicelements')]
     #[ORM\JoinColumn(nullable: false)]
     private ?Company $company = null;

     #[ORM\Column]
     private ?\DateTimeImmutable $created_at = null;

     #[ORM\Column(nullable: true)]
     private ?\DateTimeImmutable $updated_at = null;

     public function getId(): ?int
     {
     return $this->id;
     }

     public function getGraphicelement(): ?string
     {
     return $this->graphicelement;
     }

     public function setGraphicelement(?string $graphic_element): static
     {
     $this->graphicelement = $graphic_element;

     return $this;
     }

     public function getType(): ?string
     {
     return $this->type;
     }

     public function setType(?string $type): static
     {
     $this->type = $type;

     return $this;
     }

     public function getCompany(): ?Company
     {
     return $this->company;
     }

     public function setCompany(?Company $company): static
     {
     $this->company = $company;
    

     return $this;
     }

     public function getCreatedAt(): ?\DateTimeImmutable
     {
     return $this->created_at;
     }

     public function setCreatedAt(\DateTimeImmutable $created_at): static
     {
     $this->created_at = $created_at;

     return $this;
     }

     public function getUpdatedAt(): ?\DateTimeImmutable
     {
     return $this->updated_at;
     }

     public function setUpdatedAt(?\DateTimeImmutable $updated_at): static
     {
     $this->updated_at = $updated_at;

     return $this;
     }

     public function getGraphicelementFile()
     {
     return $this->graphicelementFile;
     }

     public function setGraphicelementFile($graphicelementFile)
     {
     $this->graphicelementFile = $graphicelementFile;
     return $this;
     }
}

Migrations

Une fois les entités construites, j'ai effectué des migrations avec différentes commandes
php/bin console make:migrations
Cela créé un fichier migration ou on retrouve des requètes SQL pour créer les différentes tables, colonnes et relations en base de données.
Ici par exemple une migration pour la table 'checklist' avec CREATE TABLE 'checklist' et la liste de toutes les colonnes, leur longueur et leur type et la création de la clef étrangère vers la table documents.
Une fois qu'on à vérifié le contenu de la migration, on utilise php bin/console doctrine:migrations:migrate pour appliquer les changements en base de données

<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * Auto-generated Migration: Please modify to your needs !
 */
final class Version20250115125720 extends AbstractMigration
{
    public function getDescription(): string
    {
        return '';
    {

    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE TABLE checklist (
        id INT AUTO_INCREMENT NOT NULL,
        documents_id INT NOT NULL,
        commercial VARCHAR(128) DEFAULT NULL,
        client_number VARCHAR(128) DEFAULT NULL,
        radio_choice VARCHAR(128) DEFAULT NULL,
        designation VARCHAR(255) DEFAULT NULL,
        type VARCHAR(128) DEFAULT NULL,
        company_name VARCHAR(255) DEFAULT NULL,
        siret VARCHAR(128) DEFAULT NULL,
        acquisition_siret VARCHAR(128) DEFAULT NULL,
        wanted_back_office_url VARCHAR(128) DEFAULT NULL,
        wanted_password VARCHAR(128) DEFAULT NULL,
        manager_names VARCHAR(255) DEFAULT NULL,
        manager_birthday DATE DEFAULT NULL,
        naf_ape VARCHAR(64) DEFAULT NULL,
        rcs VARCHAR(64) DEFAULT NULL,
        tva_number VARCHAR(32) DEFAULT NULL,
        social_capital VARCHAR(32) DEFAULT NULL,
        immatriculation_code VARCHAR(64) DEFAULT NULL,
        greffe_city VARCHAR(128) DEFAULT NULL,
        legal_status VARCHAR(64) DEFAULT NULL,
        phone VARCHAR(16) DEFAULT NULL,
        mail VARCHAR(320) DEFAULT NULL,
        delivery_adress VARCHAR(255) DEFAULT NULL,
        shop_phone VARCHAR(16) DEFAULT NULL,
        wanted_installation_date DATE DEFAULT NULL,
        created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
        updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
        UNIQUE INDEX UNIQ_5C696D2F5F0F2752 (documents_id),
        PRIMARY KEY(id) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
        $this->addSql('ALTER TABLE checklist ADD CONSTRAINT FK_5C696D2F5F0F2752
        FOREIGN KEY (documents_id) REFERENCES documents (id)');
    }

    public function down(Schema $schema): void
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->addSql('ALTER TABLE checklist DROP FOREIGN KEY FK_5C696D2F5F0F2752');
        $this->addSql('DROP TABLE checklist');
    }
}

Repository

La commande php bin/console make:entity crée aussi des fichiers Repository ou on peut créer des requêtes personnalisées pour accéder aux données comme par exemple ici une requête pour récupérer les espaces clients par date de création décroissant ou une requête préparée utilisée dans la barre de recherche pour filtrer les noms des entreprises.

<?php

namespace App\Repository;

use App\Entity\Company;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository
 */
class CompanyRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Company::class);
    }

    public function findAllOrderedByCreatedAt(): array
{
    return $this->createQueryBuilder('c')
        ->orderBy('c.created_at', 'DESC')
        ->getQuery()
        ->getResult()
    ;
}

    public function findBySearch(string $searchTerm): array
    {
        $qb = $this->createQueryBuilder('c');
        $qb->where('c.name LIKE :searchTerm OR c.designation LIKE :searchTerm')
        ->setParameter('searchTerm', '%' . $searchTerm . '%');

        return $qb->getQuery()->getResult();
    }
}

Contrôleurs

Une fois la base de donnée et le modèle créé, j'ai fait les controleurs avec la commande php/bin console make:controller.
Dans le controlleur, on retrouve des annotations pour des composants de symfony comme les routes pour définir les url directement dans les controleur, elles comprennent également un nom utilisé pour générer dynamiquement les url avec path() dans twig.
IsGranted pour gérer les permissions et limiter les acces à des routes pour certains utilisateurs.
Dans le corps on crée le traitement des formulaires et on récupère les données nécessaire à transmettre à la vue.

<?php

namespace App\Controller;

use Symfony\Bundle\SecurityBundle\Security;
use App\Repository\CompanyRepository;
use Symfony\Bundle\FrameworkBundle\ControllerAbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\AttributeIsGranted;

class UserHomeController extends AbstractController
{
    #[Route('/accueil', name: 'app_home_user')]
    #[IsGranted('ROLE_USER')]
    public function showClientAccount(
        Security $security,
        CompanyRepository $companyRepository,
    ) {
        $user = $security->getUser();

        /** @var User $user */
        $company = $user->getCompany();
        if (!$company) {
            throw $this->createNotFoundException('Company not found for user');
        }

        return $this->render('company/account.html.twig', [
            'company' => $company,
            'userRole' => $this->getUser()->getRoles()[0]
        ]);
    }
}

Vues

Une fois les données envoyés aux templates (ici avec twig) on met en forme le html avant d'envoyer la réponse.

Twig

Twig utilise un système d'héritage et de blocks pour permettre aux autres templates de réutiliser et personnaliser un template.
Ici, le base.html.twig contient la partie head du html et les importations de styles et de scripts.

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <link rel="icon" href="https://6xpos.fr/wp-content/uploads/2024/07/cropped-6xpos-favicon-192x192.webp" sizes="192x192" />
        <title>{% block title %}Welcome!{% endblock %}</title>

        {% block stylesheets %}
            {{ encore_entry_link_tags('app') }}
        {% endblock %}
    </head>
    <body>
        <div class="flashcontainer">
            {{ include('flash.html.twig') }}
        </div>

        {% block body %}

        {% endblock %}

        {% block javascript %}
            {{ encore_entry_script_tags('app') }}
        {% endblock %}
    </body>
</html>

Page d'accueil

Ici on récupère le template de base avec 'extends' et on remplace les différents blocs (title et body).
On peut voir l'utilisation de if/else pour changer les routes des boutons en fonction de l'utilisateur connecté, ou l'affichage conditionnel de la section plateforme de livraisons.

{% extends 'base.html.twig' %}

{% block title %}
    {{ company.designation }}
    | 6Xpos
{% endblock %}

{% block body %}
<main>
    {{ include('navigation.html.twig') }}
    <div class="content">
        <div class="content-title content-title-home">
            <h2>{{ company.designation }}</h2>
            <h3>{{ company.address }}</h3>
        </div>
        <div class="content-title content-title-home-mobile hidden">
            <h3>Tous les documents et informations sont destinés à nos équipes et resteront strictement confidentiels.</h3>
        </div>
        <div class="content-main">
            <div class="content-main-left">
                <div class="content-main-left-cards">

                    <div class="content-main-left-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <h3>Administratif</h3>
                        <p>Documents et informations liés à la validation de votre commande.</p>
                        {% if userRole == 'ROLE_ADMIN' %}
                        <a href="{{ path('app_admin_user_account_administratif', { 'id': company.id }) }}>
                        {% else %}
                        <a href="{{ path('app_home_user_administratif') }}"
                        {% endif %}
                        <div class="content-main-left-card-btn">COMMENCER</div>
                        </a>
                    </div>

                    <div class="content-main-left-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <h3>Carte/Menu établissement</h3>
                        <p>Retrouvez les éléments relatifs pour la construction de votre carte.</p>
                        {% if userRole == 'ROLE_ADMIN' %}
                        <a href="{{ path('app_admin_user_account_menu', { 'id': company.id }) }}>
                        {% else %}
                        <a href="{{ path('app_home_user_menu') }}"
                        {% endif %}
                        <div class="content-main-left-card-btn">DÉCOUVRIR</div>
                        </a>
                    </div>

                    <div class="content-main-left-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <h3>Éléments graphiques</h3>
                        <p>Déposez le logo, la charte graphique, photos produit, vidéos, etc...</p>
                        {% if userRole == 'ROLE_ADMIN' %}
                        <a href="{{ path('app_admin_user_account_graphisme', { 'id': company.id }) }}>
                        {% else %}
                        <a href="{{ path('app_home_user_graphisme') }}"
                        {% endif %}
                        <div class="content-main-left-card-btn">COMMENCER</div>
                        </a>
                    </div>

                    {% if "Uber Eats" in company.settings or "Deliveroo in company.settings or "Click&Collect" in company.settings %}
                        <div class="content-main-left-card">
                            <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            {# path svg #}
                            </svg>
                            <h3>Plateforme de livraison</h3>
                            <p>Renseignez les informations nécessaires à l'intégration des plateformes de livraison sur notre systeme.</p>
                            {% if userRole == 'ROLE_ADMIN' %}
                            <a href="{{ path('app_admin_user_account_livraison', { 'id': company.id }) }}>
                            {% else %}
                            <a href="{{ path('app_home_user_livraison') }}"
                            {% endif %}
                            <div class="content-main-left-card-btn">COMMENCER</div>
                            </a>
                        </div>
                    {% endif %}

                </div>
                <p class="content-main-left-disclaimer">Tous les documents et informations sont destinés à nos équipes et resteront strictement confidentiels.</p>
                <div class="empty"></div>
            </div>

            <div class="content-main-right">
                <h2>Bienvenue,</h2>
                <h3>Bienvenue sur votre espace administratif et de partage de fichier pour faciliter votre future installation.</h3>
                <div class="content-main-right-cards">
                    <div class="content-main-right-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <p>Dans cet espace qui vous est entièrement destiné, vous pourrez finaliser votre commande en complétant et déposant les documents nécessaires à la création de votre dossier.</p>
                    </div>

                    <div class="content-main-right-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <p>Télécharger puis déposer le fichier CSV de référence pour la constitution de votre base article.</p>
                    </div>

                    <div class="content-main-right-card">
                        <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        {# path svg #}
                        </svg>
                        <p>Vous devez également compléter les documents et déposer les éléments graphiques pour la réalisation de votre interface caisse (logo) et celle de votre borne de commande (logo, charte graphique, photos produit, etc...).</p>
                    </div>

                    {% if "Uber Eats" in company.settings or "Deliveroo in company.settings or "Click&Collect" in company.settings %}
                        <div class="content-main-right-card">
                            <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            {# path svg #}
                            </svg>
                            <p>Vous devez également compléter les documents et déposer les éléments graphiques pour la réalisation de votre interface caisse (logo) et celle de votre borne de commande (logo, charte graphique, photos produit, etc...).</p>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
{% endblock %}

CRUD

Donc à ce stade, il y a un affichage dynamique en fonction de l'utilisateur, maintenant il faut implémenter un système de CRUD.

Les FormTypes

Symfony propose une commande php/bin console make:form, qui doit être lié à une entité, il permet de préconstruire un formulaire en définissant le type d'input , les labels, les placeholders ou les classes pour chaque paramètre de l'entité.

<?php

namespace App\Form;

use App\Entity\User;
use App\Entity\Company;
use App\Entity\Checklist;
use App\Entity\Documents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class ChecklistType extends AbstractType
{
    public function buildForm(FormbuilderInterface $builder, array options): void
    {
        $builder
            ->add('commercial', ChoiceType::class, [
                'choices' => [
                    'Jean-Marc Doe' => 'Jean-Marc Doe',
                    'Julien Doe' => 'Julien Doe',
                    'Stéphane' => 'Stéphane',
                    'Thomas Doe' => 'Thomas Doe',
                ],
                'placeholder' => 'Sélectionnez un(e) commercial(e)',
                'required' => false,
                'multiple' => false,
                'label' => false,
                'attr' => [
                    'class' => 'company-form-select',
                ]
            ])
            ->add('client_number', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'N° Client'
                ]
            ])
            ->add('radio_choice', ChoiceType::class, [
                'choices' => [
                    'Matériel supplémentaire' => 'Matériel supplémentaire',
                    'Création' => 'Création',
                    'Remplacement matériel' => 'Remplacement matériel',
                    'Reprise' => 'Reprise',
                    'Nouveau PDV back-office' => 'Nouveau PDV back-office'
                ],
                'placeholder' => false,
                'expanded' => true,
                'multiple' => false,
                'label' => false,
                'required' => false,
            ])
            ->add('siret', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Siret'
                ]
            ])
            ->add('acquisition_siret', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Siret/Société'
                ]
            ])
            ->add('wanted_back_office_url', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Url back-office souhaité'
                ]
            ])
            ->add('wanted_password', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Mot de passe souhaité'
                ]
            ])
            ->add('manager_names', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Nom prénom du gérant'
                ]
            ])
            ->add('manager_birthday', DateType::class, [
                'widget' => 'single_text',
                'html5' => false,
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-date',
                    'placeholder' => 'Date de naissance',
                    'autocomplete' => 'off'
                ]
            ])
            ->add('naf_ape', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Code NAF ou APE'
                ]
            ])
            ->add('rcs', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'RCS'
                ]
            ])
            ->add('tva_number', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'TVA intracommunautaire'
                ]
            ])
            ->add('social_capital', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Capital social'
                ]
            ])
            ->add('immatriculation_code', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Code immatriculation'
                ]
            ])
            ->add('greffe_city', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Ville du greffe'
                ]
            ])
            ->add('legal_status', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Forme juridique ( SAS, SARL, etc...)'
                ]
            ])
            ->add('phone', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Téléphone'
                ]
            ])
            ->add('mail', EmailType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Email'
                ]
            ])
            ->add('delivery_adress', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Adresse postale de livraison'
                ]
            ])
            ->add('shop_phone', TextType::class, [
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-input',
                    'placeholder' => 'Téléphone du PDV'
                ]
            ])
            ->add('wanted_installation_date', DateType::class, [
                'widget' => 'single_text',
                'html5' => false,
                'label' => false,
                'required' => false,
                'attr' => [
                    'class' => 'company-form-date',
                    'placeholder' => 'Date d\'installation souhaitée,
                    'autocomplete' => 'off'
                ]
            ])
            ->add('save', SubmitType::class, [
                'label' => 'ENREGISTRER',
                'attr' => ['class' => 'checklist-form-submit']
            ]);
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Checklist::class,
        ]);
    }
}

Dans Twig

On peut juste écrire form_start et form_end pour générer le html du formulaire ou on peut lister tous les éléments voulus avec form_row ou form_widget afin de personnaliser d'avantage le rendu comme ici le rajout d'icone en svg ou l'affichage d'un input uniquement si un bouton radio à été coché.

<div class="checklist-modal hidden modal-container-layout">
    <div class="modal-full">
        <div class="close-cross-bg-edit close-cross-checklist-modal">
            <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            {# path svg #}
            </svg>
        </div >
        <div class="checklist-modal-container">
            <h2 class="sign-modal-title">Checklist</h2>
            {{ form_start(checklistform) }}
                <div class="checklist-form-input-container">
                    {{ form_widget(checklistform.commercial) }}
                    <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    {# path svg #}
                    </svg>
                </div >
                {{ form_widget(checklistform.client_number) }}
                {{ form_widget(checklistform.radio_choice) }}
                {{ form_widget(checklistform.siret) }}
                {% if company.type_operation is defined and "Reprise" in company.type_operation %}
                    {{ form_widget(checklistform.acquisition_siret) }}
                {% else %}
                    <div class="hidden">
                    {{ form_widget(checklistform.acquisition_siret) }}
                    </div >
                {% endif %}
                {{ form_widget(checklistform.wanted_back_office_url) }}
                {{ form_widget(checklistform.wanted_password) }}
                {{ form_widget(checklistform.manager_names) }}
                <div class="checklist-form-date-container">
                    {{ form_widget(checklistform.manager_birthday) }}
                    <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    {# path svg #}
                    </svg>
                </div >
                {{ form_widget(checklistform.naf_ape) }}
                {{ form_widget(checklistform.rcs) }}
                {{ form_widget(checklistform.tva_number) }}
                {{ form_widget(checklistform.social_capital) }}
                {{ form_widget(checklistform.immatriculation_code) }}
                {{ form_widget(checklistform.greffe_city) }}
                {{ form_widget(checklistform.legal_status) }}
                {{ form_widget(checklistform.phone) }}
                {{ form_widget(checklistform.mail) }}
                {{ form_widget(checklistform.delivery_adress) }}
                {{ form_widget(checklistform.shop_phone) }}
                <div class="checklist-form-date-container">
                    {{ form_widget(checklistform.wanted_installation_date) }}
                    <svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    {# path svg #}
                    </svg>
                </div >
                {{ form_widget(checklistform.save) }}
            {{ form_end(checklistform) }}
        </div >
        <div class="logo-sign-modal-layout">
            <div class="logo-checklist-modal">
                <img src="{{ asset('/build/img/6xpos_logo.svg') }}" img alt="logo 6xpos">
            </div >
        </div >
    </div >
</div >

Dans le contrôleur

On crée le formulaire avec $this→createForm en activant la protection CSRF.
handleRequest récupère les données soumises par le formulaire.
isSubmitted and isValid vérifie si le formulaire est envoyé et vérifie le token csrf.
L'EntityManager vérifie les changements et mets à jour la base de données avec persist() et flush().
Une methode vérifie si le formulaire est complet et les données mises à jour sont renvoyées à la vue.

<?php

namespace App\Controller;

use Datetime;
use App\Form\ChecklistType;
use App\Repository\CompanyRepository;
use App\Repository\ChecklistRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\AttributeIsGranted;
use Symfony\Bundle\FrameworkBundle\ControllerAbstractController;

class AdminAdministratifController extends AbstractController
{
    #[Route('/admin/client/{id}/administratif', name: 'app_admin_user_administratif')]
    #[IsGranted('ROLE_ADMIN')]
    public function showClientAccountAdministratif(
        int $id,
        EntityManagerInterface $entityManager,
        Request $request,
        CompanyRepository $companyRepository,
        ChecklistRepository $checklistRepository,
    ) {
        $company = $companyRepository->find($id);

        if (! $company) {
            throw $this->createNotFoundException('Company not found for id ' . $id);
        }

        $documents = $company->getDocuments();
        $checklist = $documents->getChecklist();

        $checklistform = $this->createForm(ChecklistType::class, $checklist, [
            'csrf_protection' => true,
        ]);

        $checklistform->handleRequest($request);

        if ($checklistform->isSubmitted() && $checklistform->isValid()) {
            $checklist->setUpdatedAt(new \DateTimeImmutable());

            $entityManager->persist($checklist);
            $entityManager->flush();
            $this->addFlash('success', 'Le formulaire a bien été modifié.');
        }

        $ischecklistcompleted = $checklistRepository->areAllColumnsFilled($company->getId());

        return $this->render('company/administratif.html.twig', [
            'checklistform' => $checklistform->createView(),
            'company' => $company,
            'userRole' => $this->getUser()->getRoles()[0],
            'documents' => $documents,
            'checklist' => $checklist,
            'ischecklistcompleted' => $ischecklistcompleted,
        ]);
    }
}

Mentions légales

Éditeur du site

Nom : Antoine RIGOLE
Contact : antoine.rigole@live.fr

Hébergeur du site

Infomaniak Network SA
Adresse : Rue Eugène-Marziano 25, 1227 Genève, Suisse
Téléphone : +41 22 820 35 44
Site : https://www.infomaniak.com

Propriété intellectuelle

Les images et contenus présents sur ce site sont la propriété exclusive de Antoine RIGOLE. Toute reproduction, diffusion, ou utilisation sans autorisation est interdite.

Données personnelles

Les informations recueillies via le formulaire de contact sont uniquement utilisées pour répondre à vos demandes. Elles ne sont pas conservées après réponse. Conformément à la loi "Informatique et Libertés", vous pouvez exercer votre droit d'accès, de rectification ou de suppression des données vous concernant en me contactant par email.

Cookies

Ce site utilise Google Analytics pour analyser l'audience et améliorer l'expérience utilisateur. Vous pouvez gérer vos préférences de cookies directement dans les paramètres de votre navigateur.