Aller au contenu

Étape 8 - Développement Custom

⏱️ Durée estimée : 6h

À la fin de cette étape, vous serez capable de :

  • Créer un module Drupal custom professionnel et maintenable
  • Définir des routes et controllers selon les standards Drupal
  • Maîtriser l’injection de dépendances et le container Symfony
  • Créer des services métier réutilisables et testables
  • Intégrer htmx pour des interactions dynamiques sans JavaScript complexe
  • Implémenter Stripe Checkout en mode sécurisé
  • Appliquer les bonnes pratiques de développement Drupal moderne
  • Étapes 1-7 terminées : Structure Drupal, contenus, thème
  • PHP orienté objet : Classes, interfaces, namespaces
  • PSR-4 autoloading : Organisation des classes
  • Notions de design patterns : Dependency Injection, Service Container
  • HTTP/REST : Méthodes GET, POST, PATCH, DELETE
  • Compte Stripe : Créer un compte test gratuit
  • Éditeur avec autocomplétion : PHPStorm, VS Code + PHP Intelephense
  • Xdebug (recommandé) : Pour debugger le code PHP
Fenêtre de terminal
# Vérifier Xdebug
ddev exec php -v | grep Xdebug
# Activer si nécessaire
ddev config --xdebug-enabled
ddev restart

1. Structure d'un module

Architecture et fichiers de base d’un module custom. Voir →

2. Routes & Controllers

Créer des pages personnalisées avec routing Drupal. Voir →

3. Services & DI

Injection de dépendances et services métier. Voir →

4. Form API

Créer des formulaires programmatiques sécurisés. Voir →

5. htmx & AJAX

Interactions dynamiques sans JavaScript complexe. Voir →

6. Stripe Checkout

Paiement sécurisé avec l’API Stripe. Voir →

TD 1 : Panier & Services

Atelier pratique - Module Cart et injections. Faire le TD →

TD 2 : Config & Search

Atelier pratique - Store Info et Search personnalisés. Faire le TD →

Nous allons créer le module tailstore_cart qui gère :

  • Le panier côté serveur
  • L’API pour htmx
  • Le checkout Stripe
modules/custom/tailstore_cart/
├── tailstore_cart.info.yml # Déclaration du module
├── tailstore_cart.module # Hooks et fonctions
├── tailstore_cart.services.yml # Déclaration des services
├── tailstore_cart.routing.yml # Routes
├── tailstore_cart.permissions.yml # Permissions
├── tailstore_cart.libraries.yml # Assets
├── src/
│ ├── Controller/
│ │ ├── CartController.php # API panier (htmx)
│ │ └── CheckoutController.php # Stripe checkout
│ │
│ ├── Service/
│ │ ├── CartService.php # Logique panier
│ │ └── StripeService.php # API Stripe
│ │
│ ├── Form/
│ │ ├── CartForm.php # Formulaire panier
│ │ └── CheckoutForm.php # Formulaire checkout
│ │
│ ├── EventSubscriber/
│ │ └── CartEventSubscriber.php # Events
│ │
│ └── Plugin/
│ └── Block/
│ └── MiniCartBlock.php # Bloc mini-panier
├── templates/
│ ├── cart-page.html.twig
│ ├── cart-item.html.twig
│ └── mini-cart.html.twig
└── config/
└── install/
└── tailstore_cart.settings.yml

Drupal utilise le standard PSR-4 pour le chargement automatique des classes :

Namespace: Drupal\tailstore_cart\Controller\CartController
Fichier: modules/custom/tailstore_cart/src/Controller/CartController.php

Règles de nommage :

  • Namespace racine : Drupal\[nom_module]
  • Dossier src/ : Point d’entrée du namespace
  • Sous-dossiers : Ajoutés au namespace
  • Nom de fichier : Identique au nom de la classe

Pattern central dans Drupal moderne (depuis Drupal 8) :

class CartController extends ControllerBase {
/**
* Constructor avec dépendances injectées.
*
* @param CartService $cartService
* Service de gestion du panier.
* @param EntityTypeManagerInterface $entityTypeManager
* Gestionnaire d'entités pour charger les produits.
*/
public function __construct(
private readonly CartService $cartService,
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* {@inheritdoc}
*
* Méthode statique appelée par le container.
*/
public static function create(ContainerInterface $container): static {
return new static(
$container->get('tailstore_cart.cart'),
$container->get('entity_type.manager'),
);
}
}

Avantages :

  • Testabilité : Facile de mocker les dépendances
  • Flexibilité : Changement d’implémentation sans toucher au controller
  • Clarté : Dépendances explicites
  • Réutilisabilité : Services partagés entre modules

Drupal utilise le Service Container de Symfony :

# tailstore_cart.services.yml
services:
# Déclaration du service
tailstore_cart.cart:
class: Drupal\tailstore_cart\Service\CartService
arguments:
- '@request_stack' # Service injecté
- '@entity_type.manager' # Service injecté
# Alias avec interface (recommandé)
Drupal\tailstore_cart\Service\CartServiceInterface: '@tailstore_cart.cart'

Conventions de nommage :

  • Format : [module].[nom_service]
  • Exemples : tailstore_cart.cart, tailstore_cart.stripe
  • Services core : Point au début (ex: .entity_type.manager)

htmx permet d’ajouter des comportements AJAX directement dans le HTML :

<button
hx-post="/cart/add/42"
hx-target="#cart-count"
hx-swap="innerHTML"
>
Ajouter au panier
</button>

Mode Redirect simplifié :

  1. L’utilisateur clique sur “Commander”
  2. Drupal crée une session Stripe
  3. Redirection vers la page Stripe hosted
  4. Stripe gère le paiement
  5. Retour sur le site avec confirmation
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => $items,
'mode' => 'payment',
'success_url' => $successUrl,
'cancel_url' => $cancelUrl,
]);
return new RedirectResponse($session->url);
┌──────────────────────────────────────────────────────────────┐
│ Frontend │
├──────────────────────────────────────────────────────────────┤
│ │
│ [Produit] ──htmx POST──> [Mini-Cart] ──> [Page Panier] │
│ │ │ │ │
│ ▼ ▼ ▼ │
├──────────────────────────────────────────────────────────────┤
│ Controller │
├──────────────────────────────────────────────────────────────┤
│ CartController::add() │
│ CartController::update() │
│ CartController::remove() │
│ CheckoutController::create() │
├──────────────────────────────────────────────────────────────┤
│ Services │
├──────────────────────────────────────────────────────────────┤
│ CartService StripeService │
│ ├── add() ├── createSession() │
│ ├── remove() ├── handleWebhook() │
│ ├── getItems() └── getSession() │
│ └── getTotal() │
├──────────────────────────────────────────────────────────────┤
│ Stockage │
├──────────────────────────────────────────────────────────────┤
│ Session PHP / Table custom │
└──────────────────────────────────────────────────────────────┘
FonctionnalitéRouteMéthode
Ajouter au panier/cart/add/{product_id}POST
Modifier quantité/cart/update/{product_id}PATCH
Supprimer/cart/remove/{product_id}DELETE
Voir le panier/cartGET
Mini-panier (htmx)/cart/miniGET
Créer checkout/checkout/createPOST
Succès paiement/checkout/successGET
Annulation/checkout/cancelGET
Webhook Stripe/webhook/stripePOST

Drupal gère automatiquement les tokens CSRF pour les formulaires Form API.

Pour les requêtes AJAX/htmx, ajoutez la validation :

// Dans le controller
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
public function add(int $product_id, Request $request): Response {
// Vérifier le token CSRF
$token = $request->headers->get('X-CSRF-Token');
if (!\Drupal::csrfToken()->validate($token, 'cart_action')) {
throw new AccessDeniedHttpException('Invalid CSRF token');
}
// Traitement...
}
<!-- Dans le template -->
<button
hx-post="/cart/add/42"
hx-headers='{"X-CSRF-Token": "{{ csrf_token('cart_action') }}"}'>
Ajouter
</button>

Toujours valider les entrées utilisateur :

public function add(int $product_id, Request $request): Response {
// Validation du product_id
$product = $this->entityTypeManager
->getStorage('node')
->load($product_id);
if (!$product || $product->bundle() !== 'product') {
throw new NotFoundHttpException('Product not found');
}
// Validation de la quantité
$quantity = (int) $request->request->get('quantity', 1);
if ($quantity < 1 || $quantity > 99) {
throw new BadRequestHttpException('Invalid quantity');
}
// Traitement sécurisé...
}

Définissez des permissions granulaires :

# tailstore_cart.permissions.yml
access cart:
title: 'Access shopping cart'
description: 'View and manage own shopping cart'
access checkout:
title: 'Access checkout'
description: 'Proceed to payment and place orders'
administer tailstore cart:
title: 'Administer TailStore Cart'
description: 'Configure cart settings and view all carts'
restrict access: true

Utilisez dans les routes :

tailstore_cart.cart:
path: '/cart'
defaults:
_controller: '\Drupal\tailstore_cart\Controller\CartController::index'
requirements:
_permission: 'access cart'
namespace Drupal\Tests\tailstore_cart\Unit;
use Drupal\Tests\UnitTestCase;
class CartServiceTest extends UnitTestCase {
public function testAddItem(): void {
// Test logic
}
}
Fenêtre de terminal
ddev exec ./vendor/bin/phpunit modules/custom/tailstore_cart/tests

Commencez par Structure d’un module.