flow php

StaticFactoryMapper

StaticFactoryMapper delegates row → object construction to a public static factory method on the target class. Use it when:

  • The target class has a private constructor and a named static factory (User::fromRow(...)),
  • You want to keep row-to-object logic on the domain class itself instead of scattering it across mapper implementations,
  • You need custom coercion inside the factory (e.g. created_at string → \DateTimeImmutable, JSONB string → decoded array).

The factory method must have this exact signature:

public static function fromRow(array $row) : self;

The method does not receive the mapping Context. If your factory needs access to the originating Query, executing Client, or user-supplied data, implement RowMapper directly — see the Custom RowMapper section of the ConstructorMapper documentation.

For simple 1:1 constructor-parameter mapping, use ConstructorMapper. For type-driven coercion via flow-php/types, use TypeMapper.

DSL

static_factory_mapper(class-string<T> $class, non-empty-string $method) : StaticFactoryMapper<T>

Returns a RowMapper<T> ready for fetchInto / fetchOneInto / fetchAllInto / Cursor::map().

Basic Usage

<?php

use function Flow\PostgreSql\DSL\{pgsql_client, pgsql_connection, static_factory_mapper};

final readonly class User
{
    private function __construct(
        public int $id,
        public string $name,
        public string $email,
        public \DateTimeImmutable $createdAt,
    ) {
    }

    /**
     * @param array<string, mixed> $row
     */
    public static function fromRow(array $row) : self
    {
        return new self(
            id: (int) $row['id'],
            name: (string) $row['name'],
            email: (string) $row['email'],
            createdAt: new \DateTimeImmutable((string) $row['created_at']),
        );
    }
}

$client = pgsql_client(pgsql_connection('host=localhost dbname=mydb'));

$user = $client->fetchOneInto(
    static_factory_mapper(User::class, 'fromRow'),
    'SELECT id, name, email, created_at FROM users WHERE id = $1',
    [1],
);

fetchInto() — First Object or Null

Returns the first row mapped via the factory, or null if no rows:

<?php

$user = $client->fetchInto(
    static_factory_mapper(User::class, 'fromRow'),
    'SELECT id, name, email, created_at FROM users WHERE email = $1',
    ['[email protected]'],
);

fetchAllInto() — All Objects

<?php

/** @var User[] $users */
$users = $client->fetchAllInto(
    static_factory_mapper(User::class, 'fromRow'),
    'SELECT id, name, email, created_at FROM users ORDER BY name',
);

foreach ($users as $user) {
    echo $user->name;
}

Streaming via Cursor

<?php

$cursor = $client->cursor('SELECT id, name, email, created_at FROM large_users');

foreach ($cursor->map(static_factory_mapper(User::class, 'fromRow')) as $user) {
    processUser($user);
}

See Cursors for details.

Error Handling

StaticFactoryMapper throws Flow\PostgreSql\Client\Exception\MappingException in these cases:

Condition Message pattern
$class does not exist Failed to map row to "<class>": Class does not exist
Factory method does not exist on $class Static factory method "<class>::<method>()" does not exist
Factory method exists but is not declared static Factory method "<class>::<method>()" must be declared static
Factory method is declared but not public Factory method "<class>::<method>()" must be declared public
Factory method throws any \Throwable Failed to map row to "<class>": <original message>, with the original exception available via MappingException::getPrevious()

Validation of $class / $method (existence, static, public) runs eagerly in the constructor — a misconfigured mapper fails fast at wiring time, not at query time. Exceptions thrown by the factory method itself surface on the matching map() call and are wrapped in a MappingException whose getPrevious() returns the original throwable.

When to Reach for Something Else

Need Use
1:1 column-to-constructor-parameter mapping with no coercion ConstructorMapper
Row validation / coercion via flow-php/types TypeMapper
Access to Context (query / parameters / client / catalog) in your mapping logic Implement RowMapper directly
Strict object hydration of complex graphs PostgreSQL Valinor Bridge

Contributors

Join us on GitHub external resource
scroll back to top