Skip to content

Repository Persistence Families — Legacy Detail

Historical only — none of the concrete schema below ships in middag-io/framework today. It documents the moodle-local_middag legacy plugin's five architectural persistence families, preserved for whoever implements or maintains the product-specific application of this pattern (middag-io/core's Core Capabilities schema). What generalised and does ship in this OSS repo is the mechanism any of these families would be built on top of — a repository contract, a mapper, an immutable query builder, and an entity-type registry. Backs FW-013.

The five families (legacy, product-specific)

Family (legacy)SchemaAggregateDescription
Current stateEAV (middag_items + middag_itemmeta)domain/itemCurrent state with extensible metadata
Revision historyEAV (middag_item_revision + middag_item_revision_meta)domain/item_revisionStructural version history
Audit trailRelational (middag_audit_log, middag_audit_diff, middag_audit_snapshot)domain/auditOperation traceability
Job governanceRelational (middag_job, middag_job_attempt)domain/jobObservability, retry, correlation
Activity feedRelational (middag_activity_feed)domain/activity_feedActivity timeline for UI/notifications

EAV applied only to the two "current state"/"revision" families (extensible metadata per type); audit, job and activity feed were plain relational tables with a fixed schema. The rule for extensions building on top of the legacy plugin: use exclusively these five families — never create a bespoke table of your own inside the host plugin.

External plugins with their own tables were a different case: they could declare their own schema and still use the framework's query engine — their own EAV pair (from('yourplugin_items', 'yourplugin_itemmeta')), dedicated non-EAV tables (table_repository + from('yourplugin_table') with no meta table), or both at once. The query engine's default fallback remained the host plugin's own middag_items/middag_itemmeta.

Repository base classes per family (legacy)

Family (legacy)Extension base classFramework base classConsumed via
Current statebase\domain\item_repositoryframework\domain\item\item_repositoryDirect repository
Revision historybase\domain\item_revision_repositoryframework\domain\item_revision\item_revision_repositoryDirect repository
Audit trail— (framework only)framework\domain\audit\audit_*_repositoryfacade\audit_log_service
Job governance— (framework only)framework\domain\job\job_repository, job_attempt_repositoryfacade\job_service
Activity feed— (framework only)framework\domain\activity_feed\activity_feed_repositoryfacade\activity_feed_service

Extensions used only the first two families directly through a base class; the other three were consumed exclusively through a service/facade, never a direct repository call — a narrower surface for the families with more governance built in.

Cache decoration (a separate, older legacy decision)

A repository could be wrapped by a cache decorator combining request-level (in-memory) and a persistent cache layer, keyed as {aggregate}_{id} and invalidated on write. That mechanism comes from a different, earlier legacy decision (ADR-303) belonging to a Moodle-specific document, not this one — relevant here only because it depends on the same rule FW-013 states: caching decorates a repository, it is never the repository's own responsibility.

A citation error worth preserving

The legacy ADR-503 body cited "see REF-503-02 for an implementation guide" — that file does not exist. The real companion reference is ref-503-01-repository-patterns.md, confirmed by directory listing at the time of the original synthesis. Treat this as an error in the original decision record's own citation, to be corrected whenever the legacy vault itself is edited — not as a pointer to a document that was somehow lost. Everything REF-503-02 would supposedly have covered (a per-family repository implementation guide) is already in ref-503-01.

Anti-patterns (legacy)

Anti-patternWhy it's wrong
$DB->get_record() (or any host global data-access call) directly inside an extensionBypasses the repository boundary entirely.
A repository returning a bare stdClass instead of a proper mapped domain objectDefeats the point of the Data-Mapper path.
An extension creating its own table inside the host plugin's schema instead of using one of the sanctioned familiesA rule specific to the legacy plugin's own product schema, not a framework-level constraint.
A repository for one family reaching into another family's table directlyBreaks the per-aggregate isolation the family split exists to guarantee.
A service embedding raw SQL instead of going through a repositoryReopens the "which layer touches storage" ambiguity the repository boundary exists to close.
A repository with no accompanying interfaceBreaks the "repository and interface belong to the aggregate" rule.

What survives today

None of the five concrete families, nor an EAV query engine, ships in this OSS repo — they are middag-io/core's concern (Pillar 3 in architecture.md's pillar map). What generalised and does ship: Persistence/Repository/AbstractRepository.php, Persistence/Mapper/AbstractMapper.php, the immutable Persistence/Query/QueryBuilder.php, Persistence/Model.php, Persistence/Query/Page.php, and a host/product-agnostic entity-typing seed (Persistence/Attribute/EntityType.php, Persistence/Entity/{DefaultEntityType,EntityTypeRegistry}.php, Persistence/Loader/EntityTypeRegistrar.php) — a generic "an entity can declare its own type" concept, with no EAV storage assumption baked in.

MIDDAG © 2015-2026