Building Your First XooPress Module - A Complete Developer's Guide

๐Ÿ“ฆ

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.

๐ŸŽฏ What You'll Build:
  • 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
    },
];
๐Ÿ’ก Understanding Module Lifecycle:
  • 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

  1. Upload โ€” Place the TaskManager folder in /modules/
  2. Navigate to Admin โ€” Go to /admin/modules
  3. Install โ€” Find "TaskManager" and click Install
  4. Activate โ€” The module activates automatically after install
  5. Verify Tables โ€” Check your database for xp_tasks table
โš ๏ธ Dependency Check: The TaskManager module depends on the System module. XooPress will block installation if System isn't installed and active!

๐ŸŽฏ 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

๐Ÿ’ก Module Development Tips:
  • 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. ๐Ÿš€