adachsoft/dynamic-table-contract

Contract for dynamic table - interfaces, DTOs, query language

Maintainers

Package info

gitlab.com/a.adach/dynamic-table-contract

Issues

pkg:composer/adachsoft/dynamic-table-contract

Statistics

Installs: 17

Dependents: 3

Suggesters: 0

Stars: 0

v1.0.1 2026-04-24 08:38 UTC

This package is auto-updated.

Last update: 2026-04-24 06:38:53 UTC


README

PHP contract library for a dynamic table system (schema + data) shared between validator, storage, UI/form generator and API layers.

Goal

This package is contract-only. It defines immutable DTOs, collections and interfaces that describe a dynamic table system, without providing any business logic or persistence implementation.

It is designed to be used by multiple cooperating packages, for example:

  • validator – validates TableDto and RowDto using ColumnTypeInterface implementations,
  • storage (JSON / SQL / other) – persists tables and rows via repository interfaces,
  • form / UI generator – builds forms and widgets from column definitions and relation configs,
  • API layer – exposes and consumes tables and rows using the same contracts.

The contracts live here so that all these layers can evolve independently while still speaking the same language.

Requirements

  • PHP: >= 8.2
  • Strict types: every PHP file in this package uses declare(strict_types=1);
  • Only external runtime dependency:

Installation

composer require adachsoft/dynamic-table-contract

The package is autoloaded under the root namespace:

AdachSoft\DynamicTableContract\

What this package is not

This library intentionally does not contain:

  • business logic or validation logic,
  • repository implementations (no database / JSON / cache code),
  • framework integration (no Symfony / Laravel specific code),
  • service containers, factories or registries.

It only defines contracts that other packages are expected to implement.

Overview of contracts

DTOs (immutable data structures)

All DTOs are readonly classes and are meant to be simple data carriers.

TableDto

Namespace: AdachSoft\DynamicTableContract\Dto\TableDto

Represents a complete table: schema (columns) + data (rows).

use AdachSoft\DynamicTableContract\Collection\ColumnCollection;
use AdachSoft\DynamicTableContract\Collection\RowCollection;
use AdachSoft\DynamicTableContract\Dto\TableDto;

$table = new TableDto(
    id: 'clients',
    columns: new ColumnCollection([...]),
    rows: new RowCollection([...]),
);

Properties:

  • string $id – unique table identifier (e.g. "clients", "orders").
  • ColumnCollection $columns – collection of column definitions describing the table schema.
  • RowCollection $rows – collection of rows associated with this table.

NewTableDto (added in Unreleased)

Namespace: AdachSoft\DynamicTableContract\Dto\NewTableDto

Immutable input DTO used for create() and update() operations on tables.

use AdachSoft\DynamicTableContract\Collection\ColumnCollection;
use AdachSoft\DynamicTableContract\Dto\NewTableDto;

$newTable = new NewTableDto(
    id: 'clients',
    columns: new ColumnCollection([...])
);

Properties:

  • string $id – desired table identifier.
  • ColumnCollection $columns – column schema (rows are managed separately via RowRepositoryInterface).

ColumnDto

Namespace: AdachSoft\DynamicTableContract\Dto\ColumnDto

Describes a single column in a dynamic table.

use AdachSoft\DynamicTableContract\Dto\ColumnDto;
use AdachSoft\DynamicTableContract\Type\ColumnType;

$column = new ColumnDto(
    name: 'status',
    type: ColumnType::STRING,
    required: true,
    config: [
        'allowed_values' => ['new', 'active', 'archived'],
    ],
);
  • string $name – unique column name within a single table (e.g. "id", "name").
  • string $type – column type identifier. Typically one of the built‑in ColumnType::* constants, but can also be a custom string handled by external ColumnTypeInterface implementations.
  • bool $required – whether a value is required for this column.
  • array<string,mixed> $config – type‑specific configuration options.
    • For type relation this MUST contain a serialized representation of RelationConfigDto.

RowDto

Namespace: AdachSoft\DynamicTableContract\Dto\RowDto

Represents a single data row.

use AdachSoft\DynamicTableContract\Dto\RowDto;

$row = new RowDto([
    'id' => 1,
    'name' => 'Alice',
    'status' => 'active',
]);
  • array<string,mixed> $values – map from column name (ColumnDto::$name) to a raw value. No validation is performed at this level.

RelationConfigDto

Namespace: AdachSoft\DynamicTableContract\Dto\RelationConfigDto

Configuration for a column of type relation. Instances of this DTO are expected to be serialized into an array and stored in ColumnDto::$config.

use AdachSoft\DynamicTableContract\Dto\RelationConfigDto;

$relationConfig = new RelationConfigDto(
    targetTableId: 'clients',
    valueColumn: 'id',
    labelColumn: 'name',
    multiple: false,
);

Properties:

  • string $targetTableId – identifier of the target table (e.g. "clients", "dict_status").
  • string $valueColumn – column name in the target table that holds the foreign key value (e.g. "id").
  • string $labelColumn – column name in the target table that should be displayed to the user (e.g. "name").
  • bool $multiple – whether multiple records can be selected for this relation.

Collections

Collections are built on top of adachsoft/collection and provide strongly‑typed containers for DTOs.

TableCollection (added in Unreleased)

Namespace: AdachSoft\DynamicTableContract\Collection\TableCollection

Mutable collection of TableDto objects (extends AbstractCollection<TableDto>).

use AdachSoft\DynamicTableContract\Collection\TableCollection;

$tables = $repository->getAll(); // returns TableCollection
foreach ($tables->all() as $table) {
    // $table is TableDto
}
  • all(): TableDto[] – returns all tables.
  • Used by TableRepositoryInterface::getAll().

ColumnCollection

Namespace: AdachSoft\DynamicTableContract\Collection\ColumnCollection

  • Extends AdachSoft\Collection\AbstractCollection<ColumnDto>.
  • Stores ColumnDto instances indexed by column name (ColumnDto::$name).

Key semantics:

  • Keys are always strings equal to the column name.
  • Adding a column with the same name overwrites the previous definition.

Important methods:

  • add(ColumnDto $column): void – adds a column and indexes it by its name.
  • find(string $name): ?ColumnDto – returns the column definition for the given name, or null when not found.
  • all(): array<string,ColumnDto> – returns all column definitions indexed by name.

Typical usage:

use AdachSoft\DynamicTableContract\Collection\ColumnCollection;
use AdachSoft\DynamicTableContract\Dto\ColumnDto;
use AdachSoft\DynamicTableContract\Type\ColumnType;

$columns = new ColumnCollection();

$columns->add(new ColumnDto('id', ColumnType::INT, true, []));
$columns->add(new ColumnDto('name', ColumnType::STRING, true, []));
$columns->add(new ColumnDto('status', ColumnType::STRING, false, []));

$idColumn = $columns->find('id');        // ColumnDto|null
$allColumns = $columns->all();          // array<string, ColumnDto>

RowCollection

Namespace: AdachSoft\DynamicTableContract\Collection\RowCollection

  • Extends AdachSoft\Collection\AbstractCollection<RowDto>.
  • Stores RowDto instances using sequential numeric keys in insertion order.

Important methods:

  • add(RowDto $row): void (inherited from AbstractCollection).
  • all(): RowDto[] – returns all rows as a numerically indexed list.

Example:

use AdachSoft\DynamicTableContract\Collection\RowCollection;
use AdachSoft\DynamicTableContract\Dto\RowDto;

$rows = new RowCollection([
    new RowDto(['id' => 1, 'name' => 'Alice']),
    new RowDto(['id' => 2, 'name' => 'Bob']),
]);

foreach ($rows->all() as $row) {
    // $row is a RowDto
}

Query Layer (added in v0.4.0)

Namespace: AdachSoft\DynamicTableContract\Query

Provides a complete, immutable contract for complex queries (filtering, sorting, pagination, column projection and computed expressions).

Key patterns:

  • Composite Pattern for filters (AndFilter, OrFilter, NotFilter + leaf ConditionInterface implementations).
  • Value Objects – all classes are final readonly.
  • Named constructors (::column(), ::expression(), ::of(), ::asc(), ::desc()) for fluent and readable API.
  • Expression system – allows computed values in conditions, sorting and projections (concatenation, templates, JSON extraction, column references).

Main contracts

  • QueryableRowRepositoryInterface – extends RowRepositoryInterface with findBy(string $tableId, QueryParametersDto $query): QueryResultDto.
  • QueryParametersDto – aggregates filter, sorts, pagination and selected columns (null = select all).
  • QueryResultDto – returns RowCollection, total count and optional pagination info.
  • FilterInterface + ConditionInterface – tree of logical filters.
  • ExpressionInterface – computed values (used in conditions, projections, sorting).

Example usage

use AdachSoft\DynamicTableContract\Query\Dto\QueryParametersDto;
use AdachSoft\DynamicTableContract\Query\Expression\ColumnReferenceVo;
use AdachSoft\DynamicTableContract\Query\Expression\ConcatExpressionVo;
use AdachSoft\DynamicTableContract\Query\Filter\AndFilter;
use AdachSoft\DynamicTableContract\Query\Filter\Condition\GreaterThanOrEqualCondition;
use AdachSoft\DynamicTableContract\Query\Filter\Condition\IsNotNullCondition;
use AdachSoft\DynamicTableContract\Query\Filter\Condition\LikeCondition;
use AdachSoft\DynamicTableContract\Query\Pagination\PaginationVo;
use AdachSoft\DynamicTableContract\Query\Sort\SortCollection;
use AdachSoft\DynamicTableContract\Query\Sort\SortVo;

$ownerFullName = new ConcatExpressionVo([
    new ColumnReferenceVo('owners.name'),
    new ColumnReferenceVo('owners.surname'),
], ' ');

$filter = AndFilter::of(
    GreaterThanOrEqualCondition::column('visit_date', '2025-01-01'),
    LikeCondition::expression($ownerFullName, '%Kowalski%'),
    IsNotNullCondition::column('pet_owner_id'),
    LikeCondition::column('action', '%szczepienie%'),
);

$query = new QueryParametersDto(
    filter: $filter,
    sorts: new SortCollection([SortVo::desc('visit_date')]),
    pagination: new PaginationVo(page: 1, perPage: 25),
    selectedColumns: [
        'visit_date',
        'owner' => $ownerFullName,
        'action',
    ],
);

$result = $queryableRowRepository->findBy('pet_visits', $query);

See src/Query/ directory for all classes (SortVo, PaginationVo, all Condition*, all Expression*, collections and interfaces).

All classes are immutable, fully type-safe and come with unit tests.

Column type system

ColumnType

Namespace: AdachSoft\DynamicTableContract\Type\ColumnType

A set of string constants representing built-in column types:

final class ColumnType
{
    public const string STRING   = 'string';
    public const string INT      = 'int';
    public const string FLOAT    = 'float';
    public const string BOOL     = 'bool';
    public const string DATE     = 'date';
    public const string NUMERIC  = 'numeric';
    public const string MONEY    = 'money';
    public const string JSON     = 'json';
    public const string RELATION = 'relation';

    private function __construct() {}
}

Important: this is not an enum on purpose. The type system is open – external packages may define additional types by implementing ColumnTypeInterface and returning their own getName() values.

ColumnTypeInterface

Namespace: AdachSoft\DynamicTableContract\Type\ColumnTypeInterface

Contract for all column type implementations. This package does not provide any concrete types.

interface ColumnTypeInterface
{
    public function getName(): string;

    /**
     * @param array<string, mixed> $config
     */
    public function validate(mixed $value, array $config = []): bool;

    /**
     * @param array<string, mixed> $config
     */
    public function normalize(mixed $value, array $config = []): mixed;
}

Guidelines:

  • getName() should return a string that matches either a value from ColumnType or a custom value used across your system.
  • validate() should not throw exceptions; it returns true/false.
  • normalize() converts the raw value into the desired PHP type (e.g. stringDateTimeImmutable, stringMoney object, etc.).

A typical validator package would:

  1. Resolve a ColumnTypeInterface implementation based on ColumnDto::$type.
  2. Call validate($value, $config) to check the value.
  3. Call normalize($value, $config) to convert it for further processing or storage.

Repository interfaces

The repository interfaces describe how table definitions and rows can be persisted and loaded. This package does not provide any implementations.

TableRepositoryInterface (updated in Unreleased)

Namespace: AdachSoft\DynamicTableContract\Repository\TableRepositoryInterface

Now provides full CRUD in a single interface (consistent with RowRepositoryInterface).

use AdachSoft\DynamicTableContract\Collection\TableCollection;
use AdachSoft\DynamicTableContract\Dto\NewTableDto;
use AdachSoft\DynamicTableContract\Dto\TableDto;
use AdachSoft\DynamicTableContract\Exception\DuplicateTableExceptionInterface;
use AdachSoft\DynamicTableContract\Exception\TableNotFoundExceptionInterface;

interface TableRepositoryInterface
{
    public function save(TableDto $tableDto): void;

    /**
     * @throws TableNotFoundExceptionInterface
     */
    public function get(string $id): TableDto;

    /**
     * Creates a new table. Throws DuplicateTableExceptionInterface if ID exists.
     */
    public function create(NewTableDto $newTableDto): TableDto;

    /**
     * Returns all tables (empty collection if none exist).
     */
    public function getAll(): TableCollection;

    /**
     * Updates table (supports rename). Throws exceptions on missing table or ID conflict.
     */
    public function update(string $id, NewTableDto $newTableDto): TableDto;

    /**
     * Deletes a table. Throws TableNotFoundExceptionInterface if not found.
     */
    public function delete(string $id): void;
}

RowRepositoryInterface

Namespace: AdachSoft\DynamicTableContract\Repository\RowRepositoryInterface

use AdachSoft\DynamicTableContract\Collection\RowCollection;
use AdachSoft\DynamicTableContract\Dto\RowDto;

interface RowRepositoryInterface
{
    public function add(string $tableId, RowDto $row): void;

    public function getAll(string $tableId): RowCollection;
}

QueryableRowRepositoryInterface (new)

Extends RowRepositoryInterface with powerful query capabilities (see Query Layer section above).

Exception contracts

All exceptions in the dynamic table system share a common root interface.

Hierarchy (updated):

TableExceptionInterface  (extends \Throwable)
├── TableNotFoundExceptionInterface
├── DuplicateTableExceptionInterface (new)
├── DuplicateRowExceptionInterface
├── ValidationExceptionInterface
├── ColumnNotFoundExceptionInterface
└── UnknownTypeExceptionInterface

DuplicateTableExceptionInterface (added in Unreleased)

Namespace: AdachSoft\DynamicTableContract\Exception\DuplicateTableExceptionInterface

Thrown when attempting to create or rename a table using an ID that already exists.

namespace AdachSoft\DynamicTableContract\Exception;

interface DuplicateTableExceptionInterface extends TableExceptionInterface
{
}

Other exceptions remain unchanged.

Type Self-Description System (new)

This release introduces a self-describing type system using PHP 8 attributes.

#[TypeMetadata]

Namespace: AdachSoft\DynamicTableContract\Type\Attribute\TypeMetadata

Every ColumnTypeInterface implementation should be decorated with this attribute to provide human-readable metadata and configuration documentation.

use AdachSoft\DynamicTableContract\Type\Attribute\TypeMetadata;
use AdachSoft\DynamicTableContract\Type\Attribute\ConfigOption;

#[TypeMetadata(
    label: 'Data i czas',
    description: 'Handles datetime values...',
    configOptions: [
        new ConfigOption('format', 'Input format', 'string', false, 'Y-m-d H:i:s'),
    ],
    displayConfigOptions: [
        new ConfigOption('format', 'Display format', 'string', false, 'Y-m-d H:i:s'),
    ]
)]
final class DateTimeType implements ColumnTypeInterface { ... }

Supporting classes

  • TypeDescriptorReader – uses Reflection to extract TypeMetadata and converts it to DTOs. Throws MissingTypeMetadataException if attribute is missing.
  • ColumnTypeDescriptionCollection – readonly collection of TypeDescriptionDto indexed by type name. Provides get(), find(), all(), has().
  • TypeDescriptionDto and ConfigOptionDto – immutable data carriers for metadata.

New built-in types

  • TimestampType (timestamp) – Unix timestamp (int).
  • DateTimeType (datetime) – Full datetime support with configurable formats.

All existing built-in types have also been decorated with appropriate #[TypeMetadata] attributes.

Usage example

$reader = new \AdachSoft\DynamicTableContract\Type\TypeDescriptorReader();
$collection = new \AdachSoft\DynamicTableContract\Type\Collection\ColumnTypeDescriptionCollection(
    ...array_map(
        fn($type) => $reader->read($type),
        $allTypeInstances
    )
);

$description = $collection->get('datetime'); // TypeDescriptionDto

This system allows external tools (UI generators, documentation generators, admin panels) to discover available types, their options and documentation without hard-coded knowledge.

All new exceptions implement TableExceptionInterface.

Putting it together – example flow

A typical high‑level flow using this contract might look like this:

  1. Schema definition (e.g. configuration, admin UI):

    • Build ColumnDto instances for each column.
    • Use ColumnCollection to group them.
    • Use NewTableDto + TableRepositoryInterface::create() or update().
  2. Data input (e.g. API or form submission):

    • Accept raw data and create RowDto instances.
    • Use a validator package that loads TableDto via repository, inspects columns, etc.
  3. Querying data (new):

    • Build QueryParametersDto with filters, sorting, pagination and projections using the Query Layer.
    • Call findBy() on QueryableRowRepositoryInterface.

(The rest of the flow remains consistent with previous version.)

Contributing

This repository is designed as a thin contract layer. When contributing, please keep in mind:

  • Do not introduce business logic or storage logic into this package.
  • Any new public contract (DTO / interface / collection) must be:
    • immutable where applicable (readonly),
    • fully documented in PHPDoc and in this README.md,
    • consistent with existing naming and namespace conventions.

Bug reports and suggestions should focus on clarifying or extending contracts rather than on specific implementations.