Meilleures pratiques du développement de modules
Ce document consolide les meilleures pratiques pour développer des modules XOOPS de haute qualité. Suivre ces directives garantit que les modules sont maintenables, sécurisés et performants.
Architecture
Section intitulée « Architecture »Suivre une architecture propre
Section intitulée « Suivre une architecture propre »Organisez le code en couches :
src/├── Domain/ # Logique métier, entités├── Application/ # Cas d'usage, services├── Infrastructure/ # Base de données, services externes└── Presentation/ # Contrôleurs, modèlesResponsabilité unique
Section intitulée « Responsabilité unique »Chaque classe devrait avoir une seule raison de changer :
// Bien : Classes cibléesclass ArticleRepository { /* persistence only */ }class ArticleValidator { /* validation only */ }class ArticleNotifier { /* notifications only */ }
// Mauvais : Classe Dieuclass Article { public function save() { } public function validate() { } public function notify() { } public function generatePDF() { }}Injection de dépendances
Section intitulée « Injection de dépendances »Injecter les dépendances, ne pas les créer :
// Bienpublic function __construct( private readonly ArticleRepositoryInterface $repository) {}
// Mauvaispublic function __construct() { $this->repository = new ArticleRepository();}Qualité du code
Section intitulée « Qualité du code »Sécurité de type
Section intitulée « Sécurité de type »Utiliser les types stricts et les déclarations de type :
<?php
declare(strict_types=1);
final class ArticleService{ public function findById(int $id): ?Article { // ... }
public function create(CreateArticleDTO $dto): Article { // ... }}Gestion des erreurs
Section intitulée « Gestion des erreurs »Utiliser les exceptions de manière appropriée :
// Lever des exceptions spécifiquesthrow new ArticleNotFoundException($id);throw new ValidationException($errors);throw new UnauthorizedException('Cannot edit this article');
// Capturer au niveau appropriétry { $article = $service->create($dto);} catch (ValidationException $e) { return $this->renderForm($e->getErrors());} catch (UnauthorizedException $e) { return $this->redirectToLogin();}Sécurité des valeurs nulles
Section intitulée « Sécurité des valeurs nulles »Éviter les valeurs nulles si possible :
// Utiliser le motif null objectpublic function getAuthor(): UserInterface{ return $this->author ?? new AnonymousUser();}
// Utiliser le motif Optional/Maybepublic function findById(int $id): ?Article{ // Explicitement nullable return}Base de données
Section intitulée « Base de données »Utiliser les critères pour les requêtes
Section intitulée « Utiliser les critères pour les requêtes »$criteria = new CriteriaCompo();$criteria->add(new Criteria('status', 'published'));$criteria->add(new Criteria('category_id', $categoryId));$criteria->setSort('created_at');$criteria->setOrder('DESC');$criteria->setLimit($limit);
$items = $handler->getObjects($criteria);Échapper l’entrée utilisateur
Section intitulée « Échapper l’entrée utilisateur »$sql = sprintf( "SELECT * FROM %s WHERE id = %d AND title = %s", $db->prefix('mymodule_items'), intval($id), $db->quoteString($title));Utiliser les transactions
Section intitulée « Utiliser les transactions »$db->query('START TRANSACTION');
try { $handler->insert($article); $handler->insert($metadata); $db->query('COMMIT');} catch (\Exception $e) { $db->query('ROLLBACK'); throw $e;}Sécurité
Section intitulée « Sécurité »Toujours valider l’entrée
Section intitulée « Toujours valider l’entrée »use Xmf\Request;
$id = Request::getInt('id', 0);$title = Request::getString('title', '');$data = Request::getArray('data', []);
// Validation supplémentaireif (strlen($title) < 5) { throw new ValidationException('Title too short');}Utiliser les jetons CSRF
Section intitulée « Utiliser les jetons CSRF »// Dans le formulaire$form->addElement(new XoopsFormHiddenToken());
// À la soumissionif (!$GLOBALS['xoopsSecurity']->check()) { redirect_header('index.php', 3, 'Invalid token');}Vérifier les permissions
Section intitulée « Vérifier les permissions »if (!$helper->isUserAdmin()) { redirect_header('index.php', 3, _NOPERM);}
if (!$permHandler->isGranted('edit', $categoryId)) { throw new UnauthorizedException();}Performance
Section intitulée « Performance »Utiliser la mise en cache
Section intitulée « Utiliser la mise en cache »$cache = $helper->getCache();$cacheKey = "articles_{$categoryId}_{$limit}";
$articles = $cache->read($cacheKey);if ($articles === false) { $articles = $handler->getArticles($categoryId, $limit); $cache->write($cacheKey, $articles, 3600);}Optimiser les requêtes
Section intitulée « Optimiser les requêtes »// Utiliser les index// Ajouter à sql/mysql.sql :// INDEX `idx_status_date` (`status`, `created_at`)
// Sélectionner uniquement les colonnes nécessaires$handler->getObjects($criteria, false, true); // asArray = true
// Utiliser la pagination$criteria->setLimit($perPage);$criteria->setStart($offset);Écrire des tests unitaires
Section intitulée « Écrire des tests unitaires »public function testCreateArticle(): void{ $repository = $this->createMock(ArticleRepositoryInterface::class); $repository->expects($this->once())->method('save');
$service = new ArticleService($repository); $dto = new CreateArticleDTO('Title', 'Content');
$article = $service->create($dto);
$this->assertInstanceOf(Article::class, $article);}Documentation connexe
Section intitulée « Documentation connexe »- Code-propre - Principes de code propre
- Organisation-du-code - Structure du projet
- Tests - Guide de test
- ../02-Core-Concepts/Security/Meilleures-pratiques-de-sécurité - Guide de sécurité