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:
- Create a directory
plugins/hello-world/
- 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:
- Log in to the admin panel
- Go to Plugins
- 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:
- 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>';
}
]);
- 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
- Always validate and sanitize user input
- Use proper permission checks
- Store sensitive data securely
- Use prepared statements for database operations
- Implement proper error handling
- Follow the principle of least privilege
Performance Considerations
- Load assets only when needed
- Cache expensive operations
- Use efficient data structures
- Minimize database queries
- Implement proper cleanup on deactivation
Code Organization
- Prefix Functions: Prefix your functions with your plugin name to avoid conflicts (e.g.,
my_plugin_function_name
). - 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
}
- Clean Uninstall: Consider adding an uninstall function that removes any data your plugin created.
- Minimal Core Modifications: Never modify core files. Use hooks for everything.
- Documentation: Include clear documentation in your plugin code.
Troubleshooting
If your plugin isn't working:
- Check PHP error logs for any errors
- Verify the plugin is properly activated
- Make sure your plugin folder and main file have the same name
- Check that all required hooks are being used correctly
- Verify file permissions are set correctly
- 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!