It's important to note that PDO::FETCH_CLASS does not behave the way you would intuitively expect. Intuitively, you might expect that it either does not call the constructor at all and sets the properties "magically", or that it simply calls the constructor with the data from the database. Both assumptions are incorrect. It *does* call the constructor, but *not* with data from the database; it uses "magic" to set the properties. This can result in problems.
For example, one might intuitively expect code like this to work, perhaps as a starting point for doing Domain-Driven Design without a full-fat ORM. (It doesn't.)
<?php
final class Example{
private static ?PDO $pdo;
private function __construct(
public readonly int $id,
public readonly string $val1,
public readonly ?string $val2,
){}
public static function init(PDO $pdo){
static::$pdo = $pdo;
static::$pdo->exec("
CREATE TABLE IF NOT EXISTS example (
id INT AUTO_INCREMENT PRIMARY KEY,
val1 TEXT NULL,
val2 TEXT NOT NULL
)
");
}
public static function create(string $val1, ?string $val2=null){
if (!isset(static::$pdo)) throw new RuntimeException('Not yet initialized');
$query = static::$pdo->prepare("
INSERT INTO example
(val1, val2)
VALUES
(?, ?)
");
$query->execute([$val1, $val2]);
return static::fetch(static::$pdo->lastInsertId());
}
public static function fetch($id){
if (!isset(static::$pdo)) throw new RuntimeException('Not yet initialized');
$query = static::$pdo->prepare("SELECT * FROM example WHERE id = ?");
$query->execute([$id]);
$result = $query->fetchAll(PDO::FETCH_CLASS, 'Example');
if (!empty($result)) return $result[0];
}
}
?>
For a pattern like this to work, you'll instead need to do one of the following:
1. Use PDO::FETCH_FUNC instead of PDO::FETCH_CLASS, and pass it an anonymous function that calls the constructor.
2. Have a constructor that does nothing, and use the create() method to set properties directly. This would require removing the readonly modifier (though you could use PHP's new asymmetric visibility instead to achieve a similar effect). This also means that you can no longer actually use the constructor to fully construct an object (which, depending on your needs, may or may not be an acceptable compromise). You'll need a private constructor to avoid having partially-constructed objects floating around.
3. Use PDO::FETCH_ASSOC instead of PDO::FETCH_CLASS, then use ReflectionClass::newInstanceWithoutConstructor(), ReflectionObject::getProperty(), and ReflectionProperty::setValue() to implement "magical" class construction yourself.
4. Give up and use Doctrine or some other ORM.
There is, unfortunately, no solution that doesn't either have some compromise, require writing boilerplate code, or require adding a dependency to your project.