Skip to the content.

← Back to Table of Contents

5. Plugin Architecture

Last Updated: July 17, 2025
Status: Complete
Phase: 4 - Plugin Architecture Documentation

The Kingdom Management Portal (KMP) employs a sophisticated plugin architecture that extends CakePHP’s BasePlugin system with organizational management-specific features. This architecture enables modular development, conditional loading, and proper dependency management for complex organizational workflows.

Table of Contents

Core Plugin Infrastructure

KMPPluginInterface

The KMPPluginInterface serves as the foundation contract for all KMP plugins, defining the essential requirements for plugin integration:

interface KMPPluginInterface
{
    public function getMigrationOrder(): int;
}

Migration Order System

The migration order system ensures plugins are initialized in the correct sequence to handle:

Standard Migration Orders:

Plugin Registration Pattern

All KMP plugins are registered in config/plugins.php with standardized configuration:

'PluginName' => [
    'migrationOrder' => 1,      // Database initialization order
    'dependencies' => [],        // Optional: explicit dependencies
    'conditional' => true,       // Optional: conditional loading
],

Standard Plugin Structure

KMP plugins follow a consistent directory structure for maintainability:

plugins/PluginName/
├── assets/                     # Frontend assets
│   ├── css/                   # Plugin-specific stylesheets
│   └── js/controllers/        # Stimulus.js controllers
├── config/                    # Plugin configuration files
│   ├── routes.php            # Plugin-specific routes
│   └── Migrations/           # Database migrations
├── src/                       # Plugin source code
│   ├── Controller/           # Controllers
│   ├── Model/                # Models (Entity, Table)
│   ├── Services/             # Business logic services
│   ├── Event/                # Event handlers
│   └── PluginNamePlugin.php  # Main plugin class
├── templates/                 # Plugin templates
├── tests/                     # Plugin tests
│   ├── TestCase/             # Unit and integration tests
│   └── Fixture/              # Test fixtures
└── webroot/                   # Public plugin assets

Plugin Registration System

Configuration Management

Plugins are configured in config/plugins.php with comprehensive metadata:

/**
 * KMP Plugin Registry Configuration
 *
 * Plugin Categories:
 * 1. Development Tools (DebugKit, Bake, Tools)
 * 2. Database Management (Migrations, Muffin/Trash, Muffin/Footprint)
 * 3. UI Framework (Bootstrap, BootstrapUI, AssetMix)
 * 4. Security & Auth (Authentication, Authorization)
 * 5. Core KMP Features (Activities, Awards, Officers)
 * 6. Utility Plugins (Queue, CsvView, ADmad/Glide, GitHubIssueSubmitter)
 */

return [
    'Activities' => [
        'migrationOrder' => 1,
        'description' => 'Activity authorization and participation tracking',
        'category' => 'Core KMP Features',
        'required' => true,
    ],
    'Awards' => [
        'migrationOrder' => 2,
        'description' => 'Award recommendation and ceremony management',
        'category' => 'Core KMP Features',
        'dependencies' => ['Activities'],
    ],
    // ... additional plugins
];

Conditional Loading

Plugins can be conditionally loaded based on environment or configuration:

// Environment-specific loading
if (env('DEBUG', false)) {
    $plugins['DebugKit'] = ['bootstrap' => true];
}

// Feature flag loading
if (StaticHelpers::getAppSetting('Plugin.Awards.Active', 'yes') === 'yes') {
    $plugins['Awards'] = ['migrationOrder' => 2];
}

Integration Points

Plugins integrate with the KMP navigation system through the NavigationRegistry:

NavigationRegistry::register(
    'PluginName',
    [], // Static navigation items
    function ($user, $params) {
        return PluginNavigationProvider::getNavigationItems($user, $params);
    }
);

Navigation Provider Pattern:

class PluginNavigationProvider
{
    public static function getNavigationItems($user, $params): array
    {
        $items = [];
        
        // Permission-based navigation
        if ($user->can('index', 'PluginController')) {
            $items[] = [
                'title' => 'Plugin Dashboard',
                'url' => '/plugin',
                'icon' => 'fas fa-chart-bar',
                'badge' => PluginService::getPendingCount($user),
            ];
        }
        
        return $items;
    }
}

View Cell Integration

Plugins register view cells for dashboard and page integration:

ViewCellRegistry::register(
    'PluginName',
    [], // Static view cells
    function ($urlParams, $user) {
        return PluginViewCellProvider::getViewCells($urlParams, $user);
    }
);

View Cell Provider Pattern:

class PluginViewCellProvider
{
    public static function getViewCells($urlParams, $user): array
    {
        $cells = [];
        
        // Context-sensitive cells
        if ($urlParams['controller'] === 'Members' && $urlParams['action'] === 'view') {
            $cells[] = [
                'cell' => 'Plugin.MemberDetails',
                'data' => ['member_id' => $urlParams['pass'][0]],
                'position' => 'member-tabs',
            ];
        }
        
        return $cells;
    }
}

Configuration Management

Plugins use StaticHelpers for versioned configuration management:

public function bootstrap(PluginApplicationInterface $app): void
{
    $currentConfigVersion = "25.01.11.a";
    $configVersion = StaticHelpers::getAppSetting("Plugin.configVersion", "0.0.0", null, true);
    
    if ($configVersion != $currentConfigVersion) {
        StaticHelpers::setAppSetting("Plugin.configVersion", $currentConfigVersion, null, true);
        
        // Update plugin configuration
        StaticHelpers::getAppSetting("Plugin.ButtonClass", "btn-primary", null, true);
        StaticHelpers::getAppSetting("Plugin.MaxItems", "100", null, true);
        
        // Configure member additional info fields
        StaticHelpers::getAppSetting(
            "Member.AdditionalInfo.PluginField",
            "select:Option1,Option2,Option3|user|public",
            null,
            true
        );
    }
}

Security Architecture

Authorization Integration

Plugins must integrate with KMP’s RBAC system through:

Policy Classes

class PluginResourcePolicy
{
    use \Authorization\Policy\BeforePolicyInterface;
    
    public function before($user, $resource, $action)
    {
        // Global authorization checks
        if (!$user || !$user->hasPermission('Plugin.Access')) {
            return false;
        }
    }
    
    public function canIndex($user, $resource)
    {
        return $user->hasPermission('Plugin.Index');
    }
    
    public function canEdit($user, $resource)
    {
        return $user->hasPermission('Plugin.Edit') && 
               $user->getBranchId() === $resource->getBranchId();
    }
}

Permission Requirements

// In plugin controller
public function initialize(): void
{
    parent::initialize();
    $this->Authorization->authorizeModel("index", "add");
}

// In plugin service
public function processAction($user, $data)
{
    if (!$user->hasPermission('Plugin.Process')) {
        throw new ForbiddenException('Insufficient permissions');
    }
    
    // Business logic
}

Data Protection Patterns

Plugin implementations must follow KMP security standards:

// Input validation
public function edit($id = null)
{
    $entity = $this->PluginTable->get($id, [
        'contain' => []
    ]);
    
    if ($this->request->is(['patch', 'post', 'put'])) {
        $entity = $this->PluginTable->patchEntity($entity, $this->request->getData());
        
        // Validate and sanitize
        if ($this->PluginTable->save($entity)) {
            $this->Flash->success(__('The record has been saved.'));
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('The record could not be saved. Please, try again.'));
    }
    
    $this->set(compact('entity'));
}

// XSS protection in templates
echo h($entity->user_input);
echo $this->Html->link(h($entity->title), ['action' => 'view', $entity->id]);

Plugin Categories

Core Business Logic Plugins

Activities Plugin (migrationOrder: 1)

graph TB
    A[Activities Plugin Admin] --> B[Activity Management]
    A --> C[Authorization Management]
    A --> D[Approval Queue]
    
    B --> B1[Create Activities]
    B --> B2[Edit Activities]
    B --> B3[Set Requirements]
    B --> B4[Configure Groups]
    
    C --> C1[Authorization Requests]
    C --> C2[Active Authorizations]
    C --> C3[Expired/Revoked]
    C --> C4[Request New Authorization]
    
    D --> D1[Pending Approvals]
    D --> D2[Approval History]
    D --> D3[Bulk Actions]
    D --> D4[AJAX Endpoints]
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0

Awards Plugin (migrationOrder: 2)

Officers Plugin (migrationOrder: 3)

Utility Plugins

Queue Plugin (migrationOrder: 10)

Bootstrap Plugin (migrationOrder: 12)

GitHubIssueSubmitter Plugin (migrationOrder: 11)

Development Best Practices

Standard Plugin Implementation

<?php
declare(strict_types=1);

namespace PluginName;

use Cake\Console\CommandCollection;
use Cake\Core\BasePlugin;
use Cake\Core\ContainerInterface;
use Cake\Core\PluginApplicationInterface;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
use App\KMP\KMPPluginInterface;
use Cake\Event\EventManager;
use App\Services\NavigationRegistry;
use App\Services\ViewCellRegistry;
use App\KMP\StaticHelpers;

class PluginNamePlugin extends BasePlugin implements KMPPluginInterface
{
    protected int $_migrationOrder = 1;

    public function __construct($config = [])
    {
        if (!isset($config['migrationOrder'])) {
            $config['migrationOrder'] = 1;
        }
        $this->_migrationOrder = $config['migrationOrder'];
    }

    public function getMigrationOrder(): int
    {
        return $this->_migrationOrder;
    }

    public function bootstrap(PluginApplicationInterface $app): void
    {
        // Event handler registration
        $handler = new PluginEventHandler();
        EventManager::instance()->on($handler);

        // Navigation registration
        NavigationRegistry::register(
            'PluginName',
            [],
            function ($user, $params) {
                return PluginNavigationProvider::getNavigationItems($user, $params);
            }
        );

        // View cell registration
        ViewCellRegistry::register(
            'PluginName',
            [],
            function ($urlParams, $user) {
                return PluginViewCellProvider::getViewCells($urlParams, $user);
            }
        );

        // Configuration management
        $this->setupConfiguration();
    }

    public function routes(RouteBuilder $routes): void
    {
        $routes->plugin(
            'PluginName',
            ['path' => '/plugin-path'],
            function (RouteBuilder $builder) {
                $builder->fallbacks();
            }
        );
        parent::routes($routes);
    }

    public function services(ContainerInterface $container): void
    {
        // Service registration and dependency injection
        $container->add(
            PluginServiceInterface::class,
            PluginService::class
        );
    }

    private function setupConfiguration(): void
    {
        $currentConfigVersion = "25.01.11.a";
        $configVersion = StaticHelpers::getAppSetting(
            "PluginName.configVersion", 
            "0.0.0", 
            null, 
            true
        );
        
        if ($configVersion != $currentConfigVersion) {
            StaticHelpers::setAppSetting(
                "PluginName.configVersion", 
                $currentConfigVersion, 
                null, 
                true
            );
            
            // Configure plugin settings
            StaticHelpers::getAppSetting(
                "Plugin.PluginName.Active", 
                "yes", 
                null, 
                true
            );
        }
    }
}

Performance Optimization

Lazy Loading Strategies

// Navigation provider with lazy loading
public static function getNavigationItems($user, $params): array
{
    // Cache expensive permission checks
    $cacheKey = "nav_plugin_{$user->id}";
    return Cache::remember($cacheKey, function () use ($user, $params) {
        // Expensive navigation generation
        return $this->generateNavigationItems($user, $params);
    }, '1 hour');
}

// View cell provider with conditional loading
public static function getViewCells($urlParams, $user): array
{
    // Only load cells for relevant pages
    if (!in_array($urlParams['controller'], ['Members', 'Branches'])) {
        return [];
    }
    
    // Load cells based on context
    return $this->loadContextualCells($urlParams, $user);
}

Database Optimization

// Optimized queries in plugin tables
class PluginTable extends BaseTable
{
    public function findActiveWithRelations(SelectQuery $query, array $options): SelectQuery
    {
        return $query
            ->contain(['RelatedEntity' => function ($q) {
                return $q->select(['id', 'name']); // Minimal fields
            }])
            ->where(['PluginEntity.active' => true])
            ->cache('plugin_active_list', 'plugin_cache'); // Cached results
    }
}

Testing Integration

Unit Tests

class PluginServiceTest extends TestCase
{
    protected $fixtures = [
        'app.Members',
        'plugin.PluginName.PluginEntities',
    ];

    public function testProcessAction()
    {
        $user = $this->getAuthenticatedUser();
        $service = new PluginService();
        
        $result = $service->processAction($user, ['test' => 'data']);
        
        $this->assertTrue($result->isSuccess());
        $this->assertEquals('Expected result', $result->getData());
    }
}

Integration Tests

class PluginControllerTest extends IntegrationTestCase
{
    public function testIndex()
    {
        $this->loginAsUser('admin');
        $this->get('/plugin');
        
        $this->assertResponseOk();
        $this->assertResponseContains('Plugin Dashboard');
    }

    public function testAuthorizationRequired()
    {
        $this->loginAsUser('member'); // User without plugin permissions
        $this->get('/plugin/admin');
        
        $this->assertResponseCode(403);
    }
}

Troubleshooting

Common Plugin Issues

Migration Order Problems

Symptom: Database foreign key constraint errors during plugin initialization

Causes:

Solutions:

// Review and adjust migration order in config/plugins.php
'DependentPlugin' => [
    'migrationOrder' => 5, // After dependencies
    'dependencies' => ['BasePlugin'], // Explicit dependencies
],

// Add dependency checking in plugin bootstrap
public function bootstrap(PluginApplicationInterface $app): void
{
    if (!TableRegistry::getTableLocator()->exists('BasePlugin.BaseEntities')) {
        throw new \RuntimeException('DependentPlugin requires BasePlugin to be loaded first');
    }
}

Service Registration Conflicts

Symptom: Service resolution errors or unexpected service behavior

Causes:

Solutions:

// Use plugin-namespaced service names
$container->add(
    'PluginName.' . ServiceInterface::class,
    ServiceImplementation::class
);

// Add explicit service dependencies
$container->add(ServiceInterface::class, ServiceImplementation::class)
    ->addArgument(DependencyInterface::class);

Configuration Conflicts

Symptom: Unexpected configuration values or plugin behavior

Causes:

Solutions:

// Use plugin-prefixed configuration keys
StaticHelpers::getAppSetting("PluginName.SpecificSetting", $default);

// Add configuration validation
private function validateConfiguration(): void
{
    $required = ['PluginName.RequiredSetting'];
    foreach ($required as $setting) {
        if (!StaticHelpers::hasAppSetting($setting)) {
            throw new \RuntimeException("Missing required configuration: {$setting}");
        }
    }
}

Performance Issues

Monitoring:

// Add timing to navigation providers
$start = microtime(true);
$items = $this->generateNavigationItems($user, $params);
$duration = microtime(true) - $start;

if ($duration > 0.1) { // 100ms threshold
    Log::warning("Slow navigation generation: {$duration}s");
}

Optimization:

// Implement proper caching
$cacheKey = "nav_plugin_{$user->id}_" . md5(serialize($params));
return Cache::remember($cacheKey, function () use ($user, $params) {
    return $this->generateNavigationItems($user, $params);
}, '15 minutes');

View Cell Performance

Monitoring:

// Profile view cell execution
$cells = [];
$timing = [];

foreach ($cellProviders as $provider) {
    $start = microtime(true);
    $providerCells = $provider($urlParams, $user);
    $timing[$provider] = microtime(true) - $start;
    $cells = array_merge($cells, $providerCells);
}

// Log slow providers
foreach ($timing as $provider => $duration) {
    if ($duration > 0.05) { // 50ms threshold
        Log::warning("Slow view cell provider: {$provider} ({$duration}s)");
    }
}

References

Individual Plugin Documentation

The following plugins are documented in detail in separate files to facilitate collaborative editing and maintenance:

Core Business Logic Plugins

Utility and Infrastructure Plugins

Each plugin document includes: