1 / 10

🚀 AJAX avec Symfony

Affichage en temps réel de la disponibilité des ateliers

🎯 Objectif du tutoriel

Apprendre à créer une requête AJAX avec JavaScript (fetch, async/await) dans une application Symfony pour afficher en temps réel la disponibilité des ateliers, sans créer d'API séparée.

📋 Ce que nous allons voir :

  • Configuration côté Symfony (Route + Contrôleur)
  • JavaScript moderne avec fetch() et async/await
  • Interface utilisateur dynamique
  • Gestion des erreurs et optimisations

1Principe général

🌐 Frontend

JavaScript fait une requête AJAX

→

🔧 Symfony

Route retourne du JSON

→

🗄️ Base de données

Données de disponibilité

→

✨ Mise à jour

Interface actualisée

💡 Pourquoi cette approche ?

  • Temps réel : Les utilisateurs voient immédiatement les changements
  • Expérience utilisateur : Pas de rechargement de page
  • Simplicité : Pas besoin d'API séparée avec Symfony
  • Performance : Seules les données nécessaires sont transférées

2Côté Symfony - La Route

// Dans votre contrôleur AtelierController
#[Route('/ateliers/disponibilite', name: 'ateliers_disponibilite', methods: ['GET'])]
public function getDisponibilite(
    AtelierRepository $atelierRepository,
    ReservationRepository $reservationRepository
): JsonResponse {
    // Logique de traitement...
    return new JsonResponse([
        'success' => true,
        'ateliers' => $disponibilites,
        'timestamp' => date('Y-m-d H:i:s')
    ]);
}
            

🔑 Points importants :

  • JsonResponse : Retourne automatiquement du JSON avec les bons headers
  • GET uniquement : Simple lecture de données
  • Route dédiée : Séparée de l'affichage principal
  • Timestamp : Pour afficher l'heure de dernière mise à jour

3Calcul de la disponibilité

// Pour chaque atelier
foreach ($ateliers as $atelier) {
    // Compter les réservations existantes
    $reservationsCount = $reservationRepository->countByAtelier($atelier->getId());
    
    // Calculer les places disponibles
    $placesDisponibles = $atelier->getCapaciteMax() - $reservationsCount;
    
    $disponibilites[] = [
        'id' => $atelier->getId(),
        'nom' => $atelier->getNom(),
        'placesDisponibles' => max(0, $placesDisponibles),
        'complet' => $placesDisponibles <= 0,
    ];
}
            

📊 Structure des données retournées :

{
    "success": true,
    "ateliers": [
        {
            "id": 1,
            "nom": "Atelier Poterie",
            "placesDisponibles": 5,
            "complet": false
        }
    ],
    "timestamp": "2025-01-15 14:30:22"
}

4JavaScript - La requête fetch

// Fonction principale avec async/await
async function updateDisponibilite() {
    try {
        console.log('Mise à jour de la disponibilité...');
        
        // Requête AJAX avec fetch
        const response = await fetch('/ateliers/disponibilite', {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            }
        });
        
        // Vérifier la réponse
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        // Parser le JSON
        const data = await response.json();
        
        // Mettre à jour l'interface
        updateAteliersDisplay(data.ateliers);
        
    } catch (error) {
        console.error('Erreur:', error);
    }
}
            

🎯 Avantages de async/await :

  • Code plus lisible que les callbacks
  • Gestion d'erreurs avec try/catch
  • Syntax moderne et propre

5Mise à jour de l'interface

// Mettre à jour l'affichage pour chaque atelier
function updateAteliersDisplay(ateliers) {
    ateliers.forEach(atelier => {
        const atelierId = atelier.id;
        
        // Trouver les éléments dans le DOM
        const badge = document.querySelector(
            `.disponibilite-badge[data-atelier-id="${atelierId}"]`
        );
        
        // Mettre à jour le badge
        if (atelier.complet) {
            badge.textContent = 'Complet';
            badge.classList.add('badge-danger');
        } else {
            badge.textContent = `${atelier.placesDisponibles} places`;
            badge.classList.add('badge-success');
        }
    });
}
            

🎨 Interface dynamique :

  • Badges colorés : Vert (disponible), Orange (bientôt complet), Rouge (complet)
  • Barres de progression : Visualisation du taux de remplissage
  • Boutons adaptatifs : Désactivés automatiquement si complet

6Mise à jour automatique

// Configuration de l'intervalle
const REFRESH_INTERVAL = 5000; // 5 secondes
let updateInterval;

// Démarrer les mises à jour automatiques
function startAutoUpdate() {
    // Première mise à jour immédiate
    updateDisponibilite();
    
    // Puis mise à jour périodique
    updateInterval = setInterval(updateDisponibilite, REFRESH_INTERVAL);
}

// Optimisation : arrêter quand l'onglet n'est pas visible
document.addEventListener('visibilitychange', function() {
    if (document.visibilityState === 'visible') {
        startAutoUpdate();
    } else {
        clearInterval(updateInterval);
    }
});
            

⚡ Optimisations importantes :

  • Pause intelligente : Arrêt des requêtes quand l'onglet n'est pas visible
  • Première mise à jour : Immédiate au chargement de la page
  • Nettoyage : Arrêt propre avant de quitter la page

7Gestion des erreurs

async function updateDisponibilite() {
    try {
        const response = await fetch('/ateliers/disponibilite');
        
        // Vérifier le statut HTTP
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        const data = await response.json();
        
        // Vérifier la structure de la réponse
        if (data.success) {
            updateAteliersDisplay(data.ateliers);
            updateTimestamp(data.timestamp);
        } else {
            console.error('Erreur dans la réponse:', data);
        }
        
    } catch (error) {
        console.error('Erreur lors de la mise à jour:', error);
        updateTimestamp('Erreur de connexion');
    }
}
            

🛡️ Types d'erreurs gérées :

  • Erreurs réseau : Pas de connexion internet
  • Erreurs HTTP : 404, 500, etc.
  • Erreurs de parsing : JSON invalide
  • Erreurs logiques : Structure de réponse inattendue

8Optimisations côté base de données

// Dans ReservationRepository.php
public function countByAtelier(int $atelierId): int
{
    return $this->createQueryBuilder('r')
        ->select('COUNT(r.id)')
        ->where('r.atelier = :atelierId')
        ->andWhere('r.statut != :statut_annule')
        ->setParameter('atelierId', $atelierId)
        ->setParameter('statut_annule', 'annule')
        ->getQuery()
        ->getSingleScalarResult();
}

// Version optimisée pour tous les ateliers en une requête
public function getStatistiquesReservations(): array
{
    return $this->createQueryBuilder('r')
        ->select('a.id as atelier_id, COUNT(r.id) as nb_reservations')
        ->leftJoin('r.atelier', 'a')
        ->groupBy('a.id')
        ->getQuery()
        ->getResult();
}
            

🚀 Optimisations de performance :

  • Une seule requête : Pour tous les ateliers au lieu d'une par atelier
  • Exclusion des annulés : Compter seulement les réservations valides
  • Requête optimisée : COUNT au lieu de récupérer tous les objets

✅ Résumé et bonnes pratiques

✅ Avantages

  • Expérience utilisateur fluide
  • Données en temps réel
  • Pas de rechargement de page
  • Performance optimisée
  • Code maintenable

⚠️ Points d'attention

  • Gestion des erreurs réseau
  • Performance avec beaucoup d'utilisateurs
  • Cache navigateur
  • Accessibilité
  • SEO (contenu dynamique)

🎯 Prochaines étapes possibles :

  • WebSockets : Pour des mises à jour encore plus rapides
  • Cache Redis : Pour améliorer les performances
  • Pagination : Si beaucoup d'ateliers
  • Tests unitaires : Pour sécuriser le code
  • Notifications : Alerter quand un atelier devient disponible

🎉 Félicitations !

Vous maîtrisez maintenant AJAX avec Symfony !