Шаблони проектування в XOOPS
2.5.x ✅ 4.0.x ✅
Патерни проектування — це багаторазові рішення типових проблем проектування програмного забезпечення. XOOPS використовує кілька добре налагоджених шаблонів, які допомагають підтримувати якість коду, покращувати тестування та підвищувати гнучкість системи.
Розуміння та належне впровадження шаблонів проектування має вирішальне значення для створення придатних для обслуговування модулів XOOPS. Цей посібник охоплює найбільш часто використовувані шаблони в розробці XOOPS.
| Візерунок | Призначення | Загальні випадки використання |
|---|---|---|
| MVC | Розділення концернів | Структура модуля |
| Синглтон | Гарантія в одному екземплярі | Підключення до бази даних |
| Завод | Абстракція створення об’єкта | Обробники бази даних |
| Спостерігач | Сповіщення про подію | Попереднє завантаження, повідомлення |
| Декоратор | Розширення динамічної поведінки | Елементи форми, фільтри |
| Стратегія | Алгоритм обміну | Автентифікація, перевірка |
| Адаптер | Сумісність інтерфейсу | Інтеграція старого коду |
| Репозиторій | Абстракція доступу до даних | Постійність даних |
Model-View-Controller (MVC)
Section titled “Model-View-Controller (MVC)”Шаблон MVC розділяє програму на три взаємопов’язані компоненти, роблячи кодову базу більш організованою та придатною для тестування.
Архітектура
Section titled “Архітектура”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:#388e3cМодель (рівень даних)
Section titled “Модель (рівень даних)”<?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); }}Перегляд (Шар презентації)
Section titled “Перегляд (Шар презентації)”{* 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>Контролер ### (рівень логіки)
<?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
Section titled “Шаблон Singleton”Шаблон Singleton гарантує, що клас має лише один екземпляр і забезпечує глобальний доступ до нього.
Коли використовувати
Section titled “Коли використовувати”- Підключення до бази даних
- Менеджери конфігурації
- Логер примірників
- Менеджери кешу
Реалізація
Section titled “Реалізація”<?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 Основні приклади
Section titled “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();Фабричний шаблон
Section titled “Фабричний шаблон”Патерн Factory створює об’єкти без вказівки їх точного класу, що забезпечує гнучке створення об’єктів.
Коли використовувати
Section titled “Коли використовувати”— Динамічне створення обробників
- Підключення до різних баз даних
- Постачальники автентифікації
- Створення елементів форми
Реалізація
Section titled “Реалізація”<?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; }}Шаблон спостерігача
Section titled “Шаблон спостерігача”Патерн «Спостерігач» дозволяє сповіщати об’єкти про зміни в стані суб’єкта, забезпечуючи поведінку, керовану подіями.
Коли використовувати
Section titled “Коли використовувати”- Обробка подій
- Системи сповіщення
- Архітектури плагінів
- Логування та аудит
Реалізація
Section titled “Реалізація”<?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 Попередні завантаження (Реалізація спостерігача)
Section titled “XOOPS Попередні завантаження (Реалізація спостерігача)”<?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 }}Візерунок декоратора
Section titled “Візерунок декоратора”Патерн Decorator динамічно додає поведінку до об’єктів, не впливаючи на інші об’єкти того самого класу.
Коли використовувати
Section titled “Коли використовувати”- Налаштування елементів форми
- Форматування виводу
- Перевірка дозволу
- Кешування шарів
Реалізація
Section titled “Реалізація”<?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>Шаблон стратегії
Section titled “Шаблон стратегії”Шаблон стратегії визначає сімейство алгоритмів, інкапсулює кожен із них і робить їх взаємозамінними.
Коли використовувати
Section titled “Коли використовувати”- Кілька методів автентифікації
- Різні алгоритми сортування
- Різні формати експорту
- Гнучкі правила перевірки
Реалізація
Section titled “Реалізація”<?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);Шаблон сховища
Section titled “Шаблон сховища”Шаблон Repository забезпечує рівень абстракції між логікою доступу до даних і бізнес-логікою.
Коли використовувати
Section titled “Коли використовувати”- Складні вимоги до доступу до даних
- Кілька джерел даних
- Тестовані рівні даних
- Дизайн, орієнтований на домен
Реалізація
Section titled “Реалізація”<?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); }}Ін’єкція залежності
Section titled “Ін’єкція залежності”Впровадження залежностей (DI) дозволяє створювати об’єкти з їхніми залежностями замість того, щоб створювати їх усередині.
Переваги
Section titled “Переваги”— Покращена тестова здатність
- Послаблене зчеплення
- Гнучка конфігурація — Краща організація коду
Реалізація
Section titled “Реалізація”<?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);Найкращі практики
Section titled “Найкращі практики”Вказівки щодо вибору шаблону
Section titled “Вказівки щодо вибору шаблону”- Вибирайте шаблони на основі реальних потреб, а не очікуваних
- Зробіть реалізацію простою – не перевантажуйтеся проектом
- Використання шаблону документа для розуміння команди
- Поєднуйте шаблони, якщо це доречно (наприклад, Factory + Singleton)
- Під час вибору шаблонів враховуйте можливість тестування
Поширені антишаблони, яких слід уникати
Section titled “Поширені антишаблони, яких слід уникати”| Антипаттерн | Проблема | Рішення |
|---|---|---|
| Бог Об’єкт | Клас робить занадто багато | Єдина відповідальність |
| Код спагетті | Немає чіткої структури | Використовуйте шаблон MVC |
| Копіювати-Вставляти | Дублювання коду | Витягти загальний код |
| Магічні числа | Нечіткі константи | Використовуйте іменовані константи |
| Жорстке зчеплення | Важко test/maintain | Використовуйте ін’єкцію залежностей |
Тестування шаблонів
Section titled “Тестування шаблонів”<?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); }}Пов’язана документація
Section titled “Пов’язана документація”- XOOPS-Architecture - Загальна архітектура системи
- Рівень бази даних - Шаблони збереження даних
- Найкращі методи безпеки - Впровадження безпечного шаблону
#xoops #design-patterns #architecture #mvc #singleton #factory #observer