Skip to content

Creating Community Plugins

George Dawoud edited this page Feb 7, 2026 · 1 revision

Creating Community Plugins for ChurchCRM

This guide walks you through creating a community plugin for ChurchCRM. Plugins allow you to extend ChurchCRM's functionality without modifying core code.

Table of Contents


Overview

ChurchCRM uses a WordPress-style plugin architecture. Plugins can:

  • Add new pages and dashboards
  • Integrate with external services (APIs, email providers, etc.)
  • Add tabs to Person/Family views
  • Hook into events (person created, donation received, etc.)
  • Add menu items to the navigation
  • Inject JavaScript/CSS into pages

Plugin Types

Type Location Description
Core src/plugins/core/ Shipped with ChurchCRM (maintained by core team)
Community src/plugins/community/ Third-party plugins (that's you!)

Plugin Structure

Every plugin follows this structure:

src/plugins/community/my-awesome-plugin/
├── plugin.json                    # Required: Plugin manifest
├── src/
│   └── MyAwesomePluginPlugin.php  # Required: Main plugin class
├── routes/
│   └── routes.php                 # Optional: MVC and API routes
├── views/
│   └── dashboard.php              # Optional: View templates
├── assets/
│   ├── css/
│   │   └── style.css              # Optional: Plugin styles
│   └── js/
│       └── plugin.js              # Optional: Plugin JavaScript
├── help.json                      # Optional: Help documentation
└── README.md                      # Optional: Developer documentation

Step-by-Step Guide

Step 1: Create the Plugin Directory

mkdir -p src/plugins/community/my-awesome-plugin/src
mkdir -p src/plugins/community/my-awesome-plugin/routes
mkdir -p src/plugins/community/my-awesome-plugin/views

Step 2: Create the Manifest (plugin.json)

Create src/plugins/community/my-awesome-plugin/plugin.json:

{
    "id": "my-awesome-plugin",
    "name": "My Awesome Plugin",
    "description": "A brief description of what your plugin does",
    "version": "1.0.0",
    "author": "Your Name",
    "authorUrl": "https://your-website.com",
    "type": "community",
    "minimumCRMVersion": "7.0.0",
    "mainClass": "ChurchCRM\\Plugins\\MyAwesomePlugin\\MyAwesomePluginPlugin",
    "dependencies": [],
    "settingsUrl": null,
    "routesFile": "routes/routes.php",
    "settings": [],
    "menuItems": [],
    "hooks": []
}

Step 3: Create the Main Plugin Class

Create src/plugins/community/my-awesome-plugin/src/MyAwesomePluginPlugin.php:

<?php

namespace ChurchCRM\Plugins\MyAwesomePlugin;

use ChurchCRM\Plugin\AbstractPlugin;

class MyAwesomePluginPlugin extends AbstractPlugin
{
    private static ?MyAwesomePluginPlugin $instance = null;

    public function __construct(string $basePath = '')
    {
        parent::__construct($basePath);
        self::$instance = $this;
    }

    public static function getInstance(): ?MyAwesomePluginPlugin
    {
        return self::$instance;
    }

    public function getId(): string
    {
        return 'my-awesome-plugin';
    }

    public function getName(): string
    {
        return 'My Awesome Plugin';
    }

    public function getDescription(): string
    {
        return 'A brief description of what your plugin does';
    }

    public function boot(): void
    {
        // Called every request when plugin is active
        // Initialize services, register hooks here
    }

    public function activate(): void
    {
        // Called when plugin is enabled
        // Run setup tasks, create database tables, etc.
    }

    public function deactivate(): void
    {
        // Called when plugin is disabled
        // Clean up temporary data
    }

    public function uninstall(): void
    {
        // Called when plugin is removed
        // Delete all plugin data, drop tables, etc.
    }
}

Step 4: Enable Your Plugin

  1. Go to Admin → Plugin Management (/plugins/management)
  2. Find your plugin in the list
  3. Click Enable

Plugin Manifest (plugin.json)

The manifest defines your plugin's metadata and capabilities.

Required Fields

Field Type Description
id string Unique identifier (lowercase, hyphens only)
name string Human-readable name
description string Brief description
version string Semantic version (e.g., "1.0.0")
author string Author name
type string Always "community" for third-party plugins
minimumCRMVersion string Minimum ChurchCRM version required
mainClass string Fully-qualified class name

Optional Fields

Field Type Description
authorUrl string Author's website
dependencies array Other plugin IDs this plugin requires
settingsUrl string Custom settings page URL (or null for inline)
routesFile string Path to routes file (relative to plugin root)
settings array Settings schema for admin UI
menuItems array Menu items to add to navigation
hooks array Hooks this plugin listens to

Settings Schema

Define settings that appear in the Plugin Management UI:

{
    "settings": [
        {
            "key": "apiKey",
            "label": "API Key",
            "type": "password",
            "required": true,
            "help": "Enter your API key from the service dashboard"
        },
        {
            "key": "enableFeatureX",
            "label": "Enable Feature X",
            "type": "boolean",
            "required": false,
            "help": "Turn on advanced features"
        },
        {
            "key": "webhookUrl",
            "label": "Webhook URL",
            "type": "text",
            "required": false,
            "help": "URL to receive notifications"
        }
    ]
}

Setting Types: text, password, boolean, number, select

Menu Items

Add items to ChurchCRM's navigation:

{
    "menuItems": [
        {
            "parent": "admin",
            "label": "My Plugin Dashboard",
            "url": "/plugins/my-awesome-plugin/dashboard",
            "icon": "fa-solid fa-plug",
            "permission": "bAdmin"
        }
    ]
}

Parent Options: calendar, people, groups, sundayschool, email, events, deposits, fundraiser, reports, admin, custom


Main Plugin Class

Your plugin class must extend AbstractPlugin and implement required methods.

Required Methods

public function getId(): string;        // Return plugin ID
public function getName(): string;      // Return display name
public function getDescription(): string; // Return description

Lifecycle Methods

public function boot(): void
{
    // Called on every request when plugin is active
    // - Initialize services
    // - Register hooks
    // - Set up autoloaders
}

public function activate(): void
{
    // Called when admin enables the plugin
    // - Create database tables
    // - Initialize default settings
    // - Run migrations
}

public function deactivate(): void
{
    // Called when admin disables the plugin
    // - Clean up temporary files
    // - Cancel scheduled tasks
    // Note: Don't delete user data here!
}

public function uninstall(): void
{
    // Called when plugin is completely removed
    // - Drop database tables
    // - Delete all plugin data
    // - Remove config entries
}

Configuration Methods

public function isConfigured(): bool
{
    // Return true if plugin has all required settings
    return !empty($this->getConfigValue('apiKey'));
}

public function getConfigurationError(): ?string
{
    // Return error message if misconfigured, null if OK
    if (empty($this->getConfigValue('apiKey'))) {
        return gettext('API Key is required');
    }
    return null;
}

public function getSettingsSchema(): array
{
    // Define settings for the admin UI
    return [
        [
            'key' => 'apiKey',
            'label' => gettext('API Key'),
            'type' => 'password',
            'required' => true,
            'help' => gettext('Get this from your account settings'),
        ],
    ];
}

Menu & UI Methods

public function getMenuItems(): array
{
    return [
        [
            'parent' => 'admin',
            'label' => gettext('My Plugin'),
            'url' => 'plugins/my-awesome-plugin/dashboard',
            'icon' => 'fa-solid fa-plug',
        ],
    ];
}

public function getHeadContent(): string
{
    // Return HTML/JS to inject into <head>
    return '<link rel="stylesheet" href="' . $this->getAssetUrl('css/style.css') . '">';
}

public function getFooterContent(): string
{
    // Return HTML/JS to inject before </body>
    return '<script src="' . $this->getAssetUrl('js/plugin.js') . '"></script>';
}

public function getClientConfig(): array
{
    // Data exposed to JavaScript as window.CRM.plugins.{pluginId}
    return [
        'apiEndpoint' => '/plugins/my-awesome-plugin/api',
        'maxItems' => 100,
    ];
}

Singleton Pattern

Always implement the singleton pattern for route access:

private static ?MyAwesomePluginPlugin $instance = null;

public function __construct(string $basePath = '')
{
    parent::__construct($basePath);
    self::$instance = $this;
}

public static function getInstance(): ?MyAwesomePluginPlugin
{
    return self::$instance;
}

Adding Routes

Create routes/routes.php to add pages and API endpoints.

Basic Structure

<?php

use ChurchCRM\dto\SystemURLs;
use ChurchCRM\Plugins\MyAwesomePlugin\MyAwesomePluginPlugin;
use ChurchCRM\Slim\SlimUtils;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteCollectorProxy;
use Slim\Views\PhpRenderer;

// Get plugin instance
$plugin = MyAwesomePluginPlugin::getInstance();
if ($plugin === null) {
    return; // Safety check - don't register routes if plugin not loaded
}

// MVC Routes (HTML pages)
$app->get('/my-awesome-plugin/dashboard', function (Request $request, Response $response) use ($plugin): Response {
    $renderer = new PhpRenderer(__DIR__ . '/../views/');
    
    return $renderer->render($response, 'dashboard.php', [
        'sRootPath' => SystemURLs::getRootPath(),
        'sPageTitle' => gettext('My Plugin Dashboard'),
        'pluginData' => $plugin->getData(),
    ]);
});

// API Routes (JSON endpoints)
$app->group('/my-awesome-plugin/api', function (RouteCollectorProxy $group) use ($plugin): void {
    
    // GET /plugins/my-awesome-plugin/api/items
    $group->get('/items', function (Request $request, Response $response) use ($plugin): Response {
        $items = $plugin->getItems();
        return SlimUtils::renderJSON($response, ['success' => true, 'data' => $items]);
    });
    
    // POST /plugins/my-awesome-plugin/api/items
    $group->post('/items', function (Request $request, Response $response) use ($plugin): Response {
        $body = $request->getParsedBody();
        
        try {
            $item = $plugin->createItem($body);
            return SlimUtils::renderJSON($response, ['success' => true, 'data' => $item]);
        } catch (\Exception $e) {
            return SlimUtils::renderErrorJSON($response, $e->getMessage(), [], 400, $e, $request);
        }
    });
    
    // DELETE /plugins/my-awesome-plugin/api/items/{id}
    $group->delete('/items/{id}', function (Request $request, Response $response, array $args) use ($plugin): Response {
        $plugin->deleteItem((int) $args['id']);
        return SlimUtils::renderJSON($response, ['success' => true]);
    });
});

Route Security

Routes are only loaded when the plugin is active. For additional security:

use ChurchCRM\Authentication\AuthenticationManager;
use ChurchCRM\Slim\Middleware\Request\Auth\AdminRoleAuthMiddleware;

// Admin-only routes
$app->group('/my-awesome-plugin/admin', function (RouteCollectorProxy $group) use ($plugin): void {
    $group->get('/settings', function ($request, $response) use ($plugin): Response {
        // Admin settings page
    });
})->add(AdminRoleAuthMiddleware::class);

// Check permissions in route handler
$app->post('/my-awesome-plugin/action', function (Request $request, Response $response) use ($plugin): Response {
    $user = AuthenticationManager::getCurrentUser();
    if (!$user->isAdmin()) {
        return SlimUtils::renderErrorJSON($response, gettext('Admin access required'), [], 403);
    }
    // ... handle action
});

Adding Views

Create PHP view templates in the views/ directory.

Basic View Template

Create views/dashboard.php:

<?php

use ChurchCRM\dto\SystemURLs;

// Include ChurchCRM header (navigation, CSS, etc.)
require SystemURLs::getDocumentRoot() . '/Include/Header.php';
?>

<!-- Breadcrumb Navigation -->
<div class="row mb-3">
    <div class="col-12">
        <nav aria-label="breadcrumb">
            <ol class="breadcrumb mb-0 bg-light">
                <li class="breadcrumb-item">
                    <a href="<?= SystemURLs::getRootPath() ?>/v2/dashboard">
                        <i class="fa-solid fa-home"></i>
                    </a>
                </li>
                <li class="breadcrumb-item">
                    <a href="<?= SystemURLs::getRootPath() ?>/plugins"><?= gettext('Plugins') ?></a>
                </li>
                <li class="breadcrumb-item active"><?= gettext('My Plugin') ?></li>
            </ol>
        </nav>
    </div>
</div>

<!-- Main Content -->
<div class="row">
    <div class="col-lg-8">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">
                    <i class="fa-solid fa-plug mr-2"></i><?= gettext('Dashboard') ?>
                </h3>
            </div>
            <div class="card-body">
                <p><?= gettext('Welcome to My Awesome Plugin!') ?></p>
                
                <!-- Data Table -->
                <table id="itemsTable" class="table table-striped">
                    <thead>
                        <tr>
                            <th><?= gettext('Name') ?></th>
                            <th><?= gettext('Status') ?></th>
                            <th><?= gettext('Actions') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <!-- Populated via JavaScript -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    
    <div class="col-lg-4">
        <div class="card card-outline card-info">
            <div class="card-header">
                <h3 class="card-title"><?= gettext('Quick Stats') ?></h3>
            </div>
            <div class="card-body">
                <ul class="list-unstyled">
                    <li><strong><?= gettext('Total Items') ?>:</strong> <span id="totalItems">0</span></li>
                    <li><strong><?= gettext('Active') ?>:</strong> <span id="activeItems">0</span></li>
                </ul>
            </div>
        </div>
    </div>
</div>

<!-- JavaScript -->
<script nonce="<?= SystemURLs::getCSPNonce() ?>">
$(document).ready(function() {
    // Load data via AJAX
    $.ajax({
        url: window.CRM.root + '/plugins/my-awesome-plugin/api/items',
        method: 'GET',
        success: function(response) {
            if (response.success) {
                renderItems(response.data);
            }
        },
        error: function() {
            window.CRM.notify(i18next.t('Failed to load data'), { type: 'error' });
        }
    });
    
    function renderItems(items) {
        var tbody = $('#itemsTable tbody');
        tbody.empty();
        
        items.forEach(function(item) {
            tbody.append(
                '<tr>' +
                '<td>' + item.name + '</td>' +
                '<td><span class="badge badge-' + (item.active ? 'success' : 'secondary') + '">' + 
                    (item.active ? i18next.t('Active') : i18next.t('Inactive')) + '</span></td>' +
                '<td><button class="btn btn-sm btn-danger delete-btn" data-id="' + item.id + '">' +
                    '<i class="fa-solid fa-trash"></i></button></td>' +
                '</tr>'
            );
        });
        
        $('#totalItems').text(items.length);
        $('#activeItems').text(items.filter(i => i.active).length);
    }
    
    // Delete handler
    $(document).on('click', '.delete-btn', function() {
        var id = $(this).data('id');
        if (confirm(i18next.t('Are you sure?'))) {
            $.ajax({
                url: window.CRM.root + '/plugins/my-awesome-plugin/api/items/' + id,
                method: 'DELETE',
                success: function() {
                    window.CRM.notify(i18next.t('Item deleted'), { type: 'success' });
                    location.reload();
                }
            });
        }
    });
});
</script>

<?php
// Include ChurchCRM footer
require SystemURLs::getDocumentRoot() . '/Include/Footer.php';

UI Guidelines

  • Use Bootstrap 4.6.2 classes (not Bootstrap 5)
  • Use AdminLTE card components
  • Use window.CRM.notify() for notifications (not alert())
  • Use i18next.t() for JavaScript translations
  • Use gettext() for PHP translations
  • Include CSP nonce on script tags: nonce="<?= SystemURLs::getCSPNonce() ?>"

Configuration & Settings

Accessing Config Values

Plugins can only access their own config (sandboxed to plugin.{id}.*):

// In your plugin class

// Get a string value
$apiKey = $this->getConfigValue('apiKey');

// Get a boolean value
$enabled = $this->getBooleanConfigValue('enableFeatureX');

// Set a value
$this->setConfigValue('lastSyncTime', date('c'));

Config Storage

Settings are stored in the config_cfg table with keys like:

  • plugin.my-awesome-plugin.apiKey
  • plugin.my-awesome-plugin.enableFeatureX
  • plugin.my-awesome-plugin.enabled (plugin active state)

Using Hooks

Hooks let you respond to events in ChurchCRM.

Registering Hooks

In your boot() method:

use ChurchCRM\Plugin\Hook\HookManager;
use ChurchCRM\Plugin\Hooks;

public function boot(): void
{
    // Register action hooks
    HookManager::addAction(Hooks::PERSON_CREATED, [$this, 'onPersonCreated']);
    HookManager::addAction(Hooks::PERSON_UPDATED, [$this, 'onPersonUpdated']);
    HookManager::addAction(Hooks::DONATION_RECEIVED, [$this, 'onDonationReceived']);
}

Hook Handlers

public function onPersonCreated($person): void
{
    // $person is the Person model object
    $this->syncToExternalService($person);
}

public function onPersonUpdated($person, array $oldData): void
{
    // $oldData contains previous values
    if ($oldData['email'] !== $person->getEmail()) {
        $this->updateEmailInExternalService($person);
    }
}

public function onDonationReceived($payment, $deposit): void
{
    // Send thank you email, update external system, etc.
    $this->sendThankYouEmail($payment);
}

Available Hooks

Hook Constant Parameters Description
PERSON_CREATED $person After person record created
PERSON_UPDATED $person, $oldData After person record updated
PERSON_DELETED $personId, $personData After person deleted
FAMILY_CREATED $family After family created
FAMILY_UPDATED $family, $oldData After family updated
DONATION_RECEIVED $payment, $deposit After donation recorded
GROUP_MEMBER_ADDED $membership, $group, $person Person joined group
GROUP_MEMBER_REMOVED $personId, $group Person left group
EVENT_CHECKIN $person, $event Person checked into event

Client-Side JavaScript

Accessing Plugin Config

Data from getClientConfig() is available in JavaScript:

// Access your plugin's client config
var config = window.CRM.plugins['my-awesome-plugin'];
console.log(config.apiEndpoint); // '/plugins/my-awesome-plugin/api'

Making API Calls

// Using jQuery (available globally)
$.ajax({
    url: window.CRM.root + '/plugins/my-awesome-plugin/api/items',
    method: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({ name: 'New Item' }),
    success: function(response) {
        if (response.success) {
            window.CRM.notify(i18next.t('Item created!'), { type: 'success' });
        }
    },
    error: function(xhr) {
        var error = xhr.responseJSON?.message || i18next.t('An error occurred');
        window.CRM.notify(error, { type: 'error' });
    }
});

Notifications

Always use window.CRM.notify() instead of alert():

// Success
window.CRM.notify(i18next.t('Operation completed'), { type: 'success' });

// Error
window.CRM.notify(i18next.t('Something went wrong'), { type: 'error' });

// Warning
window.CRM.notify(i18next.t('Please check your input'), { type: 'warning' });

// Info
window.CRM.notify(i18next.t('Did you know...'), { type: 'info' });

Best Practices

Do's ✅

  • Use namespaces: ChurchCRM\Plugins\YourPlugin\
  • Use Propel ORM for database access
  • Use gettext() for all user-facing strings
  • Use i18next.t() for JavaScript strings
  • Validate all input before processing
  • Handle errors gracefully with try/catch
  • Log important events using LoggerUtils::getAppLogger()
  • Follow PSR-12 coding standards
  • Test thoroughly before releasing

Don'ts ❌

  • Don't use raw SQL - use Propel Query classes
  • Don't modify core files - use hooks instead
  • Don't hardcode paths - use SystemURLs::getRootPath()
  • Don't use alert() - use window.CRM.notify()
  • Don't store sensitive data in client config
  • Don't skip security checks for sensitive operations

Security Guidelines

  1. Validate all input - never trust user data
  2. Escape output - prevent XSS attacks
  3. Check permissions - verify user has access
  4. Use prepared statements - Propel handles this
  5. Sanitize file uploads - check types and sizes

Testing Your Plugin

Manual Testing

  1. Enable your plugin in Admin → Plugin Management
  2. Check that menu items appear
  3. Navigate to your plugin's pages
  4. Test all CRUD operations
  5. Test with different user roles
  6. Check browser console for JavaScript errors
  7. Check src/logs/ for PHP errors

Debugging Tips

// Log debug info
use ChurchCRM\Utils\LoggerUtils;

$logger = LoggerUtils::getAppLogger();
$logger->debug('My plugin action', ['data' => $someData]);
$logger->info('Important event happened');
$logger->error('Something went wrong', ['error' => $e->getMessage()]);

Check logs at: src/logs/YYYY-MM-DD-app.log


Publishing Your Plugin

Before Publishing

  • Test on a fresh ChurchCRM install
  • Test with minimum supported CRM version
  • Remove debug/test code
  • Update version number in plugin.json
  • Write clear documentation (README.md)
  • Add help.json for in-app documentation

Distribution

Community plugins can be distributed via:

  1. GitHub Repository - Users clone/download to src/plugins/community/
  2. ZIP File - Users extract to src/plugins/community/

Plugin Directory Structure for Distribution

my-awesome-plugin-v1.0.0.zip
└── my-awesome-plugin/
    ├── plugin.json
    ├── src/
    │   └── MyAwesomePluginPlugin.php
    ├── routes/
    │   └── routes.php
    ├── views/
    │   └── dashboard.php
    ├── help.json
    └── README.md

Installation Instructions for Users

1. Download my-awesome-plugin-v1.0.0.zip
2. Extract to src/plugins/community/
3. Go to Admin → Plugin Management
4. Find "My Awesome Plugin" and click Enable
5. Configure settings as needed

Example Plugins

Study these core plugins for reference:

Plugin Complexity Features
gravatar Simple Config-only, no routes
google-analytics Simple Head content injection
mailchimp Complex Routes, views, API integration, hooks

Browse the source at: src/plugins/core/


Getting Help


Last updated: February 2026 | ChurchCRM 7.x

Clone this wiki locally