adachsoft / dynamic-table-contract
Contract library for dynamic table system
Package info
gitlab.com/a.adach/dynamic-table-contract
pkg:composer/adachsoft/dynamic-table-contract
Requires
- php: >=8.2
- adachsoft/collection: ^3.0
- adachsoft/dynamic-table-json: dev-main@dev
Requires (Dev)
- adachsoft/php-code-style: ^0.4.3
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.1
- rector/rector: ^2.4
This package is not auto-updated.
Last update: 2026-04-11 11:03:17 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
TableDtoandRowDtousingColumnTypeInterfaceimplementations, - 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.
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‑inColumnType::*constants, but can also be a custom string handled by externalColumnTypeInterfaceimplementations.bool $required– whether a value is required for this column.array<string,mixed> $config– type‑specific configuration options.- For type
relationthis MUST contain a serialized representation ofRelationConfigDto.
- For type
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.
ColumnCollection
Namespace: AdachSoft\DynamicTableContract\Collection\ColumnCollection
- Extends
AdachSoft\Collection\AbstractCollection<ColumnDto>. - Stores
ColumnDtoinstances 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, ornullwhen 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
RowDtoinstances using sequential numeric keys in insertion order.
Important methods:
add(RowDto $row): void(inherited fromAbstractCollection).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
}
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 fromColumnTypeor a custom value used across your system.validate()should not throw exceptions; it returnstrue/false.normalize()converts the raw value into the desired PHP type (e.g.string→DateTimeImmutable,string→Moneyobject, etc.).
A typical validator package would:
- Resolve a
ColumnTypeInterfaceimplementation based onColumnDto::$type. - Call
validate($value, $config)to check the value. - 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
Namespace: AdachSoft\DynamicTableContract\Repository\TableRepositoryInterface
use AdachSoft\DynamicTableContract\Dto\TableDto;
use AdachSoft\DynamicTableContract\Exception\TableNotFoundExceptionInterface;
interface TableRepositoryInterface
{
public function save(TableDto $table): void;
/**
* @throws TableNotFoundExceptionInterface
*/
public function get(string $id): TableDto;
}
save(TableDto $table): void– persists a table definition.get(string $id): TableDto– loads a table definition by id or throwsTableNotFoundExceptionInterfaceif not found.
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;
}
add(string $tableId, RowDto $row): void– stores a new row for the given table.getAll(string $tableId): RowCollection– returns all rows for the given table.
Typical storage packages (JSON, SQL, etc.) implement these interfaces and use the DTOs and collections from this contract.
Exception contracts
All exceptions in the dynamic table system share a common root interface.
Hierarchy:
TableExceptionInterface (extends \Throwable)
├── TableNotFoundExceptionInterface
├── ValidationExceptionInterface
├── ColumnNotFoundExceptionInterface
└── UnknownTypeExceptionInterface
Namespaces:
AdachSoft\DynamicTableContract\Exception\TableExceptionInterfaceAdachSoft\DynamicTableContract\Exception\TableNotFoundExceptionInterfaceAdachSoft\DynamicTableContract\Exception\ValidationExceptionInterfaceAdachSoft\DynamicTableContract\Exception\ColumnNotFoundExceptionInterfaceAdachSoft\DynamicTableContract\Exception\UnknownTypeExceptionInterface
Intended usage:
TableExceptionInterface– base marker interface for all table‑related exceptions.TableNotFoundExceptionInterface– thrown when a table cannot be found in a repository.ValidationExceptionInterface– thrown when table structure or row data fails validation.ColumnNotFoundExceptionInterface– thrown when an expected column definition is missing.UnknownTypeExceptionInterface– thrown when a column type identifier is not recognized.
Storage, validator and UI packages are expected to implement concrete exception classes that implement these interfaces.
Putting it together – example flow
A typical high‑level flow using this contract might look like this:
Schema definition (e.g. configuration, admin UI):
- Build
ColumnDtoinstances for each column. - Use
ColumnCollectionto group them. - Save the resulting
TableDtoviaTableRepositoryInterface.
- Build
Data input (e.g. API or form submission):
- Accept raw data and create
RowDtoinstances. - Use a validator package that:
- loads
TableDtoviaTableRepositoryInterface, - inspects
TableDto::$columnsto know what is required and which types to use, - for each column resolves a
ColumnTypeInterface, - calls
validate()andnormalize()for every value, - throws
ValidationExceptionInterfaceor more specific exceptions on error.
- loads
- Accept raw data and create
Persistence of data:
- After successful validation, pass
RowDtoinstances to an implementation ofRowRepositoryInterface.
- After successful validation, pass
Rendering / UI:
- A UI package can inspect
ColumnCollection(names, types, configs) to build forms or grids, - and use
RowCollectionto render data rows.
- A UI package can inspect
This package stays focused on these contracts so that other packages can implement the actual behavior independently.
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.
- immutable where applicable (
Bug reports and suggestions should focus on clarifying or extending contracts rather than on specific implementations.