Designmønstre i XOOPS
2.5.x ✅ 4.0.x ✅
Designmønstre er genanvendelige løsninger på almindelige softwaredesignproblemer. XOOPS anvender flere veletablerede mønstre, der hjælper med at opretholde kodekvalitet, forbedre testbarheden og forbedre systemets fleksibilitet.
Oversigt
Sektion kaldt “Oversigt”Forståelse og korrekt implementering af designmønstre er afgørende for at skabe vedligeholdelsesvenlige XOOPS-moduler. Denne vejledning dækker de mest almindeligt anvendte mønstre i XOOPS udvikling.
| Mønster | Formål | Tilfælde af almindelig brug |
|---|---|---|
| MVC | Adskillelse af bekymringer | Modulstruktur |
| Singleton | Enkeltinstansgaranti | Databaseforbindelser |
| Fabrik | Objekt skabelse abstraktion | Håndtere, database |
| Observatør | Begivenhedsmeddelelse | Forudladninger, meddelelser |
| Dekoratør | Dynamisk adfærdsudvidelse | Formelementer, filtre |
| Strategi | Algoritmeudveksling | Autentificering, validering |
| Adapter | Interface kompatibilitet | Ældre kodeintegration |
| Depot | Abstraktion af dataadgang | Datapersistens |
Model-View-Controller (MVC)
Sektion kaldt “Model-View-Controller (MVC)”MVC-mønsteret adskiller en applikation i tre indbyrdes forbundne komponenter, hvilket gør kodebasen mere organiseret og testbar.
Arkitektur
Sektion kaldt “Arkitektur”flowchart TB subgraph MVC["MVC Pattern in XOOPS"] Controller["🎮 Controller<br/>(index.php, admin/index.php)"] Model["📦 Model<br/>(Handlers)"] View["🎨 View<br/>(Templates)"]
Controller --> Model Controller --> View Model <--> View end
style Controller fill:#e3f2fd,stroke:#1976d2 style Model fill:#fff3e0,stroke:#f57c00 style View fill:#e8f5e9,stroke:#388e3cModel (datalag)
Sektion kaldt “Model (datalag)”<?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); }}Vis (præsentationslag)
Sektion kaldt “Vis (præsentationslag)”{* 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>Controller (logisk lag)
Sektion kaldt “Controller (logisk lag)”<?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';Singleton mønster
Sektion kaldt “Singleton mønster”Singleton-mønsteret sikrer, at en klasse kun har én instans og giver global adgang til den.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Databaseforbindelser
- Konfigurationsledere
- Logger-forekomster
- Cache-managere
Implementering
Sektion kaldt “Implementering”<?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');XOOPS Kerneeksempler
Sektion kaldt “XOOPS Kerneeksempler”<?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();Fabriksmønster
Sektion kaldt “Fabriksmønster”Fabriksmønsteret opretter objekter uden at angive deres nøjagtige klasse, hvilket giver mulighed for fleksibel objektoprettelse.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Oprettelse af handlere dynamisk
- Databaseforbindelser til forskellige databaser
- Autentificeringsudbydere
- Oprettelse af formelementer
Implementering
Sektion kaldt “Implementering”<?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();XOOPS Database Factory
Sektion kaldt “XOOPS Database Factory”<?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; }}Observer mønster
Sektion kaldt “Observer mønster”Observer-mønsteret gør det muligt for objekter at blive underrettet om ændringer i et emnes tilstand, hvilket muliggør hændelsesdrevet adfærd.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Begivenhedshåndtering
- Notifikationssystemer
- Plugin-arkitekturer
- Logning og revision
Implementering
Sektion kaldt “Implementering”<?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]);XOOPS Preloads (observatørimplementering)
Sektion kaldt “XOOPS Preloads (observatørimplementering)”<?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 }}Dekorationsmønster
Sektion kaldt “Dekorationsmønster”Decorator-mønsteret tilføjer adfærd til objekter dynamisk uden at påvirke andre objekter af samme klasse.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Tilpasning af formularelementer
- Outputformatering
- Tilladelseskontrol
- Caching af lag
Implementering
Sektion kaldt “Implementering”<?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>Strategimønster
Sektion kaldt “Strategimønster”Strategimønsteret definerer en familie af algoritmer, indkapsler hver enkelt og gør dem udskiftelige.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Flere godkendelsesmetoder
- Forskellige sorteringsalgoritmer
- Forskellige eksportformater
- Fleksible valideringsregler
Implementering
Sektion kaldt “Implementering”<?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);Opbevaringsmønster
Sektion kaldt “Opbevaringsmønster”Repository-mønsteret giver et abstraktionslag mellem dataadgangslogik og forretningslogik.
Hvornår skal bruges
Sektion kaldt “Hvornår skal bruges”- Komplekse krav til dataadgang
- Flere datakilder
- Testbare datalag
- Domænedrevet design
Implementering
Sektion kaldt “Implementering”<?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); }}Afhængighedsindsprøjtning
Sektion kaldt “Afhængighedsindsprøjtning”Dependency Injection (DI) gør det muligt at konstruere objekter med deres afhængigheder i stedet for at skabe dem internt.
Fordele
Sektion kaldt “Fordele”- Forbedret testbarhed
- Løs kobling
- Fleksibel konfiguration
- Bedre kodeorganisering
Implementering
Sektion kaldt “Implementering”<?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);Bedste praksis
Sektion kaldt “Bedste praksis”Retningslinjer for mønstervalg1. Vælg mønstre baseret på faktiske behov, ikke forventede
Sektion kaldt “Retningslinjer for mønstervalg1. Vælg mønstre baseret på faktiske behov, ikke forventede”- Hold implementeringer enkle - overkonstruer ikke
- Dokumentmønsterbrug for teamforståelse
- Kombiner mønstre, når det er relevant (f.eks. Factory + Singleton)
- Overvej testbarhed, når du vælger mønstre
Almindelige anti-mønstre, der skal undgås
Sektion kaldt “Almindelige anti-mønstre, der skal undgås”| Anti-mønster | Problem | Løsning |
|---|---|---|
| Gud objekt | Klasse gør for meget | Enkelt ansvar |
| Spaghetti kode | Ingen klar struktur | Brug MVC-mønster |
| Copy-Paste | Kode duplikering | Uddrag fælles kode |
| Magiske tal | Uklare konstanter | Brug navngivne konstanter |
| Tæt kobling | Svært at teste/vedligeholde | Brug Dependency Injection |
Test af mønstre
Sektion kaldt “Test af mønstre”<?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); }}Relateret dokumentation
Sektion kaldt “Relateret dokumentation”- XOOPS-Architecture - Overordnet systemarkitektur
- Database Layer - Datapersistensmønstre
- Best Practices for sikkerhed - Sikker mønsterimplementering
#xoops #design-mønstre #arkitektur #mvc #singleton #factory #observatør