Design Patterns in XOOPS¶
2.5.x ✅ 2026 ✅
Design patterns are reusable solutions to common software design problems. XOOPS employs several well-established patterns that help maintain code quality, improve testability, and enhance system flexibility.
Quick Pattern Selection
Not sure which pattern to use? See: - Choosing a Data Access Pattern — handlers vs repository vs service vs CQRS - Choosing an Event System — preloads vs PSR-14 events
Overview¶
Understanding and properly implementing design patterns is crucial for creating maintainable XOOPS modules. This guide covers the most commonly used patterns in XOOPS development.
| Pattern | Purpose | Common Use Cases |
|---|---|---|
| MVC | Separation of concerns | Module structure |
| Singleton | Single instance guarantee | Database connections |
| Factory | Object creation abstraction | Handlers, database |
| Observer | Event notification | Preloads, notifications |
| Decorator | Dynamic behavior extension | Form elements, filters |
| Strategy | Algorithm interchange | Authentication, validation |
| Adapter | Interface compatibility | Legacy code integration |
| Repository | Data access abstraction | Data persistence |
Model-View-Controller (MVC)¶
The MVC pattern separates an application into three interconnected components, making the codebase more organized and testable.
Architecture¶
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 Model (Data Layer)¶
<?php
namespace 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);
}
}
View (Presentation Layer)¶
{* 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 (Logic Layer)¶
<?php
// index.php
require_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 Pattern¶
The Singleton pattern ensures a class has only one instance and provides global access to it.
When to Use¶
- Database connections
- Configuration managers
- Logger instances
- Cache managers
Implementation¶
<?php
namespace 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 Core Examples¶
<?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();
Factory Pattern¶
The Factory pattern creates objects without specifying their exact class, allowing for flexible object creation.
When to Use¶
- Creating handlers dynamically
- Database connections for different databases
- Authentication providers
- Form element creation
Implementation¶
<?php
namespace 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¶
<?php
class 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 Pattern¶
The Observer pattern allows objects to be notified of changes to a subject's state, enabling event-driven behavior.
When to Use¶
- Event handling
- Notification systems
- Plugin architectures
- Logging and auditing
Implementation¶
<?php
namespace 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 (Observer Implementation)¶
<?php
// modules/mymodule/preloads/core.php
class 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
}
}
Decorator Pattern¶
The Decorator pattern adds behavior to objects dynamically without affecting other objects of the same class.
When to Use¶
- Form element customization
- Output formatting
- Permission checking
- Caching layers
Implementation¶
<?php
namespace 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>
Strategy Pattern¶
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
When to Use¶
- Multiple authentication methods
- Different sorting algorithms
- Various export formats
- Flexible validation rules
Implementation¶
<?php
namespace 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 runtime
if ($useLdap) {
$authService->setStrategy(new LdapAuthStrategy('ldap.example.com'));
}
$authenticated = $authService->login($username, $password);
Repository Pattern¶
The Repository pattern provides an abstraction layer between data access logic and business logic.
When to Use¶
- Complex data access requirements
- Multiple data sources
- Testable data layers
- Domain-Driven Design
Implementation¶
<?php
namespace 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¶
Dependency Injection (DI) allows objects to be constructed with their dependencies instead of creating them internally.
Benefits¶
- Improved testability
- Loose coupling
- Flexible configuration
- Better code organization
Implementation¶
<?php
namespace 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);
Best Practices¶
Pattern Selection Guidelines¶
- Choose patterns based on actual needs, not anticipated ones
- Keep implementations simple - don't over-engineer
- Document pattern usage for team understanding
- Combine patterns when appropriate (e.g., Factory + Singleton)
- Consider testability when selecting patterns
Common Anti-Patterns to Avoid¶
| Anti-Pattern | Problem | Solution |
|---|---|---|
| God Object | Class does too much | Single Responsibility |
| Spaghetti Code | No clear structure | Use MVC pattern |
| Copy-Paste | Code duplication | Extract common code |
| Magic Numbers | Unclear constants | Use named constants |
| Tight Coupling | Hard to test/maintain | Use Dependency Injection |
Testing Patterns¶
<?php
// Unit testing with dependency injection
class 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);
}
}
Related Documentation¶
- XOOPS-Architecture - Overall system architecture
- Database Layer - Data persistence patterns
- Security Best Practices - Secure pattern implementation