Aller au contenu
Romain Giaretti
Symfo7

Produire et contrôler une app Symfony 7.4.

Bannière Symfo7, Concevoir avec Symfony, sur fond de panneaux noirs

v4.0 · première version

Symfo7 est un cadre personnel : CLI PHP, skill Olivia, huit agents. Ce n’est pas Symfony. La revue humaine reste obligatoire.

Comment ça marche

  1. Besoin

    Un cahier des charges unique. Nadia en fait un contrat métier.

  2. Conception

    Olivia propose 2 univers. GO A ou B. Puis le squelette Symfony 7.4.

  3. Production et contrôles

    Un agent à la fois. Portes qualité. Deux échecs sur le même défaut : stop.

Portes de l’usine

syntaxe · PHPUnit · PHPStan · CSRF · limiteur de connexion · 3 cas ROLE_ · scan sécu · attaques machine · accessibilité 390 / 820 / 1440

« En ligne » et « sûr en ligne » sont deux portes séparées. Horizon n’a pas « sûr en ligne ».

Détail technique

Huit agents et le pipeline

Olivia pilote. Elle ne code pas. L’équipe est figée à huit prénoms.

Schéma Symfo7 v4 : cahier des charges, Olivia, huit agents, Symfony 7.4
  • Olivia

    Chef. Analyse, lots, DESIGN (2 univers, GO A ou B). Ne code pas.

  • Nadia

    Contrat métier validé.

  • Étienne

    Doctrine, entités, migrations, jeu de données.

  • Paul

    Backend Symfony : contrôleurs, services, formulaires.

  • Chloé

    Fondation visuelle puis habillage de lot. Twig, CSS, accessibilité.

  • Antoine

    Contrôle du backend de Paul.

  • Inès

    Contrôle des vues, WCAG, responsive 390 / 820 / 1440.

  • Diane

    Audit sécurité.

Olivia → Nadia → Olivia (packs) → GO A ou B → création du projet → Chloé fondation → Étienne → Paul → Antoine → Chloé lot → Inès → Diane → Olivia.

Site test

Horizon Voyages

Site de démonstration. Aucun voyage n’est vendu. Il sert à tester l’usine Symfo7. Volontairement incomplet.

Le test : un workflow tenu, de la sécurité appliquée, un code relisible. Pas un produit client.

Le but n’était pas un back-office complet. L’admin gère séjours, réservations et messages. Les clients sont en lecture seule.

Sur la connexion, deux boutons « Tester en tant que client » et « Tester en tant qu’administrateur » existent pour un recruteur : un clic, sans mot de passe à recopier. POST + jeton CSRF. Ce n’est pas un oubli de sécurité.

Accueil Horizon Voyages, bandeau site de démonstration

Site test

Horizon Voyages

Agence fictive. Catalogue, compte, réservation sans paiement. Connexion test en un clic.

Ouvrir le site test

Captures du site

  • Accueil Horizon Voyages, bandeau site de démonstration
    Accueil. Le bandeau dit que rien n’est vendu.
  • Fiche séjour Lisbonne et formulaire de demande de date
    Fiche séjour et demande de réservation. Aucune somme n’est encaissée.
  • Tableau de bord administrateur Horizon Voyages, volontairement limité
    Tableau de bord admin, volontairement limité.

Extraits de code

QUALITY_POLICY vient de l’usine Symfo7. Les trois autres extraits viennent d’Horizon. Chaque bloc est continu. Le reste du fichier n’est pas recopié.

docs/QUALITY_POLICY.md · usine Symfo7

Porte qualité de l’usine. Un contrôle non lancé n’est pas un succès.

## LOT

Après une fonction cohérente : `php -l`, PHPUnit, PHPStan, `lint:container --resolve-env-vars`, `lint:yaml`, `lint:twig --show-deprecations`, `composer validate --strict`, `composer audit`, `doctrine:schema:validate` (mapping et sync), `doctrine:migrations:up-to-date` si base. Scan sécu : SQL concaténé, `$_GET`/`$_POST`, hasher MD5/SHA1, suppression en GET, `|raw`, formulaire POST sans CSRF, login sans limiteur, formulaire public sans rate limiter dans le contrôleur, log de secret, upload nu, `public/_maquette`. Chaque route contrôleur doit apparaître dans une méthode de test (`request`, `crawler` ou `assert`). Un commentaire qui cite l’URL ne compte pas. `access_control` avec ROLE_ exige les 3 cas **sur cette zone** (visiteur → connexion ou 401, connecté sans le rôle → 403, rôle requis → accès). Un 403 ailleurs ne compte pas. Pas 3 tests par URL de la zone. Entité sans fichier de migration = FAIL. S’il y a des écrans : attaques machine (XSS, CSRF, page interdite, IDOR non connecté) sur `local_url` ; injoignable = BLOCKED. Étienne, Paul et Antoine ne peuvent pas écrire PASS sans cette preuve.

config/packages/security.yaml

CSRF, limite de tentatives, rôles admin et client.

            form_login:
                login_path: app_connexion
                check_path: app_connexion
                enable_csrf: true
                username_parameter: _username
                password_parameter: _password
                success_handler: App\Security\ConnexionSuccessHandler
            login_throttling:
                max_attempts: 5
                interval: '1 minute'
            logout:
                path: app_logout
                target: app_accueil
                enable_csrf: true
                csrf_parameter: _csrf_token
                csrf_token_id: logout

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/mes-reservations, roles: ROLE_CLIENT }
        - { path: ^/mon-compte, roles: ROLE_CLIENT }

src/Service/ReservationService.php

La règle métier est dans le service, pas dans le contrôleur.

public function creer(Utilisateur $client, int $dateDepartId, int $nombrePersonnes): Reservation
{
    if ($nombrePersonnes < 1) {
        throw new ExceptionMetier('Indiquez un nombre de personnes.');
    }

    $dateDepart = $this->dateDepartRepository->find($dateDepartId);
    if (!$dateDepart instanceof DateDepart) {
        throw new ExceptionMetier('Cette date de départ n’existe pas.');
    }

    $sejour = $dateDepart->getSejour();
    if (!$sejour->isVisible()) {
        throw new ExceptionMetier('Ce séjour n’est plus ouvert à la réservation.');
    }

    $aujourdHui = new DateTimeImmutable('today');
    if ($dateDepart->getDepartLe() < $aujourdHui) {
        throw new ExceptionMetier('Cette date de départ est déjà passée.');
    }

    if ($dateDepart->getPlaces() < $nombrePersonnes) {
        throw new ExceptionMetier('Il n’y a plus assez de places pour cette date.');
    }

    $reservation = (new Reservation())
        ->setClient($client)
        ->setDateDepart($dateDepart)
        ->setNombrePersonnes($nombrePersonnes)
        ->setMontantTotal($sejour->getPrixParPersonne() * $nombrePersonnes)
        ->setStatut(StatutReservation::EnAttente);

    $this->entityManager->persist($reservation);
    $this->entityManager->flush();

    return $reservation;
}

tests/Functional/DroitsTest.php

Sans compte : page perso ou admin → connexion. Client sans droit admin → 403.

public function testVisiteurEstRenvoyeVersConnexionPourLeCompte(): void
{
    $client = $this->clientAvecDemo();
    $client->request('GET', '/mes-reservations');
    self::assertResponseRedirects('/connexion');

    $client->request('GET', '/mon-compte');
    self::assertResponseRedirects('/connexion');
}

public function testVisiteurRefuseAdminVersConnexion(): void
{
    $client = $this->clientAvecDemo();
    $client->request('GET', '/admin');
    self::assertResponseRedirects('/connexion');
}

public function testClientRefuseAdminVoitPageInterdite(): void
{
    $client = $this->clientAvecDemo();
    $client->loginUser($this->utilisateur(DemoDataset::CLIENT_EMAIL));
    $client->request('GET', '/admin');
    self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
    self::assertSelectorTextContains('h1', 'Cette action n’a pas abouti.');
}

Site test pour l’usine. Pas un CRUD admin complet. Horizon n’a pas la porte « sûr en ligne ».

Contact

Un stage ou un projet à me proposer ?