TypeMapper
BackTypeMapper validates and coerces a database row against a Flow\Types\Type from flow-php/types. Use it for:
- Strict type validation of incoming row data.
- Coercion of raw driver values, for example JSONB strings into arrays, date strings into
\DateTimeImmutable, or JSONB into typed structures orFlow\Types\Value\Json. - Composition with another
RowMapper, for exampleConstructorMapperor the PostgreSQL Valinor Bridge. The cast result goes to the next mapper.
Internally, TypeMapper::map() calls Type::cast($row). cast() is the lenient counterpart of assert(). It tries to coerce the value into the requested type before the validation. For JSONB-style columns, a JSON string that starts with { or [ is therefore automatically json_decode'd before the inner Type applies.
DSL
type_mapper(Type<TType> $type, ?RowMapper<TNext> $next = null) : TypeMapper<TType, TNext>
- With
$nextset tonull,map()returns the result of$type->cast(). - With a
$next, the cast result goes to$next->map(), and the return value of$nextis the output of the mapper.
Basic Usage - Validating a Row Shape
<?php
use function Flow\PostgreSql\DSL\{pgsql_client, pgsql_connection, type_mapper};
use function Flow\Types\DSL\{type_integer, type_optional, type_string, type_structure};
$client = pgsql_client(pgsql_connection('host=localhost dbname=mydb'));
$mapper = type_mapper(type_structure([
'id' => type_integer(),
'name' => type_string(),
'last_name' => type_optional(type_string()),
]));
$row = $client->fetchInto(
$mapper,
'SELECT id, name, last_name FROM users WHERE id = $1',
[1],
);
// $row === ['id' => 1, 'name' => 'Alice', 'last_name' => null]
Coercing JSONB Columns
Without the native decoding of the driver, PostgreSQL JSON and JSONB columns arrive as strings. TypeMapper handles all three common targets:
JSONB → Nested Structure
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_string, type_structure};
$mapper = type_mapper(type_structure([
'profile' => type_structure([
'name' => type_string(),
'address' => type_structure([
'city' => type_string(),
'country' => type_string(),
]),
]),
]));
// Row from driver: ['profile' => '{"name":"Alice","address":{"city":"Warsaw","country":"PL"}}']
$row = $client->fetchInto($mapper, 'SELECT profile FROM users WHERE id = $1', [1]);
// $row === ['profile' => ['name' => 'Alice', 'address' => ['city' => 'Warsaw', 'country' => 'PL']]]
JSONB → Flow\Types\Value\Json
To pass the JSON value around as a first-class object (with ->toArray(), __toString(), JsonSerializable):
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_json, type_structure};
$mapper = type_mapper(type_structure([
'metadata' => type_json(),
]));
// Row from driver: ['metadata' => '{"theme":"dark","notifications":true}']
$row = $client->fetchInto($mapper, 'SELECT metadata FROM users WHERE id = $1', [1]);
$json = $row['metadata']; // \Flow\Types\Value\Json
$json->toArray(); // ['theme' => 'dark', 'notifications' => true]
(string) $json; // '{"theme":"dark","notifications":true}'
JSONB → Plain String
For the raw JSON text, for example to send it to an HTTP response without a new encode:
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_string, type_structure};
$mapper = type_mapper(type_structure([
'metadata' => type_string(),
]));
// Row from driver: ['metadata' => '{"theme":"dark"}']
$row = $client->fetchInto($mapper, 'SELECT metadata FROM users WHERE id = $1', [1]);
// $row === ['metadata' => '{"theme":"dark"}']
JSONB Array → List of Structures
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_list, type_string, type_structure};
$mapper = type_mapper(type_structure([
'addresses' => type_list(type_structure([
'city' => type_string(),
'country' => type_string(),
])),
]));
// Row from driver: ['addresses' => '[{"city":"Warsaw","country":"PL"},{"city":"Berlin","country":"DE"}]']
$row = $client->fetchInto($mapper, 'SELECT addresses FROM users WHERE id = $1', [1]);
// $row === ['addresses' => [
// ['city' => 'Warsaw', 'country' => 'PL'],
// ['city' => 'Berlin', 'country' => 'DE'],
// ]]
Coercing Date / Time Columns
PostgreSQL TIMESTAMP / TIMESTAMPTZ already come back as \DateTimeImmutable from ext-pgsql. But text columns with date/time strings, for example ISO formats stored as TEXT, need explicit coercion.
String → \DateTimeImmutable
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_datetime, type_integer, type_structure};
$mapper = type_mapper(type_structure([
'id' => type_integer(),
'created_at' => type_datetime(),
]));
// Row from driver: ['id' => 42, 'created_at' => '2026-01-01 14:30:00']
$row = $client->fetchInto($mapper, 'SELECT id, created_at FROM events WHERE id = $1', [42]);
$row['created_at']; // \DateTimeImmutable
$row['created_at']->format('Y-m-d H:i:s'); // '2026-01-01 14:30:00'
type_datetime()->cast(...) accepts calendar date and datetime strings, numeric Unix timestamps,
\DateTime, \DateTimeImmutable, and \DateInterval. A string must be a real calendar date. A date-only string
becomes midnight. A wall-clock expression such as '', 'now' or '+12' is refused, so the same input never
produces a different value on a different run.
A string without an offset is read in the column zone: UTC for type_datetime(), never date.timezone. Declare
type_datetime('Europe/Warsaw') to read local timestamps.
String → Date
For DATE columns (no time component) stored as text:
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_date, type_structure};
$mapper = type_mapper(type_structure([
'birthday' => type_date(),
]));
// Row from driver: ['birthday' => '1990-04-15']
$row = $client->fetchInto($mapper, 'SELECT birthday FROM users WHERE id = $1', [1]);
$row['birthday']; // \DateTimeImmutable normalized to date
$row['birthday']->format('Y-m-d'); // '1990-04-15'
Coercing UUID Columns
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{type_string, type_structure, type_uuid};
$mapper = type_mapper(type_structure([
'id' => type_uuid(),
'name' => type_string(),
]));
// Row from driver: ['id' => '019d9293-793d-7036-918a-f19abffd545c', 'name' => 'Alice']
$row = $client->fetchInto($mapper, 'SELECT id, name FROM users WHERE id = $1', [$id]);
$row['id']; // \Flow\Types\Value\Uuid
(string) $row['id']; // '019d9293-793d-7036-918a-f19abffd545c'
Optional Fields
Absent and null are two different facts about a structure field. Declare a field as
structure_element(..., optional: true) for a column that the driver row can omit fully. Wrap its
type in type_optional(...) for a present column that can hold null:
<?php
use function Flow\PostgreSql\DSL\type_mapper;
use function Flow\Types\DSL\{structure_element, type_optional, type_string, type_structure};
$mapper = type_mapper(type_structure([
'name' => type_string(),
'nickname' => structure_element('nickname', type_string(), optional: true),
]));
// Row: ['name' => 'Alice', 'nickname' => 'Ali'] → ['name' => 'Alice', 'nickname' => 'Ali']
// Row: ['name' => 'Alice'] → ['name' => 'Alice']
Chaining Into Another RowMapper ($next)
Pass any RowMapper<T> as the second argument and TypeMapper will forward its cast result to it. This is useful for decode-then-hydrate pipelines: have TypeMapper clean up the raw row (decode JSONB, parse dates) and then hand the clean array to a class-aware mapper.
Chained with ConstructorMapper
<?php
use function Flow\PostgreSql\DSL\{constructor_mapper, type_mapper};
use function Flow\Types\DSL\{type_datetime, type_integer, type_string, type_structure};
readonly class Order
{
public function __construct(
public int $id,
public string $reference,
public \DateTimeImmutable $createdAt,
) {}
}
$mapper = type_mapper(
type_structure([
'id' => type_integer(),
'reference' => type_string(),
'createdAt' => type_datetime(),
]),
constructor_mapper(Order::class),
);
// Row: ['id' => 42, 'reference' => 'ORD-001', 'createdAt' => '2026-01-01 14:30:00']
$order = $client->fetchInto(
$mapper,
'SELECT id, reference, created_at AS "createdAt" FROM orders WHERE id = $1',
[42],
);
// $order instanceof Order, $order->createdAt instanceof \DateTimeImmutable
Chained with the Valinor Bridge
For complex object graphs (nested DTOs, lists of value objects), combine TypeMapper with the PostgreSQL Valinor Bridge. TypeMapper decodes JSONB and parses dates. Then Valinor builds the strict object tree.
Error Handling
If Type::cast() fails (or the chained $next mapper throws), TypeMapper wraps the error in Flow\PostgreSql\Client\Exception\MappingException. getPrevious() returns the original:
<?php
use Flow\PostgreSql\Client\Exception\MappingException;
try {
$row = $client->fetchInto($mapper, 'SELECT ... FROM ... WHERE id = $1', [$id]);
} catch (MappingException $e) {
$cause = $e->getPrevious(); // e.g. Flow\Types\Exception\CastingException
}
Found a typo or an outdated section? Edit this page on GitHub