忙しい人向けのまとめ
- PHP で Postgres のデータを取得すると、各カラムは文字列
- boolean 型を取得したときは "t" または "f"
- 普段 PHP で bool 型を扱っている時と同じように扱うには、テーブル定義に合わせて記述するクラスに boolean のカラムは含めず、マジックメソッドで別途処理するとよい
背景
通常、PHP で Postgres からデータを取得する際、各カラムの値が文字列で取得されます。
boolean 型のカラムは true が "t"
、false が "f"
で取得されます。
論理型の値は "t" あるいは "f" の形式で返します。 配列を含むそれ以外の型は、PostgreSQL のやりかたにしたがって文字列として フォーマットされた形式で返します。
この仕様をあらかじめ把握していない場合、私たちは取得した値を if のカッコにそのまま放り込んでしまいます。
"f"
は bool にキャストすると true ですから、本来の意図に反して if 節の中の処理が実行されてしまうのです。
なので、PHP と Postgres を扱う方は if ($is_deleted === 't')
という風な書き方をする必要があるのですが、いちいちこのように書いていては PHP で bool 型を扱う感覚ともかけ離れてしまうし、仮にこの書き方を忘れてしまったとしてもレビュー工程で見過ごされてしまうのではないか、と思うのです(テストでカバーされていれば話は別ですが)。
なので、 PHP でいつも bool 型を扱う感じで if ($is_deleted)
などと書いても不具合が起きないように私が採用している方法をご紹介します。
環境
Docker で PHP および Postgres の環境を用意します。
version: '3'
services:
php:
build:
context: php
volumes:
- ../src:/src
working_dir: /src
tty: true
db:
image: postgres:14-alpine
volumes:
- ./db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
POSTGRES_PASSWORD: root
POSTGRES_USER: root
POSTGRES_DB: root
ports:
- "5432:5432"
FROM php:8.1-alpine
RUN apk update \
&& apk add libpq-dev \
&& docker-php-ext-install pgsql
CREATE TABLE participant
(
"id" SERIAL NOT NULL,
"name" VARCHAR,
"can_drink" BOOLEAN
);
INSERT INTO participant
("name", "can_drink")
VALUES
('John', TRUE),
('Jane', FALSE);
用意するデータ
今回は飲み会の参加者に提供する飲み物を決めるという想定で、名前と飲酒可能かどうかを格納するテーブルを用意します。
id | name | can_drink |
---|---|---|
1 | John | true |
2 | Jane | false |
pg_fetch_object 関数を用いて取得する
DB の定義に合わせてプロパティを設定
PHP の PostgreSQL 向けモジュールには、データの行を事前に定義したクラスで取得できる pg_fetch_object
という関数があります。
型とカラム名をテーブル定義に合わせた、以下のようなクラスを使用します。
<?php
class Participant1
{
public int $id;
public string $name;
public bool $can_drink;
}
取得
<?php
require_once __DIR__ . '/Participant1.php';
$db_conn = pg_connect('host=db port=5432 dbname=root user=root password=root');
$qu = pg_query($db_conn, 'SELECT * FROM participant');
while ($data = pg_fetch_object($qu, null, Participant1::class)) {
$name = $data->name;
$canDrink = $data->can_drink;
$message = $canDrink
? "$name can drink!"
: "$name cannot drink...";
echo $message, PHP_EOL;
}
結果
John can drink!
Jane can drink!
データ上では Jane さんは can_drink = false
(飲めない) のはずなのですが、飲める!と出てしまいました。。。
これでは Jane さんが困ってしまいます 😢
各行が取得されたときのクラスを見ると、たしかに Jane さんの can_drink
が true
になっています。
object(Participant1)#3 (3) {
["id"]=>
int(1)
["name"]=>
string(4) "John"
["can_drink"]=>
bool(true)
}
object(Participant1)#4 (3) {
["id"]=>
int(2)
["name"]=>
string(4) "Jane"
["can_drink"]=>
bool(true)
}
これは bool 型の $can_drink
に "f"
を入れようとしたときに bool 型にキャストされて true
になってしまった影響と考えられます。
これを解決するため、クラスを以下のように改良します。
boolean のカラムだけプロパティを直接定義せず、マジックメソッドで正しい値を埋め込む
<?php
/**
* boolean のカラムだけプロパティを直接定義せず、マジックメソッドで正しい値を埋め込むようにする。
*
* @property bool $can_drink
*/
class Participant2
{
/**
* boolean 値として認識させたいカラムをここに記述する。
*
* @var string[]
*/
protected static array $booleanColumns = [
'can_drink',
];
/**
* string や int など、キャストされても意味合いが変わらない場合は、このようにプロパティで記述する。
*
* @var int
*/
public int $id;
public string $name;
// public bool $can_drink;
public function __set(string $name, $value): void
{
if (in_array($name, self::$booleanColumns, true)) {
$this->{$name} = match ($value) {
't' => true,
default => false,
};
return;
}
$this->{$name} = $value;
}
}
bool 型のカラムだけあえてプロパティに定義せず、マジックメソッドが実行されるようにします。
$booleanColumns
にあらかじめ bool 型のカラムはこれだよと書いておき、指定したカラムを扱う際は "t"
を true
に、それ以外の値を false
に置き換えます。
コード上に $can_drink
の定義がなくなるため、IDE 上で補完したい場合はクラスのドキュメントに @property
属性で書きます。
(bool 型を返す can_drink()
関数を作ってもいいかもしれません)
match は PHP 8 系から使えるようになった書き方で、switch 句の型が厳密になったもの、というイメージです。
スクリプトを以下のように書き換えて実行します。
<?php
require_once __DIR__ . '/Participant2.php';
$db_conn = pg_connect('host=db port=5432 dbname=root user=root password=root');
$qu = pg_query($db_conn, 'SELECT * FROM participant');
while ($data = pg_fetch_object($qu, null, Participant2::class)) {
$name = $data->name;
$canDrink = $data->can_drink;
$message = $canDrink
? "$name can drink!"
: "$name cannot drink...";
echo $message, PHP_EOL;
}
結果
John can drink!
Jane cannot drink...
Jane さんの「飲めない」が正しく出力されました
補足
__set
関数はコンストラクタよりも先に実行されている
先ほどのデータ取得にこちらのクラスを使ってみると、
class Participant3
{
// public int $id;
// public string $name;
// public bool $can_drink;
public function __construct()
{
var_dump('constructor executed');
}
public function __set(string $name, $value): void
{
$this->{$name} = $value;
var_dump('__set executed');
}
}
出力結果は以下のようになります。
string(14) "__set executed"
string(14) "__set executed"
string(14) "__set executed"
string(20) "constructor executed"
string(14) "__set executed"
string(14) "__set executed"
string(14) "__set executed"
string(20) "constructor executed"
各行ごとに __set
が先に実行され、続いてコンストラクタが実行されていることがわかります。
今回検証した内容は こちらのリポジトリ にもありますので、合わせてご覧ください。