← Persistence

PERSISTENCE FEATURE

Shared Table Extension

This is the ORM painkiller: later modules add columns to an existing table without reopening the original resource 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

Two modules can point at the same table and contribute their own columns without one module editing the other module's resource class.

How it works

SchemaCollector groups discovered ORM resources by table name. When another resource maps to the same table, it adds only missing columns and leaves already-defined columns alone.

Why it matters

This removes one of the most painful ownership traps in modular systems: the first module does not permanently own the whole table forever. Later modules stay additive instead of invasive.

Key concepts

Shared table extension
Multiple ORM resources map to one physical table and extend its schema collaboratively.
SchemaCollector
Collects table definitions from resource attributes and merges columns by table name.
Module isolation
A later module can add persistence fields without reopening the source code of the original module.

ORM Kill Feature

One table. Two modules. Zero cross-module edits.

In most stacks, the second module has to reopen the first module's model just to add a few extra columns. Semitexa keeps that additive: both resources point at the same table, and schema collection merges the result.

Why teams hate the usual approach

  • One module usually becomes the permanent owner of a table, even when another bounded context needs to extend it later.
  • The follow-up module often has to edit the original model class, creating ownership bleed and merge pressure between teams.
  • That coupling turns harmless extra columns into a cross-module coordination problem instead of an additive change.

Base resource

Catalog Module

Owns the initial product shape that the storefront and admin tools already depend on.

id tenant_id name description price status

Extension resource

Merchandising Module

Arrives later and adds campaign-only fields without reopening the catalog module.

badge_label merch_priority campaign_code
Merged column Owned by Why it stays safe
id Catalog Base primary key stays untouched.
tenant_id Catalog Tenant scoping still applies to the shared table.
name Catalog Core commerce data remains where it started.
description Catalog Existing product content is unchanged.
price Catalog Base module continues to own pricing semantics.
status Catalog Lifecycle state stays with the core domain.
badge_label Merchandising Added later for campaign UX.
merch_priority Merchandising Supports merchandising sort logic.
campaign_code Merchandising Connects rows to external campaign flows.
#[FromTable] SchemaCollector Module isolation #[Column] #[TenantScoped]

Verified against Semitexa Ultimate 2026.09.19.1020

Shared Table Extension

Two ResourceModel classes in different modules can target the same physical table with #[FromTable]. The SchemaCollector merges their column sets into one schema plan.

How it works

The base module defines the core resource with #[FromTable('products')] and declares its columns. A later module creates its own resource, also pointing at #[FromTable('products')], and declares only its additional columns. At sync time, SchemaCollector groups all discovered resources by table name and only adds missing columns — it never redefines existing ones. Neither module needs to open or modify the other's class.

Why this matters

Without shared table extension, a later module either has to edit the original resource class (creating cross-module ownership bleed) or maintain a separate table (creating join complexity). The additive model keeps module boundaries clean and schema evolution safe.

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

Catalog Module Resource Presentation boundary
<?phpdeclare(strict_types=1);namespace App\Catalog\Resource;use Semitexa\Orm\Adapter\MySqlType;use Semitexa\Orm\Attribute\Column;use Semitexa\Orm\Attribute\FromTable;use Semitexa\Orm\Attribute\PrimaryKey;use Semitexa\Orm\Attribute\TenantScoped;#[FromTable(name: 'demo_products')]#[TenantScoped(strategy: 'same_storage')]final class CatalogProductResource{    #[PrimaryKey(strategy: 'uuid')]    #[Column(type: MySqlType::Binary, length: 16)]    public string $id;    #[Column(type: MySqlType::Varchar, length: 64)]    public string $tenant_id;    #[Column(type: MySqlType::Varchar, length: 190)]    public string $name;    #[Column(type: MySqlType::Text)]    public ?string $description = null;    #[Column(type: MySqlType::Decimal, precision: 10, scale: 2)]    public string $price;    #[Column(type: MySqlType::Varchar, length: 32)]    public string $status = 'draft';}

Design Rules

Why this works without hidden magic

Both modules point to the same physical table name via #[FromTable].

SchemaCollector groups discovered resources by table and only adds missing columns.

The extension module contributes new columns without redefining the catalog columns.

No module needs to reopen the other module's class just to add campaign-specific state.

How it works

SchemaCollector groups discovered ORM resources by table name. When another resource maps to the same table, it adds only missing columns and leaves already-defined columns alone.

Why it matters

This removes one of the most painful ownership traps in modular systems: the first module does not permanently own the whole table forever. Later modules stay additive instead of invasive.

Key concepts

Shared table extension
Multiple ORM resources map to one physical table and extend its schema collaboratively.
SchemaCollector
Collects table definitions from resource attributes and merges columns by table name.
Module isolation
A later module can add persistence fields without reopening the source code of the original module.

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

Donate via PayPal