1. Structure d'un module
Architecture et fichiers de base d’un module custom. Voir →
À la fin de cette étape, vous serez capable de :
# Vérifier Xdebugddev exec php -v | grep Xdebug
# Activer si nécessaireddev config --xdebug-enabledddev restart1. 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 :
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.ymlDrupal utilise le standard PSR-4 pour le chargement automatique des classes :
Namespace: Drupal\tailstore_cart\Controller\CartControllerFichier: modules/custom/tailstore_cart/src/Controller/CartController.phpRègles de nommage :
Drupal\[nom_module]src/ : Point d’entrée du namespacePattern 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 :
Drupal utilise le Service Container de Symfony :
# tailstore_cart.services.ymlservices: # 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 :
[module].[nom_service]tailstore_cart.cart, tailstore_cart.stripe.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é :
$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é | Route | Mé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 | /cart | GET |
| Mini-panier (htmx) | /cart/mini | GET |
| Créer checkout | /checkout/create | POST |
| Succès paiement | /checkout/success | GET |
| Annulation | /checkout/cancel | GET |
| Webhook Stripe | /webhook/stripe | POST |
Drupal gère automatiquement les tokens CSRF pour les formulaires Form API.
Pour les requêtes AJAX/htmx, ajoutez la validation :
// Dans le controlleruse 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.ymlaccess 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: trueUtilisez 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 }}ddev exec ./vendor/bin/phpunit modules/custom/tailstore_cart/testsCommencez par Structure d’un module.