[php-src] Issue #13174: `PDO::FETCH_CONSTRUCTOR` fetch mode
| From: | xpple | 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! :)