amtgard/aaro-extensions

Extended production features for the AARO ORM

Maintainers

Package info

github.com/amtgard/aaro-extensions

pkg:composer/amtgard/aaro-extensions

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.9.2 2026-06-08 18:25 UTC

README

Extended production features for the AARO ORM (amtgard/active-record-orm).

Requirements

  • PHP 8.3+
  • Composer
  • For integration tests: Docker (MariaDB via Compose)

Installation

composer require amtgard/aaro-extensions

Local development against the sibling ORM checkout:

composer install

The package composer.json includes a path repository to ../active-record-orm.

Usage

Wrap a core table (or repository) so inserts, updates, and deletes are recorded automatically in a companion audit table. Application code continues to use the core table normally.

Table level

use Amtgard\AaroExtensions\Audit\AuditConfiguration;
use Amtgard\AaroExtensions\Audit\AuditTableFactory;

$config = AuditConfiguration::builder()
    ->editedBySupplier(fn () => $currentUserId)
    ->build();

$table = AuditTableFactory::build($database, $policy, 'users', $config);

$table->clear();
$table->name = 'Alice';
$table->save();

Repository / EntityManager level

use Amtgard\AaroExtensions\Audit\AuditTableFactory;

$em = EntityManager::builder()
    ->database($database)
    ->dataAccessPolicy($policy)
    ->mapperSupplier(fn ($db, $policy, $name) => AuditTableFactory::mapperSupplier(
        $db,
        $policy,
        $name,
        $auditConfiguration,
    ))
    ->build();

Or use AuditRepositoryEntityTrait on a RepositoryEntity and implement auditConfiguration().

Configuration

AuditConfiguration::builder()
    ->auditTableName('users_history')   // default: {table}_audit
    ->editedBySupplier(fn () => 42)     // or EditedBySupplier instance
    ->auditInserts(false)               // default: true
    ->build();
Option Default Description
Audit table name {core_table}_audit Companion audit table
editedBySupplier null Resolves edited_by_id on each audit row
auditInserts true Write an audit row when a core row is inserted

Keeping audit tables in sync

Every auditable core table needs a matching audit table. By convention the audit table is named {core_table}_audit (configurable via AuditConfiguration).

When the core schema changes, the audit table must be updated as well:

Core change Audit table action
New table Create {table}_audit with metadata columns plus nullable mirrors of every core column except id
Column added Add the same column to the audit table (null => true)
Column type changed Update the mirrored column type on the audit table
Column dropped Rename the audit column to dropped_{n}_{column_name} to preserve historical values

The audit table always includes these metadata columns (Phinx adds an auto-increment id PK automatically):

  • audit_id — core row id (NOT NULL)
  • edit_at, edit_fields, edited_by_id, operation

See Audit reference below for full schema and write semantics.

Migration workflow

  1. Create — after defining a core table, create its audit table in Phinx with mirrored columns (all nullable except metadata).
  2. Patch — whenever a core migration adds, changes, or drops columns, apply the corresponding change to the audit table in the same release (use dropped_{n}_* for removed columns).

Migration generator and patcher commands:

# Generate Phinx migration(s) for audit table(s) from the live database schema
composer audit:phinx -- --env=.env --out-dir=db/migrations [--table=users]

# Emit a patch migration after core schema changes
composer audit:patch -- --env=.env --out-dir=db/migrations [--table=users]

# Merge project-specific table exclusions with the bundled defaults
composer audit:phinx -- --env=.env --out-dir=db/migrations --exclude-file=db/audit-exclusions.yaml

Or invoke the binary directly: vendor/bin/aaro-audit-migrate phinx ...

When --table is omitted, the CLI scans every table in the database and generates migrations for each one that is not excluded. Explicit --table always targets that table, even if it appears in an exclusions file.

Table exclusions

Infrastructure and framework tables (Phinx migration log, existing audit tables, etc.) should not receive audit migrations. The package ships a default list at resources/audit-table-exclusions.yaml:

tables:
  - phinxlog

suffixes:
  - _audit
Key Matches
tables Exact table names
suffixes Table names ending with the given suffix

Add project-specific exclusions in a local YAML file and pass it with --exclude-file. Entries are merged with the bundled defaults, not replaced:

# db/audit-exclusions.yaml
tables:
  - queue_jobs
  - sessions
suffixes:
  - _history
composer audit:patch -- --env=.env --out-dir=db/migrations --exclude-file=db/audit-exclusions.yaml

To exclude a table that ships in the defaults (unlikely), target it explicitly with --table instead of relying on the bulk scan.

Example audit migration (Phinx)

$this->table('users_audit')
    ->addColumn('audit_id', 'integer', ['null' => false])
    ->addColumn('edit_at', 'datetime', ['null' => false])
    ->addColumn('edit_fields', 'json', ['null' => true])
    ->addColumn('edited_by_id', 'integer', ['null' => true])
    ->addColumn('operation', 'enum', ['values' => ['insert', 'update', 'delete'], 'null' => false])
    ->addColumn('name', 'string', ['null' => true, 'limit' => 255])
    ->addColumn('email', 'string', ['null' => true, 'limit' => 255])
    ->create();

Testing

Unit tests

composer test:unit

Integration tests (Docker)

Integration tests use MariaDB on port 24307 via this package's docker-compose.dev.yml.

composer docker:up
composer migrate:test
composer test:integ
composer docker:down

Copy test-resources/.env.example to test-resources/.env if needed.

composer test    # unit + integration

Audit reference

Technical details for the row-level audit feature.

Overview

Transparent auditing for ORM Table and Repository usage. The wrapper records inserts, updates, and deletes in a companion audit table without audit-specific code in business logic.

Each audit row is a rolling snapshot of what happened (new/current values), ordered by time so you can answer “who changed this record to what?” and replay a record’s history from insert through delete.

Table pairing

Core table Audit table (default)
users users_audit

Schema

Phinx adds an auto-increment id as the audit row primary key. You declare the rest.

Metadata columns

Column Type Nullable Description
audit_id same as core PK NO Core row id this event belongs to
edit_at datetime NO When the event was recorded
edit_fields JSON YES Field names changed on update; [] on insert/delete
edited_by_id int (typical) YES Actor id from supplier
operation enum NO insert, update, or delete

Mirrored core columns

  • Every core column except id is replicated on the audit table.
  • All mirrored columns are explicitly nullable.
  • Values are snapshots after the operation (see write semantics).

When a core column is dropped, patch migrations preserve history as dropped_{n}_{column_name} on the audit table.

Write semantics

Event When operation edit_fields Mirrored columns
Insert After core insert insert [] All inserted values
Update After core update (only if something changed) update Changed field names New values for changed fields only
Delete Before core delete delete [] Full final row

Example for core row id = 42:

id | audit_id | operation | edit_at          | edit_fields      | string_value
---+----------+-----------+------------------+------------------+-------------
 1 |       42 | insert    | 2026-06-07 10:00 | []               | Alice
 2 |       42 | update    | 2026-06-07 11:00 | ["string_value"] | Alicia
 3 |       42 | delete    | 2026-06-07 12:00 | []               | Alicia

Package layout

src/Audit/
  AuditTable.php              Wrapper around core Table
  AuditTableFactory.php       Builds AuditTable + EntityMapper supplier
  AuditConfiguration.php      Table name, supplier, insert toggle
  AuditSnapshotCapture.php    Computes snapshot payload per operation
  AuditSnapshot.php           Value object for one audit event
  AuditOperation.php          insert | update | delete
  AuditColumns.php            Metadata column name constants
  AuditRepository.php         Repository base class
  AuditRepositoryEntityTrait.php
  EditedBySupplier.php        Optional supplier interface
  Migration/
    AuditMigrationService.php       phinx / patch orchestration
    AuditSchemaGenerator.php        core schema → audit schema
    AuditSchemaDiffer.php             diff core vs audit for patches
    AuditPhinxWriter.php              Phinx migration file output
    AuditTableExclusions.php          table/suffix exclusion rules
    AuditTableExclusionsLoader.php    YAML loader for exclusions
    Cli/AuditMigrateCommand.php       aaro-audit-migrate CLI
resources/
  audit-table-exclusions.yaml   default tables skipped by bulk scan
bin/
  aaro-audit-migrate            CLI entry point

Roadmap

  • Phase 1 — Runtime audit wrapper + unit tests
  • Phase 2 — Docker Compose + integration tests
  • Phase 3 — Phinx migration generator and audit-table patcher (audit:phinx, audit:patch)

License

MIT — see LICENSE.