adachsoft / dynamic-table-contract
Contract for dynamic table - interfaces, DTOs, query language
Package info
gitlab.com/a.adach/dynamic-table-contract
pkg:composer/adachsoft/dynamic-table-contract
Requires
- php: >=8.2
- adachsoft/collection: ^3.0
Requires (Dev)
- adachsoft/php-code-style: ^0.4
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.1
- rector/rector: ^2.4
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.
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 viaRowRepositoryInterface).
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.
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
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
}
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+ leafConditionInterfaceimplementations). - 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– extendsRowRepositoryInterfacewithfindBy(string $tableId, QueryParametersDto $query): QueryResultDto.QueryParametersDto– aggregates filter, sorts, pagination and selected columns (null = select all).QueryResultDto– returnsRowCollection, 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 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 (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 extractTypeMetadataand converts it to DTOs. ThrowsMissingTypeMetadataExceptionif attribute is missing.ColumnTypeDescriptionCollection– readonly collection ofTypeDescriptionDtoindexed by type name. Providesget(),find(),all(),has().TypeDescriptionDtoandConfigOptionDto– 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:
Schema definition (e.g. configuration, admin UI):
- Build
ColumnDtoinstances for each column. - Use
ColumnCollectionto group them. - Use
NewTableDto+TableRepositoryInterface::create()orupdate().
- Build
Data input (e.g. API or form submission):
- Accept raw data and create
RowDtoinstances. - Use a validator package that loads
TableDtovia repository, inspects columns, etc.
- Accept raw data and create
Querying data (new):
- Build
QueryParametersDtowith filters, sorting, pagination and projections using the Query Layer. - Call
findBy()onQueryableRowRepositoryInterface.
- Build
(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.
- immutable where applicable (
Bug reports and suggestions should focus on clarifying or extending contracts rather than on specific implementations.