RRelayer
ホーム/機能

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 など実行時注入の値が毎リクエスト正しく解決されます(サービスと DICLI コマンド)。

ページ側はコンストラクタで型を取るだけです。

<?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 / findManyinsert / update / delete / countupsert / 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));
    }
});

関連#

リクエストスコープの読み取りキャッシュはこちらの機能で、Tehilim とは別系統。

/_profiler の見方。

リレーション・マイグレーションの詳細。

最終更新: 2026-05-23
変更履歴 (3)
  • v0.23.0: プロファイラ設定を when@dev による dev限定 DI 注入に変更
  • DI: factory takes injected args, no $_ENV access (mirror DocStoreFactory)
  • Add Tehilim integration page (profiler hook)