Prompt Guardrails for Module Development

CRITICAL REQUIREMENTS & GUARDRAILS

1. Module Structure & Naming Convention (MANDATORY)

text
module_name/
├── module_name.php          ← Main module file (lowercase, matches folder)
├── controllers/
│   └── Module_name.php      ← Controller (capitalized, matches folder name)
├── models/
├── views/
├── assets/
├── language/
├── migrations/
└── install.php              ← Installation logic
⚠️ CRITICAL RULES:
  • Folder name = Route name = Controller class name (e.g., routing folder → admin/routing → class Routing)
  • Controller filename MUST be capitalized: Routing.php not routing.php
  • Controller class MUST match filename exactly: class Routing extends AdminController

2. Main Module File Structure (module_name.php)

php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/*
Module Name: [Human Readable Name]
Description: [Description]
Version: 1.0.0
Requires at least: 2.0.0
*/
// Use define() NOT const for module name
define('MODULE_NAME', 'module_name');
// Register activation hook
register_activation_hook(MODULE_NAME, 'moduleNameActivation');
// Register language files
register_language_files(MODULE_NAME, [MODULE_NAME]);
// Activation function - KEEP IT SIMPLE
function moduleNameActivation()
{
    require_once(__DIR__ . '/install.php');
}
// Register hooks AFTER activation
hooks()->add_action('admin_init', 'module_init_menu_items');
hooks()->add_action('admin_init', 'module_permissions');
hooks()->add_filter('module_MODULE_NAME_action_links', 'module_MODULE_NAME_action_links');
// Menu registration
function module_init_menu_items()
{
    if (is_admin()) {
        $CI = &get_instance();
        
        // Setup menu
        $CI->app_menu->add_setup_menu_item('module-name', [
            'name' => 'Module Name',
            'href' => admin_url('module_name'), // Use folder name, not custom route
            'icon' => 'fa fa-icon',
            'position' => 35,
        ]);
        
        // Submenu
        $CI->app_menu->add_setup_children_item('module-name', [
            'slug' => 'module-settings',
            'name' => 'Settings',
            'href' => admin_url('module_name/settings'),
            'icon' => 'fa fa-cogs',
        ]);
    }
}
// Permissions function
function module_permissions()
{
    $capabilities = [];
    $capabilities['capabilities'] = [
        'view' => 'View Module',
        'manage' => 'Manage Module',
        'configure' => 'Configure Module',
    ];
    register_staff_capabilities('module_name', $capabilities, 'Module Name');
}
// Action links
function module_MODULE_NAME_action_links($actions)
{
    $actions[] = '<a href="' . admin_url('module_name') . '">Main</a>';
    $actions[] = '<a href="' . admin_url('module_name/settings') . '">Settings</a>';
    return $actions;
}

3. Controller Structure (controllers/Module_name.php)

php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Module_name extends AdminController // Class name MUST match filename
{
    public function __construct()
    {
        parent::__construct();
        
        // Admin-only access
        if (!is_admin()) {
            access_denied('admin_access_required');
        }
        
        // Load models with proper paths
        $this->load->model('module_name/model_name'); // Use folder prefix
        $this->load->model('tasks_model');
    }
    
    // Main route: /admin/module_name
    public function index()
    {
        if (!is_admin()) {
            access_denied('admin_access_required');
        }
        
        $data['title'] = 'Module Name';
        $this->load->view('main_view', $data);
    }
    
    // Settings route: /admin/module_name/settings
    public function settings()
    {
        if (!is_admin()) {
            access_denied('admin_access_required');
        }
        
        if ($this->input->post()) {
            // Handle form submission
        }
        
        $data['title'] = 'Module Settings';
        $this->load->view('settings', $data);
    }
    
    // AJAX methods
    public function ajax_method()
    {
        if (!is_admin()) {
            ajax_access_denied();
        }
        
        // AJAX logic here
        header('Content-Type: application/json');
        echo json_encode(['success' => true]);
    }
}

4. Install.php Structure

php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
// ENABLE ERROR DISPLAY BY DEFAULT (MANDATORY)
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Debug logging
log_activity('Module: Starting installation...');
try {
    // Create permissions (use register_staff_capabilities in main file, not here)
    log_activity('Module: Creating options...');
    
    // Create basic options only
    add_option('module_setting_1', 'default_value');
    add_option('module_setting_2', 'default_value');
    
    // Create custom fields with error handling
    $CI = &get_instance();
    
    if (!class_exists('Custom_fields_model')) {
        $CI->load->model('custom_fields_model');
    }
    
    // Create custom fields with try-catch
    try {
        $field_data = [
            'fieldto' => 'tasks',
            'name' => 'Field Name',
            'type' => 'checkbox',
            'required' => 0,
            'active' => 1,
            'show_on_pdf' => 0,
            'show_on_ticket_form' => 0,
            'visible_to_client' => 0,
            'disalow_client_to_edit' => 1,
            'bs_column' => 6
        ];
        
        $field_id = $CI->custom_fields_model->add($field_data);
        if ($field_id) {
            update_option('module_field_id', $field_id);
            log_activity('Module: Custom field created successfully');
        }
    } catch (Exception $e) {
        log_activity('Module: Warning - Custom field creation failed: ' . $e->getMessage());
        // Don't fail installation, just log warning
    }
    
    log_activity('Module: Installation completed successfully');
    
} catch (Exception $e) {
    // Display detailed error in browser
    log_activity('Module: Installation FAILED - ' . $e->getMessage());
    error_log('Module Installation Error: ' . $e->getMessage());
    
    echo '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 15px; margin: 10px; border-radius: 4px;">';
    echo '<h3>Module Installation Error</h3>';
    echo '<p><strong>Error:</strong> ' . htmlspecialchars($e->getMessage()) . '</p>';
    echo '<p><strong>File:</strong> ' . htmlspecialchars($e->getFile()) . '</p>';
    echo '<p><strong>Line:</strong> ' . htmlspecialchars($e->getLine()) . '</p>';
    echo '<p><strong>Stack Trace:</strong></p>';
    echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
    echo '</div>';
    
    throw $e;
}

5. Hook Integration (PHASED APPROACH)

php
// PHASE 1: Basic module loading (start here)
hooks()->add_action('admin_init', 'module_init_menu_items');
hooks()->add_action('admin_init', 'module_permissions');
// PHASE 2: Add after basic functionality works
// hooks()->add_action('after_task_form_custom_fields', 'add_module_fields');
// hooks()->add_action('after_task_table_columns', 'add_module_columns');
// PHASE 3: Add complex integrations last
// hooks()->add_action('after_cron_run', 'module_cron_job');

6. Common Mistakes to AVOID

  • ❌ Usinconst instead of define() for module name
  • ❌ Complex logic in install.php - keep it simple
  • ❌ Missing error display - always enable for debugging
  • ❌ Wrong controller naming - class must match filename exactly
  •  Wrong route URLs - use folder name, not custom names
  • ❌ Loading models without folder prefix - use module_name/model_name
  •  Complex hooks from start - add gradually after basic functionality works
  • ❌ Using _l() function - not available during module loading
  • ❌ Missing admin checks - always check is_admin() in controller methods

7. Testing Checklist

  1. ✅ Module appears in /admin/modules list
  1. ✅ Activation works without errors
  1. ✅ Menu items appear in Setup menu
  1.  Main route works (/admin/module_name)
  1. ✅ Settings route works (/admin/module_name/settings)
  1. ✅ Task creation/editing works normally
  1. ✅ Custom fields are created during installation
  1. ✅ Permissions are registered properly

8. Debugging Steps When Issues Occur

  1. Check PHP syntaxph-l module_name.php
  1. Check controller syntaxphp -l controllers/Module_name.php
  1. Check install.php syntaxphp -l install.php
  1. Verify file naming: Controller filename must be capitalized
  1. Check route URLs: Must use folder name, not custom names
  1. Enable error display: Always have error display enabled
  1. Check MAMP logs: Look for specific error messages
  1. Test basic functionality first: Don't add complex hooks until basic routes work

9. URL Structure Examples

  • Module folderrouting/
  • Main route: /admin/routing → Routing::index()
  • Settings route/admin/routing/settings → Routing::settings()
  • AJAX route: /admin/routing/ajax_method → Routing::ajax_method()

Remember: Start simple, test each phase, enable error display by default, and follow PSA's naming conventions exactly. The module folder name determines the route, and the controller class must match the filename exactly.

Did you find this article useful?

  • Introduction to modules

    The modules documentation is valid starting from version 1.2.3.2 Daedelix PSA version 1.2.3.0 comes ...
  • Module Basics

    The modules documentation is valid starting from version 2.3.2 Daedelix PSA modules use the Code...
  • Module File Headers

    Each module in Daedelix PSA consist of init file which contains the general module configuration an...
  • Create Menu Items

    If you are creating your custom modules, probably you will want to create menu items that will be s...
  • Common Module Functions

    register_activation_hook /** * Register module activation hook * @param string $module module s...