[php-src] Issue #13174: `PDO::FETCH_CONSTRUCTOR` fetch mode

From: Date: Wed, 17 Jan 2024 00:28:26 +0000
Subject: [php-src] Issue #13174: `PDO::FETCH_CONSTRUCTOR` fetch mode
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-246280@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/13174 Author: 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: ```php readonly class User { public function __construct(public string $userId, public string $userName) { } } ``` Next, suppose you have a table in a database storing these users: ```sql 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: ```php $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()](https://www.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.members) > 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). 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: ```php $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 ``` 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: ```php $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: ```php readonly class User { public function __construct(public string $userId, public string $userName) { } public function wrapper(string $userId, string $userName): User { return new User($userId, $userName); } } ``` And then use PDO::FETCH_FUNC: ```php $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: ```php $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: ```php $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! :)

« previous php.bugs (#246280) next »