AtoM Heratio — Technical Manual

For: Developers, DevOps Engineers, System Integrators Product: AtoM Heratio Framework v2.8.2 Date: 16 March 2026 Author: The Archive and Heritage Group (Pty) Ltd


About This Manual

This manual covers architecture, plugin development, CLI commands, database schema, API integration, and deployment. For end-user workflows, see the User Manual. For administration, see the Admin Manual.


1. Architecture

1.1 Two-Layer Design

┌─────────────────────────────────────────────────────────────┐
│                    AtoM 2.10 BASE (Symfony 1.x)              │
│  Routing, Templates, ACL, Propel ORM, sfPluginAdmin          │
├─────────────────────────────────────────────────────────────┤
│  LAYER 1: atom-framework v2.8.2 (REQUIRED)                   │
│  ├── Laravel Query Builder (Illuminate\Database\Capsule)     │
│  ├── Extension Manager (CLI + Service)                       │
│  ├── 90+ Services (Services/, Write/, Search/, Pagination/)  │
│  ├── RouteLoader + RouteCollector                            │
│  ├── Base Repository/Service classes                         │
│  └── Helper classes (Core utilities)                         │
├─────────────────────────────────────────────────────────────┤
│  LAYER 2: atom-ahg-plugins (80 plugins)                      │
│  ├── 15 locked core plugins (required)                       │
│  ├── 6 stable GLAM sector plugins                            │
│  └── 59 optional feature plugins                             │
└─────────────────────────────────────────────────────────────┘

Philosophy: Modern data layer, legacy presentation layer. Laravel Query Builder handles all database operations while preserving Symfony's routing, templates, and ACL.

1.2 Plugin Loading

ProjectConfiguration::setup()
  └── loadPluginsFromDatabase($corePlugins)
        └── SELECT name FROM atom_plugin WHERE is_enabled = 1
              └── enablePlugins($plugins)

Source of truth: atom_plugin table. Legacy: setting_i18n id=1 maintained for sfPluginAdmin UI only.

1.3 Database Access Patterns

Context Use This NOT This
All plugins & services Illuminate\Database\Capsule\Manager as DB Raw PDO
Plugin Manager only Propel::getConnection() + PDO Laravel QB
atom_plugin table PDO only Laravel QB

Why Plugin Manager uses PDO: Symfony autoloader conflicts prevent Laravel from working when managing the atom_plugin and atom_plugin_audit tables.

1.4 Namespace Convention

namespace AtomExtensions\Repositories;
namespace AtomExtensions\Services;
use Illuminate\Database\Capsule\Manager as DB;

2. Plugin Development

2.1 Directory Structure

atom-ahg-plugins/ahgMyPlugin/
├── extension.json                    # Plugin manifest
├── config/
│   ├── ahgMyPluginConfiguration.class.php
│   ├── routing.yml                   # Routes (optional)
│   └── settings.yml                  # Enabled modules
├── database/
│   └── install.sql                   # Schema + seed data
├── lib/
│   ├── Repositories/                 # Data access (Laravel QB)
│   ├── Services/                     # Business logic
│   ├── Helpers/                      # Utilities
│   └── task/                         # CLI tasks
├── modules/
│   └── myModule/
│       ├── actions/
│       │   └── actions.class.php     # Controller
│       │   └── myAction.class.php    # Single action
│       └── templates/
│           └── indexSuccess.php       # View
├── css/
├── js/
└── web/                              # Public assets

2.2 extension.json

{
  "name": "My Plugin",
  "machine_name": "ahgMyPlugin",
  "version": "1.0.0",
  "description": "What this plugin does",
  "author": "The Archive and Heritage Group",
  "license": "GPL-3.0",
  "requires": {
    "atom_framework": ">=1.0.0",
    "atom": ">=2.8",
    "php": ">=8.1"
  },
  "dependencies": ["ahgCorePlugin"],
  "tables": ["my_table"],
  "shared_tables": [],
  "theme_support": ["ahgThemeB5Plugin"],
  "install_task": "my:install"
}

2.3 Repository Pattern

<?php
namespace AtomExtensions\Repositories;

use Illuminate\Database\Capsule\Manager as DB;

class MyRepository
{
    public function findAll(): \Illuminate\Support\Collection
    {
        return DB::table('my_table')
            ->where('is_active', 1)
            ->orderBy('created_at', 'desc')
            ->get();
    }

    public function findById(int $id): ?object
    {
        return DB::table('my_table')->where('id', $id)->first();
    }

    public function create(array $data): int
    {
        return DB::table('my_table')->insertGetId(array_merge($data, [
            'created_at' => date('Y-m-d H:i:s'),
        ]));
    }
}

2.4 Template Rules

// CORRECT — always use this
<?php echo url_for(['module' => 'myModule', 'action' => 'show', 'slug' => $item->slug]) ?>
<?php echo $resource->title ?? $resource->slug ?>

// WRONG — never use
<?php echo url_for([$resource, 'module' => 'myModule']) ?>
<?php echo render_title($resource) ?>
<?php echo $resource->__toString() ?>

2.5 CSP Nonce (Scripts & Styles)

All inline <script> and <style> tags must include the CSP nonce:

<script <?php $n = sfConfig::get('csp_nonce', ''); echo $n ? preg_replace('/^nonce=/', 'nonce="', $n).'"' : ''; ?>>
// Your JavaScript here
</script>

2.6 Namespaced Global Classes

In namespaced PHP files, prefix global classes with \:

\sfConfig::get('app_setting');
\QubitActor::getById($id);
\sfContext::getInstance();

2.7 install.sql Rules

-- ALLOWED: tables, indexes, seed data
CREATE TABLE IF NOT EXISTS my_table (...);
INSERT INTO term (taxonomy_id, ...) VALUES (...);

-- FORBIDDEN: never include this
INSERT INTO atom_plugin (...) VALUES (...);

Note: ADD COLUMN IF NOT EXISTS does not work in MySQL 8. Use CREATE TABLE IF NOT EXISTS or check information_schema.COLUMNS before altering.

2.8 Enabling Plugins

php bin/atom extension:enable ahgMyPlugin
php symfony cc

3. CLI Commands

3.1 Framework Commands (php bin/atom)

Command Description
extension:discover Scan directories, register new plugins
extension:list List all plugins with status
extension:enable <name> Enable a plugin
extension:disable <name> Disable a plugin
extension:info <name> Show plugin details
extension:install <name> Install from GitHub
extension:update --all Update all plugins
extension:cleanup Delete data past grace period
framework:install Initial setup
framework:update Pull framework updates
framework:version Show version

3.2 Plugin Commands (php symfony)

Namespace Plugin Commands
ai:* ahgAIPlugin install, ner-extract, ner-sync, translate, summarize, spellcheck, suggest-description, process-pending, sync-entity-cache
backup:* ahgBackupPlugin run-scheduled
preservation:* ahgPreservationPlugin convert, fixity, identify, migration, package, pronom-sync, replicate, scheduler, verify-backup, virus-scan
doi:* ahgDoiPlugin deactivate, mint, process-queue, sync, verify
cdpa:* ahgCDPAPlugin license-check, report, requests, status
naz:* ahgNAZPlugin closure-check, permit-expiry, report, transfer-due
dedupe:* ahgDedupePlugin merge, report, scan
forms:* ahgFormsPlugin export, import, list
heritage:* ahgHeritagePlugin build-graph, install, region
display:* ahgDisplayPlugin auto-detect, reindex
privacy:* ahgPrivacyPlugin jurisdiction, scan-pii
embargo:* ahgExtendedRightsPlugin process, report
museum:* ahgMuseumPlugin aat-sync, exhibition, getty-link, migrate
ingest:* ahgIngestPlugin commit
ipsas:* ahgIPSASPlugin report
nmmz:* ahgNMMZPlugin report
metadata:* ahgMetadataExportPlugin export
library:* ahgLibraryPlugin process-covers
portable:* ahgPortableExportPlugin export
api:* ahgAPIPlugin webhook-process-retries
queue:* Framework work, status, retry, failed, cleanup

3.3 Standard AtoM Commands

php symfony cc                          # Clear cache
php symfony search:populate             # Rebuild ES index
php symfony search:status               # Index status
php bin/atom import:csv <file>          # CSV import
php bin/atom digitalobject:extract-text # Extract text/OCR
php bin/atom tools:add-superuser        # Create admin user
php bin/atom tools:reset-password       # Reset password

4. Database Schema

4.1 Core AtoM Tables (DO NOT MODIFY)

object, information_object, information_object_i18n, actor, actor_i18n, term, term_i18n, taxonomy, setting, setting_i18n, user, repository, digital_object, slug, property, property_i18n, relation, event, note, note_i18n

4.2 Framework Tables

Table Purpose
atom_plugin Plugin registry (source of truth for loading)
atom_plugin_audit Plugin enable/disable/install/uninstall log
ahg_settings Key-value settings store (200+ settings)
ahg_error_log Application error log
ahg_dropdown Dropdown value lists for custom fields
ahg_queue_job Background job queue
ahg_queue_batch Job batches
ahg_queue_failed Failed jobs
ahg_queue_log Queue processing log
ahg_queue_rate_limit Rate limiting for queue

4.3 Entity Inheritance (FK Constraints)

object (base)
  ├── actor
  │     ├── user      (FK: user.id → actor.id)
  │     ├── donor     (FK: donor.id → actor.id)
  │     ├── repository (FK: repository.id → actor.id)
  │     └── rights_holder (FK: rights_holder.id → actor.id)
  └── information_object
        └── digital_object (FK: digital_object.object_id → information_object.id)

Create pattern: INSERT object → INSERT actor → INSERT user/donor/etc. Delete cascade: DELETE object cascades to actor → cascades to user/donor/etc.

4.4 Reporting Views

Pre-built SQL views for BI tools (Power BI, Tableau, Metabase):

View Description
v_report_descriptions Flattened archival descriptions
v_report_authorities Flattened authority records
v_report_accessions Flattened accessions

5. API Integration

5.1 REST API

Base URL: /api/v1/

Authentication: Bearer token via Authorization: Bearer <token>

Endpoint Method Description
/api/v1/records GET List records (paginated)
/api/v1/records/:slug GET Get single record
/api/v1/authorities GET List authorities
/api/v1/repositories GET List repositories
/api/v1/search GET Search records

5.2 GraphQL

Endpoint: /graphql

query {
  informationObjects(limit: 10, offset: 0) {
    id
    title
    identifier
    levelOfDescription
    repository { name }
    digitalObjects { mimeType, name }
  }
}

5.3 IIIF

Endpoint Description
/iiif/manifest/:id IIIF Presentation manifest
/iiif/2/:identifier/info.json IIIF Image API info
/iiif/2/:identifier/:region/:size/:rotation/:quality.:format IIIF Image tile

Cantaloupe image server on port 8182, proxied via nginx.


6. Deployment

6.1 Server Requirements

Component Version
PHP 8.3+
MySQL 8.0+
Elasticsearch 7.10
Nginx 1.18+
Node.js 18+ (for webpack)
Cantaloupe 5.0.6 (IIIF)

6.2 Installation

cd /usr/share/nginx/archive
git clone https://github.com/ArchiveHeritageGroup/atom-framework.git
git clone https://github.com/ArchiveHeritageGroup/atom-ahg-plugins.git
cd atom-framework && composer install && bash bin/install
sudo systemctl restart php8.3-fpm
php bin/atom extension:discover

6.3 Webpack Build

cd /usr/share/nginx/archive
npx webpack --mode production

Generates hashed bundles in /dist/js/ and /dist/css/. The theme dynamically discovers bundles via PHP glob().

6.4 Version Release

cd /usr/share/nginx/archive/atom-framework   # or atom-ahg-plugins
./bin/release patch "Description of changes"
./bin/release minor "New feature description"
./bin/release major "Breaking change description"

6.5 Cron Jobs

# Backup scheduler (hourly check)
0 * * * * cd /usr/share/nginx/archive && php symfony backup:run-scheduled >> /var/log/atom/backup-cron.log 2>&1

# Queue worker (if not using systemd)
* * * * * cd /usr/share/nginx/archive && php bin/atom queue:work --max-jobs=10 >> /var/log/atom/queue.log 2>&1

# Extension cleanup (daily)
0 2 * * * cd /usr/share/nginx/archive && php bin/atom extension:cleanup >> /var/log/atom/cleanup.log 2>&1

6.6 Systemd Services

# Queue worker (per-queue instances)
sudo systemctl enable atom-queue-worker@default
sudo systemctl start atom-queue-worker@default

# Cantaloupe IIIF
sudo systemctl restart cantaloupe

7. Troubleshooting

Issue Solution
Plugins not loading Check atom_plugin table: SELECT name, is_enabled FROM atom_plugin
Theme not applied Clear cache: rm -rf cache/* && php symfony cc
Plugin enabled but not working Check symlink: ls -la plugins/ahgMyPlugin
in_array error Add ?: [] fallback: unserialize($value) ?: []
Namespaced class not found Use \ prefix: \sfConfig, \QubitActor
name="action" form bug Rename to name="form_action" — Symfony reserves action
IIIF viewer broken Restart Cantaloupe: sudo systemctl restart cantaloupe
Elasticsearch empty Rebuild index: php symfony search:populate
Cache stale after changes rm -rf cache/* && php symfony cc && sudo systemctl restart php8.3-fpm

8. Security

8.1 CSP (Content Security Policy)

Configured in config/app.yml under all.csp. Nonce generated per request via QubitCSPFilter.

Rules:

  • All <script> and <style> tags must have CSP nonce
  • External CDN domains must be whitelisted in app.yml
  • Never use 'unsafe-inline' — use nonces

8.2 CSRF Protection

All POST forms include _csrf_token hidden field. Validated by Symfony's CSRF filter.

8.3 Bell-LaPadula MAC

Security classification enforced at query level — users cannot read records above their clearance.


AtoM Heratio Framework v2.8.2 — The Archive and Heritage Group (Pty) Ltd