Tehilim 連携#
Tehilim は Relayer と同じ作者による、 PHP 向けの スキーマファースト DB ツールキットです。Prisma 風の体験を持ちつつ、ORM のクラスマッピングを採らず「データは連想配列、型は PHPDoc の array shape」という方針で型安全性を PHPStan に委ねます。
Relayer 標準の データベース レイヤー(DATABASE_DSN の PDO ラッパー)とは独立した別物で、置き換えではなく併用できます。連携の要は プロファイラフック——Tehilim の計測フックは Relayer の プロファイラ Profiler::measure() とシグネチャが一致するように設計されており、追加のアダプタなしでそのまま挿せます。
Tehilim は v0.1 段階です。プロトタイピングや小〜中規模アプリで実用的ですが、API は変わり得ます。
インストールと生成#
composer require polidog/tehilim
vendor/bin/tehilim init # スタータースキーマを生成
vendor/bin/tehilim generate # 型付きクライアントを生成
vendor/bin/tehilim migrate dev --name init # マイグレーション作成&適用schema.tehilim にデータモデルを宣言し、generate で型付きクライアント(既定で App\Generated\TehilimClient)を吐き出します。返り値は連想配列で、その正確な形状を PHPStan が認識します。
Relayer アプリへの組み込み(DI)#
生成された TehilimClient を DI コンテナに束ねれば、ページ/コンポーネントが型で受け取れます。このリポジトリの DocStoreFactory と同じく、ファクトリは 環境変数を一切読まず(純粋)、接続情報は services.yaml の %env()% プレースホルダで解決して引数として注入します。プロファイラは dev だけの関心事なので、Profiler は env 別設定(when@dev)で dev のときだけ 注入します——本番のサービス定義は Profiler を一切参照しません。
<?php
// src/Db/TehilimClientFactory.php
namespace App\Db;
use App\Generated\TehilimClient;
use Polidog\Relayer\Profiler\Profiler;
final class TehilimClientFactory
{
// 依存はすべて引数で受け取る。$_ENV / getenv は触らない。
// $profiler は dev だけ注入される(本番は null)。
public static function create(
string $dsn,
string $user,
string $password,
?Profiler $profiler = null,
): TehilimClient {
$pdo = new \PDO(
$dsn,
'' !== $user ? $user : null,
'' !== $password ? $password : null,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
);
$client = TehilimClient::fromPdo($pdo);
// measure() を第一級コーラブルとして渡す。Profiler が来るのは
// dev だけなので、計測コードは本番のクライアントに載らない。
return null !== $profiler
? $client->withProfiler($profiler->measure(...))
: $client;
}
}接続情報は services.yaml で %env()% から解決し、未設定時は空文字にフォールバックさせます(throw させない)。本番のサービス定義は接続情報だけ——Profiler は when@dev ブロックで dev のときだけ 4 番目の引数に足します(env 別設定の仕組みは サービスと DI の「環境別の設定」を参照)。
# config/services.yaml
parameters:
# 未設定なら空文字(dev/CI で DATABASE_* 無しでも throw しない)
env(DATABASE_DSN): ''
env(DATABASE_USER): ''
env(DATABASE_PASSWORD): ''
app.database_dsn: '%env(string:DATABASE_DSN)%'
app.database_user: '%env(string:DATABASE_USER)%'
app.database_password: '%env(string:DATABASE_PASSWORD)%'
services:
# 本番のクライアント — Profiler は参照しない
App\Generated\TehilimClient:
factory: ['App\Db\TehilimClientFactory', 'create']
arguments:
- '%app.database_dsn%'
- '%app.database_user%'
- '%app.database_password%'
autowire: false
# dev だけ Profiler を 4 番目の引数として足す
when@dev:
services:
App\Generated\TehilimClient:
factory: ['App\Db\TehilimClientFactory', 'create']
arguments:
- '%app.database_dsn%'
- '%app.database_user%'
- '%app.database_password%'
- '@Polidog\Relayer\Profiler\Profiler'
autowire: false右辺は configure 時に焼き込む文字列ではなく %env()% プレースホルダなので、 vendor/bin/relayer container:compile でコンテナをダンプしても安全です—— ダンプはこれをリクエストごとの getEnv() 呼び出しに展開し、Fly secret など実行時注入の値が毎リクエスト正しく解決されます(サービスと DI・CLI コマンド)。
ページ側はコンストラクタで型を取るだけです。
<?php
use App\Generated\TehilimClient;
use Polidog\Relayer\Router\Component\PageComponent;
use Polidog\UsePhp\Runtime\Element;
final class UsersPage extends PageComponent
{
public function __construct(private readonly TehilimClient $db) {}
public function render(): Element
{
$users = $this->db->user->findMany([
'include' => ['posts' => ['where' => ['published' => true]]],
'orderBy' => ['id' => 'asc'],
]);
return <ul>{array_map(fn ($u) => <li>{$u['name']}</li>, $users)}</ul>;
}
}プロファイラ連携#
Tehilim の計測フックは function (string $collector, string $label, callable $fn): mixed という形で、 Relayer の Profiler::measure(string $collector, string $label, callable $fn): mixed と完全に一致します。だから専用デコレータを書く必要はなく、measure() を第一級コーラブルとして渡すだけ——それが上のファクトリの withProfiler() の一行です。
上で見たとおり、Profiler を注入するのは when@dev ブロックだけなので、 dev でだけフックが挿さります。本番のサービス定義は Profiler を参照せず、計測コードはそもそも production のクライアントに載りません。
これで dev では Tehilim の各操作が /_profiler タイムラインにスパンとして並びます。計測対象は findUnique / findFirst / findMany、 insert / update / delete / count、upsert / insertMany / updateMany / deleteMany。キャッシュヒット時はスキップされます。
measure() は時間のみを記録し、引数・戻り値は残しません(プロファイラ と同じ挙動)。
dev で挿したフックを実行時に外したいときは null を渡します。
$db->withProfiler(null); // 計測フックをクリアRelayer 以外でのフック(任意)#
withProfiler() は任意のコーラブルを受けるので、Relayer のプロファイラを使わずに自前のログへ流すこともできます。
$db->withProfiler(function (string $collector, string $label, callable $fn) {
$start = hrtime(true);
try {
return $fn();
} finally {
error_log(sprintf('[%s/%s] %.2fms', $collector, $label, (hrtime(true) - $start) / 1e6));
}
});関連#
- データベース — Relayer 標準の
Database(DATABASE_DSN)。
リクエストスコープの読み取りキャッシュはこちらの機能で、Tehilim とは別系統。
- プロファイラ —
measure()とトレーサブルデコレータ、
/_profiler の見方。
- Tehilim リポジトリ — スキーマ記法・
リレーション・マイグレーションの詳細。
変更履歴 (3)
- v0.23.0: プロファイラ設定を when@dev による dev限定 DI 注入に変更
- DI: factory takes injected args, no $_ENV access (mirror DocStoreFactory)
- Add Tehilim integration page (profiler hook)