AtoM AHG Plugin Architecture¶
Version: 2.8.2 Last Updated: February 2026
1. Architecture Overview¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ AtoM AHG STACK │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ PRESENTATION LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ ahgThemeB5Plugin│ │ Templates (PHP) │ │ Assets (CSS/JS via Webpack) │ │ │
│ │ │ (Bootstrap 5) │ │ Symfony 1.x │ │ web/dist/ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ MODULE LAYER │ │
│ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ahgDisplayPlugin│ │ahgSettingsPlug│ │ahgSecurityPlug│ │ Sector Plugins │ │ │
│ │ │(informationobj,│ │(settings │ │(accessFilter, │ │(museum,library,│ │ │
│ │ │ digitalobject) │ │ module) │ │ securityClear)│ │ gallery, dam) │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ │ │ │
│ │ RULE: Each module has exactly ONE owner plugin for actions/*.php │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ EXTENSION SURFACE │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ AhgHooks │ │AhgCapabilities│ │ AhgPanels │ │ AhgSectorProfile │ │ │
│ │ │ (events) │ │ (features) │ │ (UI slots) │ │ (labels/vocab) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
│ │ ahgCorePlugin/lib/ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ SERVICE LAYER │ │
│ │ ┌────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ atom-framework │ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │ │
│ │ │ │ExtensionMgr │ │MigrationHndlr│ │PluginFetcher │ │DataHandler │ │ │ │
│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Laravel Query Builder (Illuminate\Database) │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │ │
│ │ │ AtoM Core Tables │ │ AHG Extension Tables │ │ │
│ │ │ (Propel ORM) │ │ (Laravel Query Builder) │ │ │
│ │ │ │ │ │ │ │
│ │ │ information_object │ │ atom_plugin │ │ │
│ │ │ actor │ │ audit_log │ │ │
│ │ │ digital_object │ │ privacy_breach │ │ │
│ │ │ term, taxonomy │ │ condition_assessment │ │ │
│ │ │ repository │ │ heritage_asset │ │ │
│ │ │ user, acl_* │ │ loan_*, research_* │ │ │
│ │ └─────────────────────────┘ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ MySQL 8 Database │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
Heratio Dual-Mode¶
The Heratio migration adds an alternative rendering path. When .heratio_enabled exists, AHG plugin routes are served by heratio.php (HTTP Kernel + Blade rendering) instead of Symfony. Base AtoM routes continue through index.php unchanged. See HERATIO_MIGRATION.md for full details.
2. Plugin Types and Responsibilities¶
2.1 Plugin Categories¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PLUGIN CLASSIFICATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ CORE (Required, Locked) │
│ ├── ahgThemeB5Plugin ────────── UI theme, templates, assets (NO actions) │
│ └── ahgSecurityClearancePlugin ─ Security classification (owns: accessFilter) │
│ │
│ INFRASTRUCTURE │
│ ├── ahgCorePlugin ──────────── Extension surface (hooks, capabilities, panels) │
│ ├── ahgSettingsPlugin ──────── Settings management (owns: settings module) │
│ └── ahgDisplayPlugin ───────── Browse/display (owns: informationobject, etc.) │
│ │
│ SECTOR (Profile-only, no module actions) │
│ ├── ahgMuseumPlugin ─────────── Museum profiles, CCO, Spectrum │
│ ├── ahgLibraryPlugin ────────── Library profiles, MARC, ISBN │
│ ├── ahgGalleryPlugin ────────── Gallery profiles, artwork metadata │
│ └── ahgDAMPlugin ────────────── DAM profiles, media management │
│ │
│ CAPABILITY (Feature plugins, integrate via hooks) │
│ ├── ahgIiifPlugin ──────────── IIIF manifests, viewers │
│ ├── ahgPrivacyPlugin ────────── POPIA/GDPR compliance │
│ ├── ahgConditionPlugin ──────── Condition assessment │
│ ├── ahgLoanPlugin ──────────── Loan management │
│ ├── ahgAIPlugin ────────────── NER, translation, summarization │
│ └── ... (30+ capability plugins) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
2.2 Module Ownership Rules¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MODULE OWNERSHIP MAP │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Module Name │ Owner Plugin │ Action Files │
│ ─────────────────────────┼─────────────────────────┼────────────────────────── │
│ informationobject │ ahgDisplayPlugin │ 10 actions (browse, cart, │
│ │ │ favorites, rename, etc.) │
│ digitalobject │ ahgDisplayPlugin │ 5 actions │
│ settings │ ahgSettingsPlugin │ 42 actions │
│ accessFilter │ ahgSecurityClearancePlugin │ security actions │
│ securityClearance │ ahgSecurityClearancePlugin │ clearance management │
│ user │ ahgCorePlugin │ password reset, etc. │
│ api │ ahgAPIPlugin │ REST endpoints │
│ condition │ ahgConditionPlugin │ assessments │
│ loan │ ahgLoanPlugin │ loan workflows │
│ ─────────────────────────┴─────────────────────────┴────────────────────────── │
│ │
│ RULE: For any module X, only ONE plugin may contain │
│ modules/X/actions/*.php files. │
│ │
│ Other plugins integrate via: │
│ • AhgHooks::register('record.view.panels', ...) │
│ • AhgPanels::register('informationobject', ...) │
│ • Templates in modules/X/templates/ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
3. Extension Loading Flow¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PLUGIN LOADING SEQUENCE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. PHP Request │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ config/ProjectConfiguration.class.php │ │
│ │ │ │
│ │ setup() { │ │
│ │ $corePlugins = ['sfWebBrowserPlugin', 'sfThumbnailPlugin', ...]; │ │
│ │ $this->loadPluginsFromDatabase($corePlugins); ◄─── Entry point │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 2. Bootstrap atom-framework │ │
│ │ require_once 'atom-framework/bootstrap.php'; │ │
│ │ │ │
│ │ • Initialize Laravel Query Builder │ │
│ │ • Connect to MySQL using PDO │ │
│ │ • Register Illuminate\Database\Capsule\Manager │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 3. Query atom_plugin table │ │
│ │ │ │
│ │ SELECT name FROM atom_plugin │ │
│ │ WHERE is_enabled = 1 │ │
│ │ ORDER BY load_order ASC; │ │
│ │ │ │
│ │ Result: ['ahgThemeB5Plugin', 'ahgSecurityClearancePlugin', ...] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 4. Enable plugins via Symfony │ │
│ │ │ │
│ │ foreach ($plugins as $plugin) { │ │
│ │ $this->enablePlugins([$plugin]); │ │
│ │ } │ │
│ │ │ │
│ │ Symfony auto-discovers: │ │
│ │ • modules/<module>/actions/*.php │ │
│ │ • modules/<module>/templates/*.php │ │
│ │ • config/<plugin>Configuration.class.php │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 5. Plugin initialization │ │
│ │ │ │
│ │ Each plugin's Configuration class initialize() is called: │ │
│ │ │ │
│ │ • Register hooks via AhgHooks::register(...) │ │
│ │ • Register capabilities via AhgCapabilities::register(...) │ │
│ │ • Register panels via AhgPanels::register(...) │ │
│ │ • Set sector profile via AhgSectorProfile::register(...) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 6. Request handled │ │
│ │ │ │
│ │ Symfony routes request to appropriate module/action │ │
│ │ Action can query extension data via Laravel Query Builder │ │
│ │ Templates render with hook/panel integrations │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
4. Extension Surface API¶
4.1 AhgHooks - Event System¶
// Register a hook (in plugin Configuration)
AhgHooks::register('record.view.sidebar', function($record) {
return ['html' => '<div>My Panel</div>'];
}, priority: 10);
// Trigger hooks (in action/template)
$panels = AhgHooks::trigger('record.view.sidebar', $record);
// Filter hooks (modify value through chain)
$title = AhgHooks::filter('record.title', $record->title, $record);
4.2 AhgCapabilities - Feature Detection¶
// Register capability (in plugin Configuration)
AhgCapabilities::register('iiif', 'ahgIiifPlugin', [
'version' => '3.0',
'auth' => true
]);
// Check capability (anywhere)
if (AhgCapabilities::has('iiif')) {
// Show IIIF viewer
}
4.3 AhgPanels - UI Slots¶
// Register panel (in plugin Configuration)
AhgPanels::register('informationobject', 'condition-status', [
'title' => 'Condition',
'position' => 'sidebar',
'component' => 'condition/statusComponent'
]);
// Get panels for position (in template)
$panels = AhgPanels::forPosition('informationobject', 'sidebar', $record);
4.4 AhgSectorProfile - Sector Configuration¶
// Register sector (in Museum plugin)
AhgSectorProfile::register('museum', [
'name' => 'Museum',
'standard' => 'Spectrum 5.0',
'labels' => [
'extent' => 'Dimensions',
'scopeAndContent' => 'Object Description'
],
'vocabularies' => [
'materialType' => ['Oil paint', 'Bronze', 'Marble']
]
]);
// Get label for current sector
$label = AhgSectorProfile::getLabel('extent', 'Extent');
5. Directory Structure Standards¶
plugin-name/
├── config/
│ └── pluginNameConfiguration.class.php # Plugin initialization
├── database/
│ ├── install.sql # Initial schema
│ └── migrations/ # Version migrations
├── lib/
│ ├── Services/ # Business logic
│ ├── Repositories/ # Data access
│ └── Extensions/ # Extension implementations
├── modules/
│ └── moduleName/
│ ├── actions/ # ONLY if this plugin OWNS the module
│ │ └── actionName.class.php
│ ├── templates/ # Can exist without owning module
│ │ └── _partial.php
│ └── config/
│ └── module.yml
├── web/
│ ├── css/ # Plugin stylesheets
│ ├── js/ # Plugin scripts
│ ├── images/ # Plugin images
│ └── vendor/ # Third-party assets
└── extension.json # Plugin metadata
6. Cross-Plugin Link Guards¶
6.1 The Problem¶
Sector plugins (Gallery, Museum, DAM) and shared menus (theme, reports) contain links to features provided by optional capability plugins (Spectrum, Provenance, GRAP, etc.). When those plugins are disabled, the links become broken (404 errors or blank pages).
6.2 The ahg_is_plugin_enabled() Helper¶
A cached helper function checks whether a plugin is enabled at runtime:
// Located in: ahgUiOverridesPlugin/lib/helper/AhgLaravelHelper.php
function ahg_is_plugin_enabled(string $pluginName): bool
{
static $cache = [];
if (isset($cache[$pluginName])) {
return $cache[$pluginName];
}
try {
$cache[$pluginName] = DB::table('atom_plugin')
->where('name', $pluginName)
->where('is_enabled', 1)
->exists();
} catch (\Exception $e) {
$cache[$pluginName] = false;
}
return $cache[$pluginName];
}
Performance: Uses a static cache. Each plugin name is queried only once per request.
6.3 Template Usage Pattern¶
Every template that uses ahg_is_plugin_enabled() must include the helper:
<?php require_once sfConfig::get('sf_plugins_dir')
. '/ahgUiOverridesPlugin/lib/helper/AhgLaravelHelper.php'; ?>
Then wrap optional plugin links:
<?php if (ahg_is_plugin_enabled('ahgSpectrumPlugin')): ?>
<a href="<?php echo url_for(['module' => 'spectrum', 'action' => 'index']); ?>">
SPECTRUM Procedures
</a>
<?php endif; ?>
6.4 Plugin Name Reference¶
Templates use module names for routing but plugin names for enable checks:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MODULE → PLUGIN MAPPING │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Module Name │ Plugin Name │ Check Expression │
│ ─────────────────┼───────────────────────────┼──────────────────────────────── │
│ spectrum │ ahgSpectrumPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgSpectrumPlugin') │
│ cco │ ahgProvenancePlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgProvenancePlugin') │
│ grap │ ahgHeritageAccountingPlugin│ ahg_is_plugin_enabled( │
│ │ │ 'ahgHeritageAccountingPlugin') │
│ oais │ ahgPreservationPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgPreservationPlugin') │
│ condition │ ahgConditionPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgConditionPlugin') │
│ loan │ ahgLoanPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgLoanPlugin') │
│ research │ ahgResearchPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahgResearchPlugin') │
│ 3dmodel │ ahg3DModelPlugin │ ahg_is_plugin_enabled( │
│ │ │ 'ahg3DModelPlugin') │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
6.5 Guarded Templates¶
| Plugin | Templates with Guards |
|---|---|
| ahgGalleryPlugin | gallery/templates/indexSuccess.php (sidebar + admin dropdown) |
| ahgMuseumPlugin | museum/templates/indexSuccess.php, museum/templates/conditionReportSuccess.php, cco/templates/conditionReportSuccess.php, cco/templates/_actions.php |
| ahgDAMPlugin | dam/templates/dashboardSuccess.php |
| ahgThemeB5Plugin | templates/_glamDamMenu.php |
| ahgReportsPlugin | reports/templates/_reportsMenu.php |
6.6 Rules for Developers¶
- Always guard cross-plugin links - If a template links to a module owned by another optional plugin, wrap the link with
ahg_is_plugin_enabled(). - Use the correct plugin name - Module names differ from plugin names (see mapping above).
- Include the helper - Every template using the function must
require_oncethe AhgLaravelHelper. - Never guard links to core/infrastructure plugins - ahgDisplayPlugin, ahgThemeB5Plugin, ahgSettingsPlugin, and ahgSecurityClearancePlugin are always enabled.
7. CI Architecture Checks¶
The bin/check-architecture.sh script enforces:
| Check | Rule | Severity |
|---|---|---|
| Cross-Plugin Guards | Links to optional plugins use ahg_is_plugin_enabled() |
WARNING |
| Check | Rule | Severity |
|---|---|---|
| Theme Actions | ahgThemeB5Plugin has no actions/*.php | ERROR |
| Module Ownership | Each module has exactly one action owner | ERROR |
| Absolute Paths | No /usr/share/nginx in PHP files | ERROR |
| Asset Location | Assets only under web/ | ERROR |
| DB Location | SQL only under database/ | ERROR |
| Zero-byte Files | No empty files (except .gitkeep) | ERROR |
| Sector Plugins | No core module actions in sector plugins | ERROR |
Part of the AtoM AHG Framework - v2.8.2