Introduction
ConstructorMapper
ConstructorMapper maps a database row directly into a class constructor by name. It performs no type coercion — values from the driver are passed through as-is. Use it when:
- The driver's PHP types (
int,string,bool,\DateTimeImmutable, …) already match your DTO's parameter types, - Column names match constructor parameter names exactly (use SQL
ASaliases when they don't).
For coercion (e.g. JSONB string → array, date string → \DateTimeImmutable), use TypeMapper instead — optionally chained in front of ConstructorMapper.
DSL
constructor_mapper(class-string<T> $class) : ConstructorMapper<T>
Returns a RowMapper<T> ready for fetchInto / fetchOneInto / fetchAllInto.
Basic Usage
<?php
use function Flow\PostgreSql\DSL\{constructor_mapper, pgsql_client, pgsql_connection};
readonly class User
{
public function __construct(
public int $id,
public string $name,
public string $email,
public bool $active,
) {}
}
$client = pgsql_client(pgsql_connection('host=localhost dbname=mydb'));
$user = $client->fetchInto(
constructor_mapper(User::class),
'SELECT id, name, email, active FROM users WHERE id = $1',
[1],
);
fetchInto() — First Object or Null
Returns the first row mapped to an object, or null if no rows:
<?php
$user = $client->fetchInto(
constructor_mapper(User::class),
'SELECT id, name, email, active FROM users WHERE email = $1',
['[email protected]'],
);
fetchOneInto() — Exactly One Object
Throws QueryException if zero or more than one row:
<?php
use Flow\PostgreSql\Client\Exception\QueryException;
try {
$user = $client->fetchOneInto(
constructor_mapper(User::class),
'SELECT id, name, email, active FROM users WHERE id = $1',
[1],
);
} catch (QueryException $e) {
// Row not found or multiple rows
}
fetchAllInto() — All Objects
Returns an array of objects:
<?php
/** @var User[] $users */
$users = $client->fetchAllInto(
constructor_mapper(User::class),
'SELECT id, name, email, active FROM users WHERE active = $1 ORDER BY name',
[true],
);
foreach ($users as $user) {
echo $user->name;
}
Column Name Aliasing
When DB columns use snake_case and your DTO uses camelCase, alias the columns in the SQL:
<?php
readonly class UserProfile
{
public function __construct(
public int $id,
public string $firstName,
public string $lastName,
public \DateTimeImmutable $createdAt,
) {}
}
$profile = $client->fetchInto(
constructor_mapper(UserProfile::class),
'SELECT id,
first_name AS "firstName",
last_name AS "lastName",
created_at AS "createdAt"
FROM users
WHERE id = $1',
[1],
);
Nullable Parameters
Nullable constructor parameters receive null when the DB value is NULL:
<?php
readonly class Product
{
public function __construct(
public int $id,
public string $name,
public float $price,
public ?string $description,
) {}
}
$products = $client->fetchAllInto(
constructor_mapper(Product::class),
'SELECT id, name, price, description FROM products',
);
Default Values
If a column is missing from the row but the constructor parameter has a default, the default is used:
<?php
readonly class FeatureFlag
{
public function __construct(
public string $name,
public bool $enabled = false,
) {}
}
$flag = $client->fetchInto(
constructor_mapper(FeatureFlag::class),
'SELECT name FROM flags WHERE name = $1',
['beta_ui'],
);
Extra Columns
Extra columns in the row that don't match constructor parameters are silently ignored — you can SELECT * and only consume what you declare:
<?php
readonly class UserSummary
{
public function __construct(
public int $id,
public string $name,
) {}
}
$summary = $client->fetchInto(
constructor_mapper(UserSummary::class),
'SELECT * FROM users WHERE id = $1',
[1],
);
Streaming via Cursor
<?php
$cursor = $client->cursor('SELECT id, name, email, active FROM large_users');
foreach ($cursor->map(constructor_mapper(User::class)) as $user) {
processUser($user);
}
See Cursors for details.
Custom RowMapper
When ConstructorMapper's 1:1 column-to-parameter rule isn't enough, implement RowMapper directly:
<?php
use Flow\PostgreSql\Client\RowMapper;
readonly class UserMapper implements RowMapper
{
/**
* @param array<string, mixed> $row
*/
public function map(array $row) : User
{
return new User(
id: (int) $row['id'],
name: $row['name'],
email: $row['email'],
active: $row['active'] === 't',
);
}
}
$user = $client->fetchInto(new UserMapper(), 'SELECT * FROM users WHERE id = $1', [1]);
Error Handling
ConstructorMapper throws Flow\PostgreSql\Client\Exception\MappingException when:
- The target class does not exist,
- The class has no constructor,
- A required parameter is missing from the row and has no default value and is not nullable.
Type Conversion in Mappings
PostgreSQL types are converted to PHP types by the driver before being passed to the mapper. The mapper receives:
| PostgreSQL Type | PHP Type |
|---|---|
| INTEGER, BIGINT | int |
| REAL, DOUBLE PRECISION | float |
| BOOLEAN | bool |
| TEXT, VARCHAR | string |
| TIMESTAMP, TIMESTAMPTZ | \DateTimeImmutable |
| DATE | \DateTimeImmutable |
| JSON, JSONB | string |
| BYTEA | string (binary) |
| NULL | null |
For explicit type coercion (JSONB → structure, string → DateTimeImmutable, etc.), see TypeMapper. For full type system control, see Type System.
When to Reach for Something Else
| Need | Use |
|---|---|
| Coerce JSONB string into array / nested structure | TypeMapper |
Coerce date string into \DateTimeImmutable |
TypeMapper with type_datetime() |
| Hydrate complex object graphs from JSONB | TypeMapper chained with PostgreSQL Valinor Bridge |