Plugin Development

FearlessCMS Plugin Development Guide

Introduction

This guide will walk you through creating plugins for FearlessCMS. Plugins allow you to extend the functionality of your site without modifying the core code, making your customizations more maintainable and portable.

Plugin Structure

Each plugin must follow this basic structure:

plugins/
  your-plugin-name/
    your-plugin-name.php  (Main plugin file - required)
    assets/               (Optional directory for CSS, JS, images)
    includes/            (Optional directory for additional PHP files)

The plugin folder name and main PHP file name must match. For example, if your plugin is called "gallery", the structure would be:

plugins/
  gallery/
    gallery.php

Creating Your First Plugin

Let's create a simple "Hello World" plugin:

  1. Create a directory plugins/hello-world/
  2. Create a file plugins/hello-world/hello-world.php

Here's a basic plugin template:

<?php
/*
Plugin Name: Hello World
Description: A simple example plugin for FearlessCMS
Version: 1.0
Author: Your Name
*/

// Plugin code goes here

// Add a route for /hello
fcms_add_hook('route', function (&$handled, &$title, &$content, $path) {
    if ($path === 'hello') {
        $title = 'Hello World';
        $content = '<p>Hello from my first FearlessCMS plugin!</p>';
        $handled = true;
    }
});

Activating Your Plugin

To activate your plugin:

  1. Log in to the admin panel
  2. Go to Plugins
  3. Find your plugin and click "Activate"

Alternatively, you can manually add your plugin name to the admin/config/plugins.json file:

["blog", "hello-world"]

The Hook System

FearlessCMS uses a hook system that allows plugins to extend functionality at specific points in the code execution. Here are the available hooks:

Hook Description
init Triggered when the plugin system initializes
route Used to handle custom URL routes
before_render Triggered before the page template is rendered
after_render Triggered after the page template is rendered
before_content Triggered before the main content is processed
after_content Triggered after the main content is processed
check_permission Used for custom permission checks
filter_admin_sections For modifying admin sections

Using Hooks

To add your code to a hook, use the fcms_add_hook() function:

fcms_add_hook('hook_name', function (...$args) {
    // Your code here
});

Different hooks receive different arguments. For example, the route hook receives these arguments:

fcms_add_hook('route', function (&$handled, &$title, &$content, $path) {
    // $handled - Set to true if your plugin handles this route
    // $title - The page title (modify this if handling the route)
    // $content - The page content (modify this if handling the route)
    // $path - The current URL path
});

Note the & before variable names - these are passed by reference, allowing your plugin to modify them.

Adding Admin Pages

Plugins can add their own sections to the admin panel:

fcms_register_admin_section('my-plugin', [
    'label' => 'My Plugin',
    'menu_order' => 50, // Lower numbers appear earlier in the menu
    'render_callback' => function() {
        // Return HTML for your admin page
        return '<h2>My Plugin Settings</h2><p>Configure your plugin here.</p>';
    }
]);

Plugin Data Storage

For storing plugin data, you can create JSON files in the content directory:

define('MY_PLUGIN_DATA_FILE', CONTENT_DIR . '/my_plugin_data.json');

function my_plugin_save_data($data) {
    file_put_contents(MY_PLUGIN_DATA_FILE, json_encode($data, JSON_PRETTY_PRINT));
}

function my_plugin_load_data() {
    if (!file_exists(MY_PLUGIN_DATA_FILE)) return [];
    return json_decode(file_get_contents(MY_PLUGIN_DATA_FILE), true) ?: [];
}

Advanced Plugin Features

Permission System

Plugins can integrate with FearlessCMS's permission system:

// Check if a user has a specific permission
if (fcms_check_permission($username, 'manage_plugins')) {
    // User has permission
}

// Register custom permissions
fcms_add_hook('check_permission', function($username, $capability, $args) {
    if ($capability === 'my_custom_permission') {
        // Implement custom permission logic
        return true; // or false
    }
    return null; // Let other hooks handle the permission
});

Template Integration

Plugins can modify templates and add custom assets:

// Add custom CSS/JS to the page
fcms_add_hook('before_render', function(&$template, $path) {
    // Add custom CSS
    $custom_css = '<style>.my-plugin-class { color: blue; }</style>';
    
    // Add custom JavaScript
    $custom_js = '<script>console.log("My plugin loaded");</script>';
    
    // The template will automatically include these
    $GLOBALS['custom_css'] = $custom_css;
    $GLOBALS['custom_js'] = $custom_js;
});

Admin Section Best Practices

When creating admin sections:

  1. Use proper menu ordering:
fcms_register_admin_section('my-plugin', [
    'label' => 'My Plugin',
    'menu_order' => 50, // Lower numbers appear earlier
    'render_callback' => function() {
        // Handle form submissions
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            // Process form data
        }
        
        // Return admin page HTML
        return '<h2>My Plugin Settings</h2>';
    }
]);
  1. Handle form submissions securely:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!fcms_check_permission($_SESSION['username'], 'manage_settings')) {
        return '<div class="error">Permission denied</div>';
    }
    
    // Process form data
    $data = [
        'setting1' => trim($_POST['setting1'] ?? ''),
        'setting2' => trim($_POST['setting2'] ?? '')
    ];
    
    // Save to JSON file
    file_put_contents(MY_PLUGIN_CONFIG, json_encode($data, JSON_PRETTY_PRINT));
}

Plugin Activation/Deactivation

Plugins should handle their own activation and deactivation:

// Check if plugin is being activated
if (isset($_POST['action']) && $_POST['action'] === 'activate_plugin') {
    // Create necessary directories
    if (!is_dir(MY_PLUGIN_DATA_DIR)) {
        mkdir(MY_PLUGIN_DATA_DIR, 0755, true);
    }
    
    // Initialize default settings
    if (!file_exists(MY_PLUGIN_CONFIG)) {
        file_put_contents(MY_PLUGIN_CONFIG, json_encode([
            'default_setting' => 'value'
        ], JSON_PRETTY_PRINT));
    }
}

// Check if plugin is being deactivated
if (isset($_POST['action']) && $_POST['action'] === 'deactivate_plugin') {
    // Clean up temporary files
    // Note: Don't delete user data unless explicitly requested
}

Advanced Example: Contact Form Plugin

Here's a more complete example of a contact form plugin:

<?php
/*
Plugin Name: Contact Form
Description: Adds a contact form to your FearlessCMS site
Version: 1.0
Author: Your Name
*/

define('CONTACT_FORM_DATA_FILE', CONTENT_DIR . '/contact_form_submissions.json');

// Load submissions
function contact_form_load_submissions() {
    if (!file_exists(CONTACT_FORM_DATA_FILE)) return [];
    return json_decode(file_get_contents(CONTACT_FORM_DATA_FILE), true) ?: [];
}

// Save submissions
function contact_form_save_submissions($submissions) {
    file_put_contents(CONTACT_FORM_DATA_FILE, json_encode($submissions, JSON_PRETTY_PRINT));
}

// Add admin section
fcms_register_admin_section('contact-form', [
    'label' => 'Contact Form',
    'menu_order' => 45,
    'render_callback' => function() {
        $submissions = contact_form_load_submissions();

        $html = '<h2 class="text-2xl font-bold mb-6">Contact Form Submissions</h2>';

        if (empty($submissions)) {
            $html .= '<p>No submissions yet.</p>';
        } else {
            $html .= '<table class="w-full border-collapse">';
            $html .= '<tr><th class="border-b py-2">Date</th><th class="border-b py-2">Name</th><th class="border-b py-2">Email</th><th class="border-b py-2">Message</th></tr>';

            foreach ($submissions as $submission) {
                $html .= '<tr>';
                $html .= '<td class="py-2 border-b">' . htmlspecialchars($submission['date']) . '</td>';
                $html .= '<td class="py-2 border-b">' . htmlspecialchars($submission['name']) . '</td>';
                $html .= '<td class="py-2 border-b">' . htmlspecialchars($submission['email']) . '</td>';
                $html .= '<td class="py-2 border-b">' . htmlspecialchars($submission['message']) . '</td>';
                $html .= '</tr>';
            }

            $html .= '</table>';
        }

        return $html;
    }
]);

// Add route for contact form
fcms_add_hook('route', function (&$handled, &$title, &$content, $path) {
    if ($path === 'contact') {
        $title = 'Contact Us';
        $formSubmitted = false;
        $error = '';

        // Handle form submission
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_submit'])) {
            $name = trim($_POST['name'] ?? '');
            $email = trim($_POST['email'] ?? '');
            $message = trim($_POST['message'] ?? '');

            if (empty($name) || empty($email) || empty($message)) {
                $error = 'All fields are required.';
            } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                $error = 'Please enter a valid email address.';
            } else {
                // Save the submission
                $submissions = contact_form_load_submissions();
                $submissions[] = [
                    'date' => date('Y-m-d H:i:s'),
                    'name' => $name,
                    'email' => $email,
                    'message' => $message
                ];
                contact_form_save_submissions($submissions);
                $formSubmitted = true;
            }
        }

        // Display the form or success message
        if ($formSubmitted) {
            $content = '<div class="bg-green-100 text-green-700 p-4 rounded mb-4">Thank you for your message! We\'ll get back to you soon.</div>';
        } else {
            $errorHtml = !empty($error) ? '<div class="bg-red-100 text-red-700 p-4 rounded mb-4">' . htmlspecialchars($error) . '</div>' : '';

            $content = '
                <h2>Contact Us</h2>
                ' . $errorHtml . '
                <form method="post" class="space-y-4">
                    <div>
                        <label class="block mb-1">Your Name:</label>
                        <input type="text" name="name" value="' . htmlspecialchars($_POST['name'] ?? '') . '" class="w-full px-3 py-2 border rounded">
                    </div>
                    <div>
                        <label class="block mb-1">Your Email:</label>
                        <input type="email" name="email" value="' . htmlspecialchars($_POST['email'] ?? '') . '" class="w-full px-3 py-2 border rounded">
                    </div>
                    <div>
                        <label class="block mb-1">Message:</label>
                        <textarea name="message" rows="5" class="w-full px-3 py-2 border rounded">' . htmlspecialchars($_POST['message'] ?? '') . '</textarea>
                    </div>
                    <div>
                        <button type="submit" name="contact_submit" value="1" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Send Message</button>
                    </div>
                </form>
            ';
        }

        $handled = true;
    }
});

// Add shortcode for embedding the contact form
fcms_add_hook('before_render', function (&$title, &$content, &$template) {
    // Replace [contact_form] shortcode with the actual form
    if (strpos($content, '[contact_form]') !== false) {
        // Create a temporary route handling to get the form content
        $tempTitle = '';
        $tempContent = '';
        $tempHandled = false;
        $tempHook = function (&$handled, &$title, &$content, $path) {
            if ($path === 'contact') {
                // This will set $content to the form HTML
                $handled = true;
            }
        };

        // Call our route hook manually
        $tempHook($tempHandled, $tempTitle, $tempContent, 'contact');

        // Replace the shortcode with the form
        $content = str_replace('[contact_form]', $tempContent, $content);
    }
});

Best Practices

Security Best Practices

  1. Always validate and sanitize user input
  2. Use proper permission checks
  3. Store sensitive data securely
  4. Use prepared statements for database operations
  5. Implement proper error handling
  6. Follow the principle of least privilege

Performance Considerations

  1. Load assets only when needed
  2. Cache expensive operations
  3. Use efficient data structures
  4. Minimize database queries
  5. Implement proper cleanup on deactivation

Code Organization

  1. Prefix Functions: Prefix your functions with your plugin name to avoid conflicts (e.g., my_plugin_function_name).
  2. Check for Dependencies: If your plugin depends on other plugins or PHP extensions, check for them early:
if (!function_exists('required_function')) {
    // Display an error or gracefully degrade
}
  1. Clean Uninstall: Consider adding an uninstall function that removes any data your plugin created.
  2. Minimal Core Modifications: Never modify core files. Use hooks for everything.
  3. Documentation: Include clear documentation in your plugin code.

Troubleshooting

If your plugin isn't working:

  1. Check PHP error logs for any errors
  2. Verify the plugin is properly activated
  3. Make sure your plugin folder and main file have the same name
  4. Check that all required hooks are being used correctly
  5. Verify file permissions are set correctly
  6. Check for conflicts with other plugins

Conclusion

This guide covers the basics of creating plugins for FearlessCMS. As you become more familiar with the system, you can create increasingly complex plugins to extend your site's functionality.

Remember that FearlessCMS is designed to be lightweight and simple, so aim to keep your plugins focused and efficient, following the same philosophy.

Happy coding!