← Persistence

PERSISTENCE FEATURE

Domain-Level Models

Resource models exist for persistence. Domain models exist for business behavior. Explicit mappers and repositories bridge them instead of collapsing them into one class.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Semitexa deliberately separates persistence resources from domain-level business models instead of pretending one class should do both jobs.

How it works

A ResourceModel maps the table, and an explicit mapper converts between persistence and domain. Repositories then return domain objects on the normal read path.

Why it matters

This keeps business code free from ORM metadata and keeps persistence code free from fake business semantics. The boundary stays reviewable and intentional.

Key concepts

ResourceModel
Persistence-side model that maps one table row shape and relation metadata.
Mapper
Explicit converter between ResourceModel and DomainModel.
Resource model
ORM-facing class responsible for table mapping, columns, indexes, and storage conversion.
Domain model
Business object carrying behavior, invariants, and ubiquitous language without persistence metadata.

Two Model Layers

Resource model and domain model do different jobs

Semitexa does not force one giant class to be table mapping, behavior container, and business language at the same time. The ORM resource maps persistence. The domain model carries the business rules.

Why this matters

  • When one class is both ORM mapping and business model, storage concerns leak into business code immediately.
  • That usually pushes handlers and services to pass persistence resources around as if they were the real business objects.
  • Semitexa keeps the separation explicit: repositories can expose domain models while still using ResourceModel + mapper for persistence.

Persistence layer

ResourceModel + mapper

Maps columns, table name, and explicit conversion rules into the domain layer.

#[FromTable] #[Column] #[AsMapper] explicit mapping

Business layer

Domain model

Holds business semantics like revoke(), rotateSecretHash(), recordUsage(), hasScope().

behavior invariants business language no ORM attrs
Concern Resource model Domain model
Primary responsibility Describe how one table row is stored and rehydrated. Describe what the business object means and how it behaves.
Allowed dependencies ORM attributes, SQL-facing concerns, mapping metadata. Only domain language and behavior, no storage metadata.
Repository default reads Built as ResourceModel and then mapped explicitly inside the repository implementation. Returned by the repository contract through the canonical ORM path.
Mutation path Useful for low-level persistence mutation or infrastructure-only workflows. Preferred for business flows through contract repositories like MachineCredentialRepositoryInterface.
ResourceModel mapper #[AsMapper] #[SatisfiesRepositoryContract] DomainRepository

Verified against Semitexa Ultimate 2026.09.19.1020

Domain-Level Models

Semitexa keeps persistence resources and business domain models as separate classes with explicit mappers between them.

How it works

A ResourceModel describes how one table row is stored and rehydrated. A domain model holds business semantics — methods like revoke(), rotateSecretHash(), recordUsage(), and hasScope(). A mapper annotated with #[AsMapper] converts between the two, and a repository implementation annotated with #[SatisfiesRepositoryContract] returns domain models from the contract boundary.

Why this matters

When one class is both an ORM mapping and a business model, storage concerns leak into business code immediately. Semitexa keeps the separation explicit so that handlers and services work with objects that carry business meaning, not persistence metadata.

© Linus Torvalds: "Talk is cheap. Show me the code."

Domain Model Business model
<?phpdeclare(strict_types=1);namespace App\Domain\Auth;final class MachineCredential{    public function __construct(        public readonly string $id,        public readonly string $secretHash,        public readonly array $scopes,    ) {}}

Repository Boundary

What should cross the boundary

Repository contracts at the application boundary should usually speak in domain models, not ORM resources.

ResourceModel and mapper classes are still first-class, but mostly for mapping, hydration, and persistence-specific operations.

When a workflow truly needs raw persistence shape, keep that path explicit and infrastructure-scoped.

The canonical repository read path should remain ResourceModel -> mapper -> domain model.

How it works

A ResourceModel maps the table, and an explicit mapper converts between persistence and domain. Repositories then return domain objects on the normal read path.

Why it matters

This keeps business code free from ORM metadata and keeps persistence code free from fake business semantics. The boundary stays reviewable and intentional.

Key concepts

ResourceModel
Persistence-side model that maps one table row shape and relation metadata.
Mapper
Explicit converter between ResourceModel and DomainModel.
Resource model
ORM-facing class responsible for table mapping, columns, indexes, and storage conversion.
Domain model
Business object carrying behavior, invariants, and ubiquitous language without persistence metadata.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal