Oblikovalski vzorci v XOOPS
2.5.x ✅ 4.0.x ✅
Načrtovalni vzorci so rešitve za pogoste težave pri načrtovanju programske opreme, ki jih je mogoče ponovno uporabiti. XOOPS uporablja več dobro uveljavljenih vzorcev, ki pomagajo vzdrževati kakovost kode, izboljšati možnost testiranja in povečati prilagodljivost sistema.
Hitra izbira vzorca
Niste prepričani, kateri vzorec uporabiti? Glej:
- Izbira vzorca dostopa do podatkov — upravljavci vs. repozitorij vs. storitev vs. CQRS
- Izbira sistema dogodkov — prednalaganja v primerjavi z dogodki PSR-14
Pregled
Section titled “Pregled”Razumevanje in pravilna implementacija oblikovalskih vzorcev je ključnega pomena za ustvarjanje modulov XOOPS, ki jih je mogoče vzdrževati. Ta vodnik pokriva najpogosteje uporabljene vzorce v razvoju XOOPS.
| Vzorec | Namen | Pogosti primeri uporabe |
|---|---|---|
| MVC | Ločitev pomislekov | Struktura modula |
| Singleton | Garancija v enem primeru | Povezave z bazo podatkov |
| Tovarna | Abstrakcija ustvarjanja predmeta | Obdelovalci podatkovnih baz |
| Opazovalec | Obvestilo o dogodku | Prednalaganja, obvestila |
| Dekorater | Razširitev dinamičnega vedenja | Elementi obrazca, filtri |
| Strategija | Izmenjava algoritmov | Avtentikacija, validacija |
| Adapter | Združljivost vmesnika | Integracija podedovane kode |
| Repozitorij | Abstrakcija dostopa do podatkov | Obstojnost podatkov |
Vzorec MVC loči aplikacijo na tri med seboj povezane komponente, zaradi česar je kodna baza bolj organizirana in jo je mogoče preizkusiti.
Arhitektura
Section titled “Arhitektura”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 (podatkovna plast)
Section titled “Model (podatkovna plast)”<?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); }}Pogled (predstavitvena plast)
Section titled “Pogled (predstavitvena plast)”{* 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>krmilnik (logična plast)
Section titled “krmilnik (logična plast)”<?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 vzorec
Section titled “Singleton vzorec”Vzorec Singleton zagotavlja, da ima razred samo en primerek in omogoča globalni dostop do njega.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Povezave z bazo podatkov
- Upravitelji konfiguracije
- Primerki zapisovalnika
- Upravljalniki predpomnilnika
Izvedba
Section titled “Izvedba”<?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 Osnovni primeri
Section titled “XOOPS Osnovni primeri”<?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();Tovarniški vzorec
Section titled “Tovarniški vzorec”Vzorec Factory ustvari objekte, ne da bi določil njihov točen razred, kar omogoča prilagodljivo ustvarjanje objektov.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Dinamično ustvarjanje upravljavcev
- Povezave baz podatkov za različne baze podatkov
- Ponudniki avtentikacije
- Izdelava elementov obrazca
Izvedba
Section titled “Izvedba”<?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
Section titled “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; }}Vzorec opazovalca
Section titled “Vzorec opazovalca”Vzorec Opazovalec omogoča, da so predmeti obveščeni o spremembah stanja subjekta, kar omogoča vedenje, ki ga vodi dogodek.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Vodenje dogodkov
- Sistemi obveščanja
- Arhitekture vtičnikov
- Beleženje in revizija
Izvedba
Section titled “Izvedba”<?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 Prednalaganja (izvedba opazovalca)
Section titled “XOOPS Prednalaganja (izvedba opazovalca)”<?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 }}Vzorec dekoraterja
Section titled “Vzorec dekoraterja”Vzorec Decorator dinamično doda vedenje objektom, ne da bi vplival na druge objekte istega razreda.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Prilagajanje elementov obrazca
- Oblikovanje izhoda
- Preverjanje dovoljenj
- Plasti za predpomnjenje
Izvedba
Section titled “Izvedba”<?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>Strateški vzorec
Section titled “Strateški vzorec”Vzorec strategije definira družino algoritmov, vsakega posebej inkapsulira in naredi medsebojno zamenljive.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Več načinov preverjanja pristnosti
- Različni algoritmi razvrščanja
- Različni izvozni formati
- Prilagodljiva pravila potrjevanja
Izvedba
Section titled “Izvedba”<?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);Vzorec skladišča
Section titled “Vzorec skladišča”Vzorec repozitorija zagotavlja abstrakcijsko plast med logiko dostopa do podatkov in poslovno logiko.
Kdaj uporabiti
Section titled “Kdaj uporabiti”- Zapletene zahteve za dostop do podatkov
- Več virov podatkov
- Podatkovne plasti, ki jih je mogoče preizkusiti
- Oblikovanje, ki temelji na domeni
Izvedba
Section titled “Izvedba”<?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); }}Injekcija odvisnosti
Section titled “Injekcija odvisnosti”Vstavljanje odvisnosti (DI) omogoča, da se objekti sestavijo z njihovimi odvisnostmi, namesto da bi jih ustvarili interno.
Prednosti
Section titled “Prednosti”- Izboljšana preizkušljivost
- Zrahljana spojka
- Prilagodljiva konfiguracija
- Boljša organizacija kode
Izvedba
Section titled “Izvedba”<?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);Najboljše prakse
Section titled “Najboljše prakse”Navodila za izbiro vzorca
Section titled “Navodila za izbiro vzorca”- Izberite vzorce na podlagi dejanskih potreb, ne pričakovanih
- Implementacije naj bodo preproste – ne pretiravajte z inženiringom
- Uporaba vzorca dokumenta za razumevanje ekipe
- Kombinirajte vzorce, kadar je primerno (npr. Factory + Singleton)
- Pri izbiri vzorcev upoštevajte možnost testiranja
Pogosti anti-vzorci, ki se jim je treba izogibati
Section titled “Pogosti anti-vzorci, ki se jim je treba izogibati”| Anti-vzorec | Težava | Rešitev |
|---|---|---|
| Božji predmet | Razred naredi preveč | Enotna odgovornost |
| Koda za špagete | Brez jasne strukture | Uporabite vzorec MVC |
| Kopiraj-Prilepi | Podvajanje kode | Izvleček skupne kode |
| Čarobne številke | Nejasne konstante | Uporabi imenovane konstante |
| Tesna spojka | Težko test/maintain | Uporabi vbrizgavanje odvisnosti |
Testiranje vzorcev
Section titled “Testiranje vzorcev”<?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); }}Povezana dokumentacija
Section titled “Povezana dokumentacija”- XOOPS-Arhitektura - Celotna sistemska arhitektura
- Sloj baze podatkov - Vzorci obstojnosti podatkov
- Najboljše varnostne prakse - Implementacija varnega vzorca
#XOOPS #design-patterns #architecture #mvc #singleton #factory #observer