Skip to content

PDO::FETCH_CONSTRUCTOR fetch mode #13174

@xpple

Description

@xpple

Description

Dear PHP team,

I hereby want to propose a new fetch mode for PDO.

Motivation

To explain its necessity, first suppose you have a class User:

readonly class User {
    public function __construct(public string $userId, public string $userName) {
    }
}

Next, suppose you have a table in a database storing these users:

CREATE TABLE users (
    user_id int NOT NULL AUTO_INCREMENT,
    user_name varchar(256) NOT NULL,
    PRIMARY KEY (user_id)
);

All good. Now, suppose you want to fetch these users from the database using PDO and automatically convert them to objects of type User. You might try the following:

$connection = /* ... */
$statement = $connection->prepare(<<<SQL
    SELECT user_id, user_name
    FROM users
    SQL);
$statement->execute();
$results = $statement->fetchAll(PDO::FETCH_CLASS, User::class);

$var_dump($results);

However, you'll get the following error:

Uncaught Error: Cannot create dynamic property User::$user_id

Shame, but expected. After all:

PDO::FETCH_CLASS (int)
Specifies that the fetch method shall return a new instance of the requested class, mapping the columns to named properties in the class.
Note: The magic __set() method is called if the property doesn't exist in the requested class

https://www.php.net/manual/en/pdo.constants.php#pdo.constants.fetch-class

That is, the property names of the class don't match with the fields in the table (userId vs. user_id and userName vs. user_name). In general, the names of the fields of the table need not match the names of the properties of the class. This actually also goes wrong for a second reason. Even if the names had matched, the constructor would be called with zero arguments, resulting in the following error (check this by supplying PDO::FETCH_PROPS_LATE as well):

Uncaught ArgumentCountError: Too few arguments to function User::__construct(), 0 passed and exactly 2 expected

Is it, then, really impossible to create instances of User automatically? Well, maybe we can use PDO::FETCH_FUNC together with the constructor to fix this:

$statement->fetchAll(PDO::FETCH_FUNC, User::class.'::__construct');

However, the __construct method can not be used statically in contrary to Java for example (User::new), so you get the following error:

Uncaught TypeError: non-static method User::__construct() cannot be called statically

Note that you also cannot do $statement->fetchAll(PDO::FETCH_FUNC, new User(...)); for reasons described here. So yeah, it seems it is not possible to do this neatly using the current fetch modes.

Workarounds

With that said, let's look at some of the workarounds. Before I was looking into all these fetch options, I simply used the following:

$results = $statement->fetchAll(PDO::FETCH_NUM);
$results = array_map(static fn($result) => new User(...$result), $results);

This is not ideal, as the results need to be traversed again only to be typed correctly. Another solution would be to simply create a wrapper for the constructor in the User class:

readonly class User {
    public function __construct(public string $userId, public string $userName) {
    }

    public static function wrapper(string $userId, string $userName): User {
        return new User($userId, $userName);
    }
}

And then use PDO::FETCH_FUNC:

$statement->fetchAll(PDO::FETCH_FUNC, User::class.'::wrapper');

This is also not ideal, as you have to keep on changing the wrapper function as the class changes. You could also inline the function if you do not want to make a wrapper:

$statement->fetchAll(PDO::FETCH_FUNC, static fn (...$props) => new User(...$props));

This is slightly less efficient than the wrapper function, but works relatively well. This way, you do not have to maintain a wrapper function. However it is still quite a hack in my opinion. At least such a hack, that I doubt many people will even find this solution.

Proposal

For this reason, I propose a new fetch mode: PDO::FETCH_CONSTRUCTOR. It would basically do what my last workaround did: unpack the results from each row into the constructor. Now, I understand some concerns with this approach. For one, the arguments might not be in the correct order. Although this could easily be fixed by changing the SQL query, you could also pass an (associative) array that describes the order among other things. As usual, you could use setFetchMode to provide fixed constructor arguments.

In this example case, the resulting code would be the following:

$statement->fetchAll(PDO::FETCH_CONSTRUCTOR, User::class);

I said that my last workaround would probably not be found by many people, so I guess another option would be to give this workaround as an example on the PHP website.

Thank you for reading, please let me know what you think! :)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions