Modèles de conception dans XOOPS
2.5.x ✅ 4.0.x ✅
Les modèles de conception sont des solutions réutilisables aux problèmes courants de conception logicielle. XOOPS emploie plusieurs modèles bien établis qui aident à maintenir la qualité du code, à améliorer la testabilité et à améliorer la flexibilité du système.
Vue d’ensemble
Section intitulée « Vue d’ensemble »Comprendre et implémenter correctement les modèles de conception est crucial pour créer des modules XOOPS maintenables. Ce guide couvre les modèles les plus couramment utilisés dans le développement XOOPS.
| Modèle | Objectif | Cas d’usage courants |
|---|---|---|
| MVC | Séparation des préoccupations | Structure du module |
| Singleton | Garantie d’une seule instance | Connexions à la base de données |
| Factory | Abstraction de création d’objets | Handlers, base de données |
| Observer | Notification d’événements | Preloads, notifications |
| Decorator | Extension de comportement dynamique | Éléments de formulaire, filtres |
| Strategy | Échange d’algorithmes | Authentification, validation |
| Adapter | Compatibilité d’interface | Intégration de code hérité |
| Repository | Abstraction d’accès aux données | Persistance des données |
Model-View-Controller (MVC)
Section intitulée « Model-View-Controller (MVC) »Le modèle MVC sépare une application en trois composants interconnectés, rendant la base de code plus organisée et testable.
Architecture
Section intitulée « Architecture »flowchart TB subgraph MVC["Modèle MVC dans XOOPS"] Controller["🎮 Contrôleur<br/>(index.php, admin/index.php)"] Model["📦 Modèle<br/>(Handlers)"] View["🎨 Vue<br/>(Modèles)"]
Controller --> Model Controller --> View Model <--> View end
style Controller fill:#e3f2fd,stroke:#1976d2 style Model fill:#fff3e0,stroke:#f57c00 style View fill:#e8f5e9,stroke:#388e3cModèle (Couche de données)
Section intitulée « Modèle (Couche de données) »<?phpnamespace XoopsModules\MyModule;
class Article extends \XoopsObject{ public function __construct() { $this->initVar('article_id', XOBJ_DTYPE_INT, null, false); $this->initVar('title', XOBJ_DTYPE_TXTBOX, '', true, 255); $this->initVar('content', XOBJ_DTYPE_TXTAREA, '', true); $this->initVar('author_id', XOBJ_DTYPE_INT, 0, true); $this->initVar('status', XOBJ_DTYPE_INT, 1, false); $this->initVar('created', XOBJ_DTYPE_INT, time(), false); $this->initVar('modified', XOBJ_DTYPE_INT, time(), false); }
public function isPublished(): bool { return $this->getVar('status') === 1; }
public function getFormattedDate(): string { return formatTimestamp($this->getVar('created')); }}
class ArticleHandler extends \XoopsPersistableObjectHandler{ public function __construct(\XoopsDatabase $db) { parent::__construct($db, 'mymodule_articles', Article::class, 'article_id', 'title'); }
public function getPublishedArticles(int $limit = 10): array { $criteria = new \CriteriaCompo(); $criteria->add(new \Criteria('status', 1)); $criteria->setSort('created'); $criteria->setOrder('DESC'); $criteria->setLimit($limit);
return $this->getObjects($criteria); }}Vue (Couche de présentation)
Section intitulée « Vue (Couche de présentation) »{* templates/article_list.tpl *}<div class="article-list"> <h2>{$smarty.const._MD_MYMODULE_ARTICLES}</h2>
{foreach from=$articles item=article} <article class="article-item"> <h3> <a href="{$xoops_url}/modules/mymodule/article.php?id={$article.article_id}"> {$article.title|escape} </a> </h3> <p class="meta"> {$smarty.const._MD_MYMODULE_POSTED}: {$article.formatted_date} </p> <div class="content"> {$article.content|truncate:200} </div> </article> {/foreach}</div>Contrôleur (Couche de logique)
Section intitulée « Contrôleur (Couche de logique) »<?phprequire_once dirname(__DIR__, 2) . '/mainfile.php';
use XoopsModules\MyModule\Helper;
$helper = Helper::getInstance();$articleHandler = $helper->getHandler('Article');
// Get action from request$op = \Xmf\Request::getString('op', 'list');
switch ($op) { case 'view': $articleId = \Xmf\Request::getInt('id', 0); $article = $articleHandler->get($articleId);
if (!$article) { redirect_header(XOOPS_URL, 3, _MD_MYMODULE_NOT_FOUND); }
$GLOBALS['xoopsOption']['template_main'] = 'mymodule_article_view.tpl'; require_once XOOPS_ROOT_PATH . '/header.php';
$xoopsTpl->assign('article', $article->toArray()); break;
case 'list': default: $articles = $articleHandler->getPublishedArticles(10);
$GLOBALS['xoopsOption']['template_main'] = 'mymodule_article_list.tpl'; require_once XOOPS_ROOT_PATH . '/header.php';
$xoopsTpl->assign('articles', array_map(fn($a) => $a->toArray(), $articles)); break;}
require_once XOOPS_ROOT_PATH . '/footer.php';Modèle Singleton
Section intitulée « Modèle Singleton »Le modèle Singleton garantit qu’une classe n’a qu’une seule instance et fournit un accès global à celle-ci.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Connexions à la base de données
- Gestionnaires de configuration
- Instances d’enregistreurs
- Gestionnaires de cache
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
class ConfigurationManager{ private static ?self $instance = null; private array $config = [];
private function __construct() { // Load configuration $this->loadConfiguration(); }
// Prevent cloning private function __clone() {}
// Prevent unserialization public function __wakeup() { throw new \Exception("Cannot unserialize singleton"); }
public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); }
return self::$instance; }
private function loadConfiguration(): void { $helper = Helper::getInstance(); $this->config = [ 'items_per_page' => $helper->getConfig('items_per_page', 10), 'allow_comments' => $helper->getConfig('allow_comments', true), 'date_format' => $helper->getConfig('date_format', 'Y-m-d'), ]; }
public function get(string $key, mixed $default = null): mixed { return $this->config[$key] ?? $default; }}
// Usage$config = ConfigurationManager::getInstance();$itemsPerPage = $config->get('items_per_page');Exemples du noyau XOOPS
Section intitulée « Exemples du noyau XOOPS »<?php// XoopsDatabaseFactory uses Singleton pattern$db = XoopsDatabaseFactory::getDatabaseConnection();
// XMF Module Helper uses Singleton$helper = \Xmf\Module\Helper::getHelper('mymodule');
// Xoops main instance$xoops = \Xoops::getInstance();Modèle Factory
Section intitulée « Modèle Factory »Le modèle Factory crée des objets sans spécifier leur classe exacte, permettant une création d’objets flexible.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Création de handlers de manière dynamique
- Connexions à la base de données pour différentes bases de données
- Fournisseurs d’authentification
- Création d’éléments de formulaire
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
interface ContentInterface{ public function render(): string;}
class ArticleContent implements ContentInterface{ private array $data;
public function __construct(array $data) { $this->data = $data; }
public function render(): string { return "<article><h2>{$this->data['title']}</h2><p>{$this->data['body']}</p></article>"; }}
class NewsContent implements ContentInterface{ private array $data;
public function __construct(array $data) { $this->data = $data; }
public function render(): string { return "<div class='news'><h3>{$this->data['headline']}</h3><p>{$this->data['summary']}</p></div>"; }}
class ContentFactory{ public static function create(string $type, array $data): ContentInterface { return match ($type) { 'article' => new ArticleContent($data), 'news' => new NewsContent($data), default => throw new \InvalidArgumentException("Unknown content type: $type"), }; }}
// Usage$article = ContentFactory::create('article', ['title' => 'Hello', 'body' => 'World']);echo $article->render();Factory de base de données XOOPS
Section intitulée « Factory de base de données XOOPS »<?phpclass XoopsDatabaseFactory{ public static function getDatabaseConnection() { static $instance;
if (!isset($instance)) { $dbType = XOOPS_DB_TYPE ?? 'mysql'; $className = 'XoopsDatabase' . ucfirst($dbType);
if (!class_exists($className)) { $file = XOOPS_ROOT_PATH . '/class/database/' . strtolower($dbType) . '.php'; if (file_exists($file)) { require_once $file; } }
$instance = new $className();
if (!$instance->connect()) { trigger_error('Unable to connect to database', E_USER_ERROR); } }
return $instance; }}Modèle Observer
Section intitulée « Modèle Observer »Le modèle Observer permet aux objets d’être notifiés des changements d’état d’un sujet, permettant un comportement piloté par les événements.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Gestion des événements
- Systèmes de notification
- Architectures de plugins
- Journalisation et audit
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
interface ObserverInterface{ public function update(string $event, array $data): void;}
class EventDispatcher{ private array $observers = [];
public function attach(string $event, ObserverInterface $observer): void { if (!isset($this->observers[$event])) { $this->observers[$event] = []; }
$this->observers[$event][] = $observer; }
public function detach(string $event, ObserverInterface $observer): void { if (isset($this->observers[$event])) { $key = array_search($observer, $this->observers[$event], true); if ($key !== false) { unset($this->observers[$event][$key]); } } }
public function notify(string $event, array $data = []): void { if (isset($this->observers[$event])) { foreach ($this->observers[$event] as $observer) { $observer->update($event, $data); } } }}
class EmailNotifier implements ObserverInterface{ public function update(string $event, array $data): void { if ($event === 'article.published') { // Send email notification $this->sendEmail($data['article']); } }
private function sendEmail($article): void { $xoopsMailer = xoops_getMailer(); $xoopsMailer->setSubject('New Article Published: ' . $article->getVar('title')); $xoopsMailer->setBody('A new article has been published.'); $xoopsMailer->send(); }}
// Usage$dispatcher = new EventDispatcher();$dispatcher->attach('article.published', new EmailNotifier());
// When article is published$dispatcher->notify('article.published', ['article' => $article]);Preloads XOOPS (Implémentation Observer)
Section intitulée « Preloads XOOPS (Implémentation Observer) »<?phpclass MymoduleCorePreload extends XoopsPreloadItem{ public static function eventCoreIncludeCommonEnd($args) { // React to core common include completing $GLOBALS['xoopsLogger']->addExtra('MyModule', 'Initialized'); }
public static function eventCoreHeaderEnd($args) { // Add custom headers $GLOBALS['xoTheme']->addStylesheet('modules/mymodule/assets/css/custom.css'); }
public static function eventCoreFooterStart($args) { // Execute before footer renders }}Modèle Decorator
Section intitulée « Modèle Decorator »Le modèle Decorator ajoute un comportement aux objets de manière dynamique sans affecter les autres objets de la même classe.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Personnalisation des éléments de formulaire
- Formatage de sortie
- Vérification des permissions
- Couches de cache
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
interface FormElementInterface{ public function render(): string;}
class TextInput implements FormElementInterface{ private string $name; private string $value;
public function __construct(string $name, string $value = '') { $this->name = $name; $this->value = $value; }
public function render(): string { return sprintf( '<input type="text" name="%s" value="%s">', htmlspecialchars($this->name), htmlspecialchars($this->value) ); }}
abstract class FormElementDecorator implements FormElementInterface{ protected FormElementInterface $element;
public function __construct(FormElementInterface $element) { $this->element = $element; }
public function render(): string { return $this->element->render(); }}
class RequiredDecorator extends FormElementDecorator{ public function render(): string { return $this->element->render() . '<span class="required">*</span>'; }}
class LabelDecorator extends FormElementDecorator{ private string $label;
public function __construct(FormElementInterface $element, string $label) { parent::__construct($element); $this->label = $label; }
public function render(): string { return sprintf( '<label>%s</label>%s', htmlspecialchars($this->label), $this->element->render() ); }}
class HelpTextDecorator extends FormElementDecorator{ private string $helpText;
public function __construct(FormElementInterface $element, string $helpText) { parent::__construct($element); $this->helpText = $helpText; }
public function render(): string { return $this->element->render() . sprintf( '<small class="help-text">%s</small>', htmlspecialchars($this->helpText) ); }}
// Usage - decorators can be stacked$input = new TextInput('username');$input = new RequiredDecorator($input);$input = new LabelDecorator($input, 'Username');$input = new HelpTextDecorator($input, 'Enter your username');
echo $input->render();// Output: <label>Username</label><input type="text" name="username" value=""><span class="required">*</span><small class="help-text">Enter your username</small>Modèle Strategy
Section intitulée « Modèle Strategy »Le modèle Strategy définit une famille d’algorithmes, encapsule chacun d’eux et les rend interchangeables.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Multiples méthodes d’authentification
- Différents algorithmes de tri
- Divers formats d’exportation
- Règles de validation flexibles
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
interface AuthStrategyInterface{ public function authenticate(string $username, string $password): bool;}
class DatabaseAuthStrategy implements AuthStrategyInterface{ public function authenticate(string $username, string $password): bool { $memberHandler = xoops_getHandler('member'); $user = $memberHandler->loginUser($username, $password);
return $user !== false; }}
class LdapAuthStrategy implements AuthStrategyInterface{ private string $ldapHost; private int $ldapPort;
public function __construct(string $host, int $port = 389) { $this->ldapHost = $host; $this->ldapPort = $port; }
public function authenticate(string $username, string $password): bool { $ldap = ldap_connect($this->ldapHost, $this->ldapPort);
if (!$ldap) { return false; }
$bind = @ldap_bind($ldap, "uid=$username,ou=users,dc=example,dc=com", $password);
ldap_close($ldap);
return $bind; }}
class AuthService{ private AuthStrategyInterface $strategy;
public function __construct(AuthStrategyInterface $strategy) { $this->strategy = $strategy; }
public function setStrategy(AuthStrategyInterface $strategy): void { $this->strategy = $strategy; }
public function login(string $username, string $password): bool { return $this->strategy->authenticate($username, $password); }}
// Usage$authService = new AuthService(new DatabaseAuthStrategy());
// Can switch strategies at runtimeif ($useLdap) { $authService->setStrategy(new LdapAuthStrategy('ldap.example.com'));}
$authenticated = $authService->login($username, $password);Modèle Repository
Section intitulée « Modèle Repository »Le modèle Repository fournit une couche d’abstraction entre la logique d’accès aux données et la logique métier.
Quand l’utiliser
Section intitulée « Quand l’utiliser »- Exigences complexes d’accès aux données
- Sources de données multiples
- Couches de données testables
- Domain-Driven Design
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule\Repository;
use XoopsModules\MyModule\Entity\Article;
interface ArticleRepositoryInterface{ public function find(int $id): ?Article; public function findBySlug(string $slug): ?Article; public function findPublished(int $limit = 10, int $offset = 0): array; public function save(Article $article): bool; public function delete(Article $article): bool;}
class ArticleRepository implements ArticleRepositoryInterface{ private \XoopsPersistableObjectHandler $handler;
public function __construct(\XoopsPersistableObjectHandler $handler) { $this->handler = $handler; }
public function find(int $id): ?Article { $obj = $this->handler->get($id); return $obj ?: null; }
public function findBySlug(string $slug): ?Article { $criteria = new \Criteria('slug', $slug); $objects = $this->handler->getObjects($criteria);
return !empty($objects) ? $objects[0] : null; }
public function findPublished(int $limit = 10, int $offset = 0): array { $criteria = new \CriteriaCompo(); $criteria->add(new \Criteria('status', 'published')); $criteria->setSort('published_at'); $criteria->setOrder('DESC'); $criteria->setLimit($limit); $criteria->setStart($offset);
return $this->handler->getObjects($criteria); }
public function save(Article $article): bool { return $this->handler->insert($article); }
public function delete(Article $article): bool { return $this->handler->delete($article); }}Injection de dépendances
Section intitulée « Injection de dépendances »L’injection de dépendances (DI) permet aux objets d’être construits avec leurs dépendances au lieu de les créer en interne.
Avantages
Section intitulée « Avantages »- Amélioration de la testabilité
- Couplage faible
- Configuration flexible
- Meilleure organisation du code
Implémentation
Section intitulée « Implémentation »<?phpnamespace XoopsModules\MyModule;
class ArticleService{ private Repository\ArticleRepositoryInterface $repository; private CacheInterface $cache; private LoggerInterface $logger;
public function __construct( Repository\ArticleRepositoryInterface $repository, CacheInterface $cache, LoggerInterface $logger ) { $this->repository = $repository; $this->cache = $cache; $this->logger = $logger; }
public function getArticle(int $id): ?Entity\Article { $cacheKey = "article_{$id}";
// Try cache first if ($this->cache->has($cacheKey)) { $this->logger->debug("Article {$id} loaded from cache"); return $this->cache->get($cacheKey); }
// Load from repository $article = $this->repository->find($id);
if ($article) { $this->cache->set($cacheKey, $article, 3600); $this->logger->debug("Article {$id} loaded from database"); }
return $article; }}
// Service container setup$container = new DependencyContainer();
$container->register('db', fn() => XoopsDatabaseFactory::getDatabaseConnection());
$container->register('articleHandler', fn($c) => new ArticleHandler($c->resolve('db')));
$container->register('articleRepository', fn($c) => new Repository\ArticleRepository($c->resolve('articleHandler')));
$container->register('cache', fn() => new FileCache(XOOPS_VAR_PATH . '/caches'));
$container->register('logger', fn() => new XoopsLogger());
$container->register('articleService', fn($c) => new ArticleService( $c->resolve('articleRepository'), $c->resolve('cache'), $c->resolve('logger') ));
// Usage$articleService = $container->resolve('articleService');$article = $articleService->getArticle(1);Bonnes pratiques
Section intitulée « Bonnes pratiques »Directives de sélection de modèles
Section intitulée « Directives de sélection de modèles »- Choisir les modèles en fonction des besoins réels, pas des besoins anticipés
- Garder les implémentations simples - ne pas sur-concevoir
- Documenter l’utilisation des modèles pour la compréhension de l’équipe
- Combiner les modèles si approprié (par exemple, Factory + Singleton)
- Considérer la testabilité lors de la sélection des modèles
Anti-modèles courants à éviter
Section intitulée « Anti-modèles courants à éviter »| Anti-modèle | Problème | Solution |
|---|---|---|
| God Object | La classe fait trop | Responsabilité unique |
| Spaghetti Code | Pas de structure claire | Utiliser le modèle MVC |
| Copy-Paste | Duplication du code | Extraire le code commun |
| Magic Numbers | Constantes peu claires | Utiliser des constantes nommées |
| Tight Coupling | Difficile à tester/maintenir | Utiliser l’injection de dépendances |
Modèles de test
Section intitulée « Modèles de test »<?php// Unit testing with dependency injectionclass ArticleServiceTest extends \PHPUnit\Framework\TestCase{ private $repository; private $cache; private $logger; private $service;
protected function setUp(): void { $this->repository = $this->createMock(ArticleRepositoryInterface::class); $this->cache = $this->createMock(CacheInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->service = new ArticleService( $this->repository, $this->cache, $this->logger ); }
public function testGetArticleFromCache(): void { $article = new Article(); $article->setVar('article_id', 1);
$this->cache->expects($this->once()) ->method('has') ->with('article_1') ->willReturn(true);
$this->cache->expects($this->once()) ->method('get') ->with('article_1') ->willReturn($article);
$result = $this->service->getArticle(1);
$this->assertSame($article, $result); }}Documentation associée
Section intitulée « Documentation associée »- Architecture XOOPS - Architecture globale du système
- Couche de base de données - Modèles de persistance des données
- Bonnes pratiques de sécurité - Implémentation sécurisée des modèles
#xoops #design-patterns #architecture #mvc #singleton #factory #observer