ConstructorMapper
BackConstructorMapper maps a database row directly into a class constructor by name. It does no type coercion. Values from the driver go through as they are. Use it in these cases:
- The PHP types of the driver (
int,string,bool,\DateTimeImmutable, ...) already match the parameter types of your DTO. - Column names match constructor parameter names exactly. For names that do not match, use SQL
ASaliases.
For coercion (for example JSONB string → array, date string → \DateTimeImmutable), use TypeMapper instead. You can chain it before 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 for no rows:
<?php
$user = $client->fetchInto(
constructor_mapper(User::class),
'SELECT id, name, email, active FROM users WHERE email = $1',
['john@example.com'],
);
fetchOneInto() - Exactly One Object
Throws QueryException for zero rows 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
For DB columns in snake_case and a DTO in 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 for a DB value of 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 and 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 do not match constructor parameters are silently ignored. You can SELECT * and use only 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
function processUser(object $user): void { /* your code */ }
$cursor = $client->cursor('SELECT id, name, email, active FROM large_users');
foreach ($cursor->map(constructor_mapper(User::class)) as $user) {
processUser($user);
}
For more information, read Cursors.
Custom RowMapper
If the 1:1 column-to-parameter rule of ConstructorMapper is not sufficient, 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 in these cases:
- 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
The driver converts PostgreSQL types to PHP types before the mapper gets them. 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 (for example JSONB → structure or string → DateTimeImmutable), read TypeMapper. For full type system control, read 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 |
Found a typo or an outdated section? Edit this page on GitHub