-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Description
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 classhttps://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! :)