Building Your First XooPress Module
A Complete Developer's Guide to Creating Custom Modules
XooPress modules follow the XOOPS-style architecture โ self-contained packages that can be installed, activated, deactivated, and uninstalled without affecting the core system. This guide will walk you through creating a complete "Task Manager" module from scratch.
- A fully functional Task Manager module
- Database tables for storing tasks
- Custom routes and controllers
- Admin interface for managing tasks
- Front-end display of tasks
๐ Module Structure
Every XooPress module follows a standard structure. Here's what we'll create:
modules/TaskManager/
โโโ module.php # Module definition (required)
โโโ bootstrap.php # Loaded on every request (optional)
โโโ routes.php # Additional routes (optional)
โโโ Controllers/
โ โโโ TaskController.php # Main task controller
โ โโโ AdminController.php # Admin-only controller
โโโ Models/
โ โโโ TaskModel.php # Database interactions
โโโ views/
โ โโโ index.php # List all tasks
โ โโโ single.php # View single task
โ โโโ create.php # Create new task
โ โโโ admin/
โ โโโ dashboard.php # Admin dashboard
โโโ locales/ # Module translations (optional)
โโโ en_US/
โโโ LC_MESSAGES/
โโโ TaskManager.mo
๐ Step 1: Create the Module Definition
The module.php file is the heart of your module. It contains metadata, routes, services, and lifecycle callbacks.
Create modules/TaskManager/module.php:
<?php
return [
// Basic module information
'name' => 'TaskManager',
'version' => '1.0.0',
'description' => 'A simple task management module for XooPress',
'author' => 'Your Name',
'license' => 'GPL-3.0-or-later',
// Dependencies - System module is required for auth
'dependencies' => ['System'],
// Register services in the container
'services' => [
'taskmanager.model' => function ($container) {
return new XooPress\Modules\TaskManager\Models\TaskModel(
$container->get('database')
);
},
],
// Routes registered when module is active
'routes' => [
[
'method' => 'GET',
'pattern' => '/tasks',
'handler' => ['XooPress\Modules\TaskManager\Controllers\TaskController', 'index'],
],
[
'method' => 'GET',
'pattern' => '/tasks/:num',
'handler' => ['XooPress\Modules\TaskManager\Controllers\TaskController', 'show'],
],
[
'method' => 'GET',
'pattern' => '/admin/tasks',
'handler' => ['XooPress\Modules\TaskManager\Controllers\AdminController', 'index'],
],
],
// Runs when module is installed (creates tables)
'install' => function ($container) {
$db = $container->get('database');
$prefix = $db->getPrefix();
// Create tasks table
$sql = "CREATE TABLE IF NOT EXISTS {$prefix}tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending',
priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
due_date DATE NULL,
user_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_user_id (user_id),
INDEX idx_due_date (due_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$db->query($sql);
// Create default admin setting
$db->query("INSERT INTO {$prefix}settings (`key`, `value`) VALUES
('taskmanager_items_per_page', '10')
ON DUPLICATE KEY UPDATE `key` = `key`");
return true;
},
// Runs when module is uninstalled (drops tables)
'uninstall' => function ($container) {
$db = $container->get('database');
$prefix = $db->getPrefix();
// Drop tables
$db->query("DROP TABLE IF EXISTS {$prefix}tasks");
// Clean up settings
$db->query("DELETE FROM {$prefix}settings WHERE `key` LIKE 'taskmanager_%'");
return true;
},
// Runs on every request after module is activated
'init' => function ($container) {
// Register additional hooks or filters here
// For example, add admin menu items
},
];
- install โ Creates tables, sets up initial data
- uninstall โ Cleans up everything (tables, settings)
- init โ Runs on every request (register hooks, menus)
- services โ Bound to container when module is active
๐๏ธ Step 2: Create the Model
The model handles database interactions. Create modules/TaskManager/Models/TaskModel.php:
<?php
namespace XooPress\Modules\TaskManager\Models;
use Core\Database;
class TaskModel
{
private $db;
private $table;
public function __construct(Database $db)
{
$this->db = $db;
$this->table = $db->getPrefix() . 'tasks';
}
/**
* Get all tasks with pagination
*/
public function getAll($page = 1, $perPage = 10, $filters = [])
{
$offset = ($page - 1) * $perPage;
$where = [];
$params = [];
if (!empty($filters['status'])) {
$where[] = "status = ?";
$params[] = $filters['status'];
}
if (!empty($filters['user_id'])) {
$where[] = "user_id = ?";
$params[] = $filters['user_id'];
}
$whereClause = !empty($where) ? "WHERE " . implode(" AND ", $where) : "";
$sql = "SELECT * FROM {$this->table}
{$whereClause}
ORDER BY due_date ASC, priority DESC
LIMIT ? OFFSET ?";
$params[] = $perPage;
$params[] = $offset;
return $this->db->query($sql, $params)->fetchAll();
}
/**
* Get total count for pagination
*/
public function getCount($filters = [])
{
$where = [];
$params = [];
if (!empty($filters['status'])) {
$where[] = "status = ?";
$params[] = $filters['status'];
}
if (!empty($filters['user_id'])) {
$where[] = "user_id = ?";
$params[] = $filters['user_id'];
}
$whereClause = !empty($where) ? "WHERE " . implode(" AND ", $where) : "";
$sql = "SELECT COUNT(*) as total FROM {$this->table} {$whereClause}";
$result = $this->db->query($sql, $params)->fetch();
return $result['total'] ?? 0;
}
/**
* Get single task by ID
*/
public function getById($id)
{
$sql = "SELECT * FROM {$this->table} WHERE id = ?";
return $this->db->query($sql, [$id])->fetch();
}
/**
* Create a new task
*/
public function create($data)
{
$sql = "INSERT INTO {$this->table}
(title, description, status, priority, due_date, user_id)
VALUES (?, ?, ?, ?, ?, ?)";
return $this->db->query($sql, [
$data['title'],
$data['description'] ?? '',
$data['status'] ?? 'pending',
$data['priority'] ?? 'medium',
$data['due_date'] ?? null,
$data['user_id']
]);
return $this->db->lastInsertId();
}
/**
* Update a task
*/
public function update($id, $data)
{
$sql = "UPDATE {$this->table} SET
title = ?,
description = ?,
status = ?,
priority = ?,
due_date = ?
WHERE id = ?";
return $this->db->query($sql, [
$data['title'],
$data['description'] ?? '',
$data['status'] ?? 'pending',
$data['priority'] ?? 'medium',
$data['due_date'] ?? null,
$id
]);
}
/**
* Delete a task
*/
public function delete($id)
{
$sql = "DELETE FROM {$this->table} WHERE id = ?";
return $this->db->query($sql, [$id]);
}
/**
* Update task status
*/
public function updateStatus($id, $status)
{
$sql = "UPDATE {$this->table} SET status = ? WHERE id = ?";
return $this->db->query($sql, [$status, $id]);
}
/**
* Get task statistics for a user
*/
public function getStats($userId = null)
{
$where = $userId ? "WHERE user_id = ?" : "";
$params = $userId ? [$userId] : [];
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high_priority
FROM {$this->table} {$where}";
return $this->db->query($sql, $params)->fetch();
}
}
๐ฎ Step 3: Create the Controller
Create modules/TaskManager/Controllers/TaskController.php for front-end display:
<?php
namespace XooPress\Modules\TaskManager\Controllers;
use Core\Controller;
class TaskController extends Controller
{
private $taskModel;
public function __construct()
{
parent::__construct();
$this->taskModel = $this->container->get('taskmanager.model');
}
/**
* Display all tasks
*/
public function index($request)
{
$page = (int)($request->get('page') ?? 1);
$status = $request->get('status');
$filters = [];
if ($status && in_array($status, ['pending', 'in_progress', 'completed'])) {
$filters['status'] = $status;
}
if (isset($_SESSION['user_id'])) {
$filters['user_id'] = $_SESSION['user_id'];
}
$perPage = (int)($this->getSetting('taskmanager_items_per_page', 10));
$tasks = $this->taskModel->getAll($page, $perPage, $filters);
$total = $this->taskModel->getCount($filters);
$stats = $this->taskModel->getStats($_SESSION['user_id'] ?? null);
return $this->view('taskmanager::index', [
'title' => __('My Tasks'),
'tasks' => $tasks,
'stats' => $stats,
'currentPage' => $page,
'totalPages' => ceil($total / $perPage),
'currentStatus' => $status,
'total' => $total
]);
}
/**
* Display a single task
*/
public function show($request, $params)
{
$id = (int)$params['id'];
$task = $this->taskModel->getById($id);
if (!$task) {
return $this->notFound();
}
// Check permission
if ($task['user_id'] != ($_SESSION['user_id'] ?? 0)) {
return $this->forbidden();
}
return $this->view('taskmanager::single', [
'title' => htmlspecialchars($task['title']),
'task' => $task
]);
}
/**
* Show create task form
*/
public function create()
{
if (!isset($_SESSION['user_id'])) {
return $this->redirect('/login');
}
return $this->view('taskmanager::create', [
'title' => __('Create New Task')
]);
}
/**
* Store new task
*/
public function store($request)
{
if (!isset($_SESSION['user_id'])) {
return $this->json(['error' => 'Unauthorized'], 401);
}
$data = [
'title' => $request->post('title'),
'description' => $request->post('description'),
'status' => $request->post('status', 'pending'),
'priority' => $request->post('priority', 'medium'),
'due_date' => $request->post('due_date'),
'user_id' => $_SESSION['user_id']
];
// Validate
if (empty($data['title'])) {
return $this->back()->with('error', __('Title is required'));
}
$this->taskModel->create($data);
return $this->redirect('/tasks')->with('success', __('Task created successfully!'));
}
/**
* Update task status (AJAX)
*/
public function updateStatus($request, $params)
{
$id = (int)$params['id'];
$status = $request->post('status');
$task = $this->taskModel->getById($id);
if (!$task || $task['user_id'] != ($_SESSION['user_id'] ?? 0)) {
return $this->json(['error' => 'Unauthorized'], 401);
}
$this->taskModel->updateStatus($id, $status);
return $this->json(['success' => true]);
}
}
Create the admin controller at modules/TaskManager/Controllers/AdminController.php:
<?php
namespace XooPress\Modules\TaskManager\Controllers;
use Core\Controller;
class AdminController extends Controller
{
private $taskModel;
public function __construct()
{
parent::__construct();
$this->taskModel = $this->container->get('taskmanager.model');
// Require admin role
if (($_SESSION['user_role'] ?? '') !== 'admin') {
$this->redirect('/login');
}
}
/**
* Admin dashboard
*/
public function index($request)
{
$page = (int)($request->get('page') ?? 1);
$perPage = 20;
$tasks = $this->taskModel->getAll($page, $perPage);
$total = $this->taskModel->getCount();
$stats = $this->taskModel->getStats();
return $this->view('taskmanager::admin/dashboard', [
'title' => __('Task Manager Admin'),
'tasks' => $tasks,
'stats' => $stats,
'currentPage' => $page,
'totalPages' => ceil($total / $perPage),
'total' => $total
]);
}
/**
* Delete task (admin only)
*/
public function delete($request, $params)
{
$id = (int)$params['id'];
$this->taskModel->delete($id);
return $this->redirect('/admin/tasks')->with('success', __('Task deleted'));
}
}
๐ผ๏ธ Step 4: Create the Views
Create modules/TaskManager/views/index.php:
<?= $theme->getHeader() ?>
<div class="taskmanager-container">
<div class="taskmanager-header">
<h1><?= htmlspecialchars($title) ?></h1>
<a href="/tasks/create" class="btn btn-primary">+ New Task</a>
</div>
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?= $stats['total'] ?? 0 ?></div>
<div class="stat-label">Total Tasks</div>
</div>
<div class="stat-card pending">
<div class="stat-value"><?= $stats['pending'] ?? 0 ?></div>
<div class="stat-label">Pending</div>
</div>
<div class="stat-card progress">
<div class="stat-value"><?= $stats['in_progress'] ?? 0 ?></div>
<div class="stat-label">In Progress</div>
</div>
<div class="stat-card completed">
<div class="stat-value"><?= $stats['completed'] ?? 0 ?></div>
<div class="stat-label">Completed</div>
</div>
</div>
<!-- Status Filter -->
<div class="task-filters">
<a href="/tasks" class="filter-btn <?= !$currentStatus ? 'active' : '' ?>">All</a>
<a href="/tasks?status=pending" class="filter-btn <?= $currentStatus === 'pending' ? 'active' : '' ?>">Pending</a>
<a href="/tasks?status=in_progress" class="filter-btn <?= $currentStatus === 'in_progress' ? 'active' : '' ?>">In Progress</a>
<a href="/tasks?status=completed" class="filter-btn <?= $currentStatus === 'completed' ? 'active' : '' ?>">Completed</a>
</div>
<!-- Tasks List -->
<div class="tasks-list">
<?php if (empty($tasks)): ?>
<p class="no-tasks">No tasks found. Create your first task!</p>
<?php else: ?>
<?php foreach ($tasks as $task): ?>
<div class="task-card priority-<?= $task['priority'] ?>">
<div class="task-status">
<input type="checkbox"
class="task-checkbox"
data-id="<?= $task['id'] ?>"
<?= $task['status'] === 'completed' ? 'checked' : '' ?>>
</div>
<div class="task-content">
<h3>
<a href="/tasks/<?= $task['id'] ?>">
<?= htmlspecialchars($task['title']) ?>
</a>
</h3>
<?php if (!empty($task['description'])): ?>
<p><?= htmlspecialchars(mb_substr($task['description'], 0, 100)) ?>...</p>
<?php endif; ?>
<div class="task-meta">
<span class="priority-badge priority-<?= $task['priority'] ?>">
<?= ucfirst($task['priority']) ?>
</span>
<?php if ($task['due_date']): ?>
<span class="due-date">
๐
Due: <?= date('M j, Y', strtotime($task['due_date'])) ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="/tasks?page=<?= $i ?><?= $currentStatus ? '&status=' . $currentStatus : '' ?>"
class="<?= $i === $currentPage ? 'current' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<style>
.taskmanager-container {
max-width: 900px;
margin: 0 auto;
}
.taskmanager-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
border-bottom: 3px solid #4f46e5;
}
.stat-card.pending { border-bottom-color: #f59e0b; }
.stat-card.progress { border-bottom-color: #3b82f6; }
.stat-card.completed { border-bottom-color: #10b981; }
.stat-value {
font-size: 2rem;
font-weight: bold;
}
.task-filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-btn {
padding: 8px 16px;
border-radius: 20px;
background: #f0f0f0;
text-decoration: none;
color: #666;
}
.filter-btn.active {
background: #4f46e5;
color: white;
}
.task-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
display: flex;
gap: 15px;
transition: all 0.2s;
}
.task-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.task-card.priority-high { border-left: 4px solid #dc2626; }
.task-card.priority-medium { border-left: 4px solid #f59e0b; }
.task-card.priority-low { border-left: 4px solid #10b981; }
.task-status {
padding-top: 3px;
}
.task-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.task-content {
flex: 1;
}
.task-content h3 {
margin: 0 0 8px 0;
}
.task-content h3 a {
color: #1f2937;
text-decoration: none;
}
.task-content h3 a:hover {
color: #4f46e5;
}
.task-content p {
margin: 0 0 10px 0;
color: #6b7280;
}
.task-meta {
display: flex;
gap: 15px;
align-items: center;
}
.priority-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.priority-badge.priority-high { background: #fee2e2; color: #dc2626; }
.priority-badge.priority-medium { background: #fed7aa; color: #f59e0b; }
.priority-badge.priority-low { background: #d1fae5; color: #10b981; }
.due-date {
font-size: 0.8rem;
color: #6b7280;
}
.no-tasks {
text-align: center;
padding: 40px;
color: #9ca3af;
}
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 30px;
}
.pagination a {
padding: 8px 12px;
border: 1px solid #e0e0e0;
text-decoration: none;
color: #666;
border-radius: 4px;
}
.pagination a.current {
background: #4f46e5;
color: white;
border-color: #4f46e5;
}
</style>
<script>
document.querySelectorAll('.task-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
fetch('/tasks/' + this.dataset.id + '/status', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'status=' + (this.checked ? 'completed' : 'pending')
});
});
});
</script>
<?= $theme->getFooter() ?>
๐ง Step 5: Add Optional Bootstrap File
Create modules/TaskManager/bootstrap.php for initialization code:
<?php
// modules/TaskManager/bootstrap.php
// This runs on every request when the module is active
// Register admin menu items
add_action('admin_menu', function() {
add_menu_page(
'Task Manager',
'Tasks',
'manage_options',
'taskmanager',
function() {
// Redirect to module's admin controller
header('Location: /admin/tasks');
exit;
},
'dashicons-list-view'
);
});
// Register shortcode for displaying tasks
add_shortcode('user_tasks', function($atts) {
ob_start();
// Render tasks widget
echo '<div class="user-tasks-widget">Loading...</div>';
return ob_get_clean();
});
๐ Step 6: Module Lifecycle in Action
Module States & Transitions
| State | What Happens | Database Status |
|---|---|---|
| Not Installed | Files exist but not registered | No record in xp_modules |
| Installed (Active) | Tables created, routes active | active = 1 |
| Installed (Inactive) | Tables kept, routes inactive | active = 0 |
| Uninstalled | Tables dropped, record removed | No record |
๐ฆ Step 7: Install Your Module
- Upload โ Place the
TaskManagerfolder in/modules/ - Navigate to Admin โ Go to
/admin/modules - Install โ Find "TaskManager" and click Install
- Activate โ The module activates automatically after install
- Verify Tables โ Check your database for
xp_taskstable
๐ฏ Module Features You Can Add
REST API
Add API endpoints for frontend frameworks
/api/tasks
Email Notifications
Send reminders for due tasks
add_action('task_due', ...)
Cron Jobs
Auto-update overdue tasks
schedule_cron('daily', ...)
๐ง Advanced Module Features
Module Services
Services registered in module.php are available anywhere:
// In any controller
$taskModel = $this->container->get('taskmanager.model');
$api = $this->container->get('taskmanager.api');
Additional Routes via routes.php
// modules/TaskManager/routes.php
$router->addRoute('GET', '/tasks/export/csv', [TaskController::class, 'exportCsv']);
$router->addRoute('POST', '/tasks/bulk/delete', [AdminController::class, 'bulkDelete']);
$router->addGroup('/api/v1', function($router) {
$router->addRoute('GET', '/tasks', [ApiController::class, 'getTasks']);
$router->addRoute('POST', '/tasks', [ApiController::class, 'createTask']);
});
Module Translations
Add locale files for internationalization:
modules/TaskManager/locales/de_DE/LC_MESSAGES/TaskManager.mo modules/TaskManager/locales/fr_FR/LC_MESSAGES/TaskManager.mo
๐ Next Steps
- ๐ Read the Mastering XooPress Themes guide
- ๐ง Explore Module Management in the admin panel
- ๐ฆ Learn about Module Dependencies
- ๐จ Check out The Hook System for extending modules
- Always use table prefixes โ
$db->getPrefix() . 'tablename' - Sanitize all user input โ use
htmlspecialchars()in views - Use prepared statements โ the Database class handles this automatically
- Test uninstall thoroughly โ ensure all tables and settings are cleaned up
- Version your module โ update the version number when making changes
Congratulations! You've built a complete Task Manager module for XooPress. The module system gives you powerful tools to extend the CMS while keeping your code organized and maintainable. ๐