Heratio Help Center article. Category: Technical.

Heratio AHG Plugin Architecture

Version: 2.8.2 Last Updated: February 2026


1. Architecture Overview

HeratioAHGSTACKPRESENTATIONLAYERahgThemeB5PluginTemplates(PHP)Assets(CSS/JSviaWebpack)(Bootstrap5)Symfony1.xweb/dist/MODULELAYERahgDisplayPluginahgSettingsPlugahgSecurityPlugSectorPlugins(informationobj,(settings(accessFilter,(museum,library,digitalobject)module)securityClear)gallery,dam)RULE:EachmodulehasexactlyONEownerpluginforactions/*.phpEXTENSIONSURFACEAhgHooksAhgCapabilitiesAhgPanelsAhgSectorProfile(events)(features)(UIslots)(labels/vocab)ahgCorePlugin/lib/SERVICELAYERatom-frameworkExtensionMgrMigrationHndlrPluginFetcherDataHandlerLaravelQueryBuilder(Illuminate\Database)DATALAYERHeratioCoreTablesAHGExtensionTables(PropelORM)(LaravelQueryBuilder)information_objectatom_pluginactoraudit_logdigital_objectprivacy_breachterm,taxonomycondition_assessmentrepositoryheritage_assetuser,acl_*loan_*,research_*MySQL8Database

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 Heratio routes continue through index.php unchanged. See HERATIO_MIGRATION.md for full details.


2. Plugin Types and Responsibilities

2.1 Plugin Categories

PLUGINCLASSIFICATIONCORE(Required,Locked)ahgThemeB5PluginUItheme,templates,assets(NOactions)ahgSecurityClearancePluginSecurityclassification(owns:accessFilter)INFRASTRUCTUREahgCorePluginExtensionsurface(hooks,capabilities,panels)ahgSettingsPluginSettingsmanagement(owns:settingsmodule)ahgDisplayPluginBrowse/display(owns:informationobject,etc.)SECTOR(Profile-only,nomoduleactions)ahgMuseumPluginMuseumprofiles,CCO,MuseumahgLibraryPluginLibraryprofiles,MARC,ISBNahgGalleryPluginGalleryprofiles,artworkmetadataahgDAMPluginDAMprofiles,mediamanagementCAPABILITY(Featureplugins,integrateviahooks)ahgIiifPluginIIIFmanifests,viewersahgPrivacyPluginPOPIA/GDPRcomplianceahgConditionPluginConditionassessmentahgLoanPluginLoanmanagementahgAIPluginNER,translation,summarization...(30capabilityplugins)

2.2 Module Ownership Rules

MODULEOWNERSHIPMAPModuleNameOwnerPluginActionFilesinformationobjectahgDisplayPlugin10actions(browse,cart,favorites,rename,etc.)digitalobjectahgDisplayPlugin5actionssettingsahgSettingsPlugin42actionsaccessFilterahgSecurityClearancePluginsecurityactionssecurityClearanceahgSecurityClearancePluginclearancemanagementuserahgCorePluginpasswordreset,etc.apiahgAPIPluginRESTendpointsconditionahgConditionPluginassessmentsloanahgLoanPluginloanworkflowsRULE:ForanymoduleX,onlyONEpluginmaycontainmodules/X/actions/*.phpfiles.Otherpluginsintegratevia:•AhgHooks::register('record.view.panels',...)•AhgPanels::register('informationobject',...)•Templatesinmodules/X/templates/

3. Extension Loading Flow

PLUGINLOADINGSEQUENCE1.PHPRequestconfig/ProjectConfiguration.class.phpsetup(){$corePlugins=['sfWebBrowserPlugin','sfThumbnailPlugin',...];$this->loadPluginsFromDatabase($corePlugins);◄Entrypoint}2.Bootstrapatom-frameworkrequire_once'atom-framework/bootstrap.php';•InitializeLaravelQueryBuilder•ConnecttoMySQLusingPDO•RegisterIlluminate\Database\Capsule\Manager3.Queryatom_plugintableSELECTnameFROMatom_pluginWHEREis_enabled=1ORDERBYload_orderASC;Result:['ahgThemeB5Plugin','ahgSecurityClearancePlugin',...]4.EnablepluginsviaSymfonyforeach($pluginsas$plugin){$this->enablePlugins([$plugin]);}Symfonyauto-discovers:•modules/<module>/actions/*.php•modules/<module>/templates/*.php•config/<plugin>Configuration.class.php5.PlugininitializationEachplugin'sConfigurationclassinitialize()iscalled:•RegisterhooksviaAhgHooks::register(...)•RegistercapabilitiesviaAhgCapabilities::register(...)•RegisterpanelsviaAhgPanels::register(...)•SetsectorprofileviaAhgSectorProfile::register(...)6.RequesthandledSymfonyroutesrequesttoappropriatemodule/actionActioncanqueryextensiondataviaLaravelQueryBuilderTemplatesrenderwithhook/panelintegrations

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' => 'museum procedures',
    '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#Plugininitializationdatabase/install.sql#Initialschemamigrations/#Versionmigrationslib/Services/#BusinesslogicRepositories/#DataaccessExtensions/#Extensionimplementationsmodules/moduleName/actions/#ONLYifthispluginOWNSthemoduleactionName.class.phptemplates/#Canexistwithoutowningmodule_partial.phpconfig/module.ymlweb/css/#Pluginstylesheetsjs/#Pluginscriptsimages/#Pluginimagesvendor/#Third-partyassetsextension.json#Pluginmetadata

6.1 The Problem

Sector plugins (Gallery, Museum, DAM) and shared menus (theme, reports) contain links to features provided by optional capability plugins (Collections Procedure, 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:

MODULEPLUGINMAPPINGModuleNamePluginNameCheckExpressionspectrumahgSpectrumPluginahg_is_plugin_enabled('ahgSpectrumPlugin')ccoahgProvenancePluginahg_is_plugin_enabled('ahgProvenancePlugin')grapahgHeritageAccountingPluginahg_is_plugin_enabled('ahgHeritageAccountingPlugin')oaisahgPreservationPluginahg_is_plugin_enabled('ahgPreservationPlugin')conditionahgConditionPluginahg_is_plugin_enabled('ahgConditionPlugin')loanahgLoanPluginahg_is_plugin_enabled('ahgLoanPlugin')researchahgResearchPluginahg_is_plugin_enabled('ahgResearchPlugin')3dmodelahg3DModelPluginahg_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

  1. 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().
  2. Use the correct plugin name - Module names differ from plugin names (see mapping above).
  3. Include the helper - Every template using the function must require_once the AhgLaravelHelper.
  4. 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 Heratio AHG Framework - v2.8.2