🔖

PDO::FETCH_CLASS の挙動が気持ち悪い件

2022/09/04に公開

PDO::FETCH_CLASS とは?

PDOStatement::fetch() するときに、戻り値であるデータセットを、連想配列や StdClass オブジェクトではなく、
指定したクラスのインスタンスで受け取れる、というもの。

諸事情あって久々に(ノーフレームワークな)生PHPを触っており、
DB から持ってきた値を Entity に突っ込む時に、いちいち回さなくてよいので便利そう、と思い立ち、試してみた。

PDO::FETCH_CLASS を試してみる。

public, protected, private なプロパティを持った Sample クラスに、DBから1行 fetch する。
なお、DBには、Sample クラスのプロパティに存在しないフィールドも持っている。

DB

public_property protected_property private_property not_existing_property
public_value protected_value private_value not_existing_value

テストコード

<?php
declare(strict_types=1);

class Sample {
    public $public_property = null;
    protected $protected_property = null;
    private $private_property = null;
}

$pdo = new PDO(/* 略 */);

$sql = "select * from sample";
$stmt = $pdo->query($sql);
$stmt->setFetchMode(PDO::FETCH_CLASS, Sample::class);
var_dump($stmt->fetch());

実行結果

/var/www/html/test1.php:15:
object(Sample)[3]
  public 'public_property' => string 'public_value' (length=12)
  protected 'protected_property' => string 'protected_value' (length=15)
  private 'private_property' => string 'private_value' (length=13)
  public 'not_existing_property' => string 'not_existing_value' (length=18)

突っ込みどころ

  • private や protectd のプロパティにも値がセットされる(!?)
  • 存在しないプロパティも、動的にpublicプロパティとして作られる(まぁわかる。PHP8.2で動的プロパティが禁止されたら、変わるかもしれない)

PDO::FETCH_CLASS とコンストラクタ、マジックメソッド __set()

続いて、Sample クラスにコンストラクタと、マジックメソッド __set() を追加してみる。

テストコード

<?php
declare(strict_types=1);

class Sample {
    public $public_property = null;
    protected $protected_property = null;
    private $private_property = null;

    public function __construct($attr = [])
    {
        var_dump('__construct', $attr);
    }

    public function __set($key, $value)
    {
        var_dump('__set', $key);
    }
}

$pdo = new PDO(/* 略 */);

$sql = "select * from sample";
$stmt = $pdo->query($sql);
$stmt->setFetchMode(PDO::FETCH_CLASS, Sample::class);
var_dump($stmt->fetch());

実行結果

/var/www/html/test2.php:16:string '__set' (length=5)
/var/www/html/test2.php:16:string 'not_existing_property' (length=21)

/var/www/html/test2.php:11:string '__construct' (length=11)
/var/www/html/test2.php:11:
array (size=0)
  empty

/var/www/html/test2.php:25:
object(Sample)[3]
  public 'public_property' => string 'public_value' (length=12)
  protected 'protected_property' => string 'protected_value' (length=15)
  private 'private_property' => string 'private_value' (length=13)

突っ込みどころ

  • 存在しないプロパティに対しては、__set() が呼ばれ、動的プロパティはつくられない(わかる)
  • private や protectd のプロパティにも値をセットしようとしても __set() が呼ばれない(!?)
  • 値がセットされた後にコンストラクタが呼ばれる(!?)
  • コンストラクタの引数には値が渡されない(必然的に、必須の引数があればエラー)

PDO::FETCH_CLASS と PDO::FETCH_PROPS_LATE

「値がセットされた後にコンストラクタが呼ばれる」件については、ちゃんとマニュアルに書いてあった。

PDO::FETCH_CLASS: 結果セットのカラムがクラス内の名前付けされたプロパティに マッピングされている、要求されたクラスの新規インスタンスを返します。 PDO::FETCH_PROPS_LATE を指定していない限りは、 カラムをマッピングしてからクラスのコンストラクタを呼び出します。

というわけで、PDO::FETCH_PROPS_LATE を併用

テストコード

<?php
declare(strict_types=1);

class Sample {
    public $public_property = null;
    protected $protected_property = null;
    private $private_property = null;

    public function __construct($attr = [])
    {
        var_dump('__construct', $attr);
    }

    public function __set($key, $value)
    {
        var_dump('__set', $key);
    }
}

$pdo = new PDO(/* 略 */);

$sql = "select * from sample";
$stmt = $pdo->query($sql);
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, Sample::class);
var_dump($stmt->fetch());

実行結果

/var/www/html/test3.php:11:string '__construct' (length=11)
/var/www/html/test3.php:11:
array (size=0)
  empty

/var/www/html/test3.php:16:string '__set' (length=5)
/var/www/html/test3.php:16:string 'not_existing_property' (length=21)

/var/www/html/test3.php:25:
object(Sample)[3]
  public 'public_property' => string 'public_value' (length=12)
  protected 'protected_property' => string 'protected_value' (length=15)
  private 'private_property' => string 'private_value' (length=13)

所感

private なプロパティにも直接値がセットされる、その際に __set() が呼ばれない、というのが致命的に感じられる。

通常、エンティティクラスの設計者はこんな使われ方は想定しないだろうし、バグの元になる可能性が高い。

コンストラクタに引数追加できないのも、なかなか苦しいし、コンストラクタが最後に呼ばれる挙動がデフォルトなのも、予期せぬ結果を引き起こす可能性が高い。

Discussion