Tervezési minták XOOPS-ban
2.5.x ✅ 4.0.x ✅
A tervezési minták újrafelhasználható megoldások a gyakori szoftvertervezési problémákra. A XOOPS számos jól bevált mintát alkalmaz, amelyek segítenek megőrizni a kód minőségét, javítani a tesztelhetőséget és a rendszer rugalmasságát.
Áttekintés
Szekció neve “Áttekintés”A tervezési minták megértése és megfelelő megvalósítása kulcsfontosságú a karbantartható XOOPS modulok létrehozásához. Ez az útmutató a XOOPS fejlesztés során leggyakrabban használt mintákat ismerteti.
| Minta | Cél | Gyakori használati esetek |
|---|---|---|
| MVC | Az aggályok szétválasztása | modul felépítése |
| Singleton | Egypéldányos garancia | Adatbázis kapcsolatok |
| Gyár | Objektum létrehozása absztrakció | Kezelők, adatbázis |
| Figyelő | Eseményértesítés | Előtöltések, értesítések |
| Dekoratőr | Dinamikus viselkedés kiterjesztése | Formaelemek, szűrők |
| Stratégia | Algoritmuscsere | Hitelesítés, érvényesítés |
| Adapter | Interfész kompatibilitás | Legacy kód integráció |
| Adattár | Adathozzáférés absztrakció | Adatok fennmaradása |
Model-View-Controller (MVC)
Szekció neve “Model-View-Controller (MVC)”A MVC minta három, egymással összekapcsolt komponensre választja szét az alkalmazást, így a kódbázis szervezettebbé és tesztelhetőbbé válik.
Építészet
Szekció neve “Építészet”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:#388e3cModell (adatréteg)
Szekció neve “Modell (adatréteg)”<?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); }}Nézet (bemutató réteg)
Szekció neve “Nézet (bemutató réteg)”{* 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>Vezérlő (logikai réteg)
Szekció neve “Vezérlő (logikai réteg)”<?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 minta
Szekció neve “Singleton minta”A Singleton minta biztosítja, hogy egy osztálynak csak egy példánya legyen, és globális hozzáférést biztosít hozzá.
Mikor kell használni?
Szekció neve “Mikor kell használni?”- Adatbázis kapcsolatok
- Konfigurációkezelők
- Logger példányok
- Gyorsítótár-kezelők
Megvalósítás
Szekció neve “Megvalósítás”<?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 fő példák
Szekció neve “XOOPS fő példák”<?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();Gyári minta
Szekció neve “Gyári minta”A gyári minta objektumokat hoz létre anélkül, hogy megadná pontos osztályukat, lehetővé téve a rugalmas objektumkészítést.
Mikor kell használni
Szekció neve “Mikor kell használni”- Kezelők dinamikus létrehozása
- Adatbázis kapcsolatok különböző adatbázisokhoz
- Hitelesítés-szolgáltatók
- Űrlapelem készítés
Megvalósítás
Szekció neve “Megvalósítás”<?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 adatbázisgyár
Szekció neve “XOOPS adatbázisgyár”<?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; }}Megfigyelő minta
Szekció neve “Megfigyelő minta”Az Observer minta lehetővé teszi, hogy az objektumok értesítést kapjanak az alany állapotának változásairól, lehetővé téve az eseményvezérelt viselkedést.
Mikor kell használni
Szekció neve “Mikor kell használni”- Rendezvénykezelés
- Értesítési rendszerek
- Plugin architektúrák
- Fakitermelés és auditálás
Megvalósítás
Szekció neve “Megvalósítás”<?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 előtöltések (megfigyelő megvalósítás)
Szekció neve “XOOPS előtöltések (megfigyelő megvalósítás)”<?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 }}Dekorátor minta
Szekció neve “Dekorátor minta”A Dekorátor minta dinamikusan ad hozzá az objektumokhoz a viselkedést anélkül, hogy az azonos osztályba tartozó többi objektumra hatással lenne.
Mikor kell használni
Szekció neve “Mikor kell használni”- Űrlapelemek testreszabása
- Kimeneti formázás
- Engedélyek ellenőrzése
- Rétegek gyorsítótárazása
Megvalósítás
Szekció neve “Megvalósítás”<?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>Stratégiai minta
Szekció neve “Stratégiai minta”A stratégia minta algoritmusok családját határozza meg, mindegyiket magába foglalja, és felcserélhetővé teszi.
Mikor kell használni?
Szekció neve “Mikor kell használni?”- Több hitelesítési módszer
- Különböző rendezési algoritmusok
- Különféle export formátumok
- Rugalmas érvényesítési szabályok
Megvalósítás
Szekció neve “Megvalósítás”<?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);Repository Pattern
Szekció neve “Repository Pattern”A Repository minta absztrakciós réteget biztosít az adathozzáférési logika és az üzleti logika között.
Mikor kell használni?
Szekció neve “Mikor kell használni?”- Összetett adathozzáférési követelmények
- Több adatforrás
- Tesztelhető adatrétegek
- Domain-vezérelt tervezés
Megvalósítás
Szekció neve “Megvalósítás”<?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); }}Dependency Injection
Szekció neve “Dependency Injection”A Dependency Injection (DI) lehetővé teszi, hogy az objektumokat a függőségeikkel együtt hozzuk létre ahelyett, hogy belsőleg hoznánk létre őket.
Előnyök
Szekció neve “Előnyök”- Jobb tesztelhetőség
- Laza tengelykapcsoló
- Rugalmas konfiguráció
- Jobb kódszervezés
Megvalósítás
Szekció neve “Megvalósítás”<?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);Bevált gyakorlatok
Szekció neve “Bevált gyakorlatok”Mintaválasztási irányelvek1. Válasszon mintákat a tényleges szükségletek alapján, ne pedig a várhatóak alapján
Szekció neve “Mintaválasztási irányelvek1. Válasszon mintákat a tényleges szükségletek alapján, ne pedig a várhatóak alapján”- Legyen egyszerű megvalósítás – ne dolgozzon túl
- Dokumentumminta-használat a csapat megértése érdekében
- Ha szükséges, kombinálja a mintákat (pl. Gyári + Singleton)
- A minták kiválasztásakor vegye figyelembe a tesztelhetőséget
Elkerülendő általános anti-minták
Szekció neve “Elkerülendő általános anti-minták”| Anti-Minta | Probléma | Megoldás |
|---|---|---|
| Isten tárgya | Az osztály túl sokat csinál | Egyedülálló felelősség |
| Spagetti kód | Nincs egyértelmű szerkezet | Használjon MVC mintát |
| Másolás-Beillesztés | Kódmásolás | A közös kód kibontása |
| Varázsszámok | Nem egyértelmű állandók | Használjon elnevezett konstansokat |
| Szoros tengelykapcsoló | Nehezen test/maintain | Dependency Injection használata |
Minták tesztelése
Szekció neve “Minták tesztelése”<?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); }}Kapcsolódó dokumentáció
Szekció neve “Kapcsolódó dokumentáció”- XOOPS-Architecture - Általános rendszerarchitektúra
- Adatbázis réteg - Adatmegmaradási minták
- Bevált biztonsági gyakorlatok - Biztonságos mintamegvalósítás
#xoops #design-minták #architektúra #mvc #singleton #gyár #megfigyelő