サービスと DI#
Relayer は Symfony の DI コンテナを使い、オートワイヤ + public が既定です。登録方法は 2 通りあり、併用できます。
config/services.yaml#
services:
_defaults:
autowire: true
autoconfigure: true
public: true
App\Service\PdoUserRepository: ~
App\Service\UserRepository:
alias: App\Service\PdoUserRepository.yaml / .yml / .php 形式に対応。引数なしで登録した定義には自動でオートワイヤと public が付与されます(PSR-11 の get($id) で取得可能)。
環境別の設定#
services.yaml は環境ごとに切り替えられます。<env> は APP_ENV=dev (または development)なら dev、それ以外はすべて prod に解決されます。
when@<env> ブロック — 1 つの services.yaml 内で env 分岐する Symfony 標準の書き方です。
# config/services.yaml
services:
App\Service\Mailer: ~
when@dev:
services:
App\Service\LoggingMailer: ~ # dev でだけ Mailer を差し替えservices.{env}.{yaml,yml,php} サイドファイル — 基本ファイルの後に読み込まれる環境専用ファイル。環境別の定義が増えたら切り出せます。
# config/services.dev.yaml
services:
App\Service\LoggingMailer: ~読み込み順(低 → 高、後勝ち):
| 順 | 対象 |
|---|---|
| 1 | フレームワーク既定値 |
| 2 | services.{yaml,yml,php}(when@<env> ブロック含む) |
| 3 | services.{env}.{yaml,yml,php} |
| 4 | AppConfigurator(後述) |
.yaml と .yml は同じファイルの別綴りで、両方あれば先に見つかった方だけを読みます(重ねがけしません)。services.php / services.{env}.php も同様です。
AppConfigurator#
<?php
namespace App;
use Polidog\Relayer\AppConfigurator as BaseConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class AppConfigurator extends BaseConfigurator
{
public function configure(ContainerBuilder $container): void
{
$container->register(PdoUserRepository::class);
$container->setAlias(UserRepository::class, PdoUserRepository::class)
->setPublic(true);
}
}Relayer::boot(__DIR__ . '/..', new App\AppConfigurator(__DIR__ . '/..'))->run(); のように渡します。services.yaml の後に走るので上書きも可能。プロジェクトルートは $this->projectRoot、%app.project_root% パラメータも使えます。
ファクトリ(実行時に実装を選ぶ)#
このサイトの DocStore は、環境変数で接続先を切り替えるためファクトリで生成しています。
services:
App\Docs\DocStore:
factory: ['App\Docs\DocStoreFactory', 'create']
arguments:
- '%app.project_root%'
autowire: falseDocStoreFactory::create() は TURSO_DATABASE_URL があれば Turso、なければローカル SQLite を返します。ページや route.php は引数に DocStore を型宣言するだけで注入されます。
環境変数を引数に渡す(%env()%)#
services.yaml では Symfony 標準の %env(VAR)% プレースホルダで環境変数を参照できます。
services:
App\Logger\NewRelic:
arguments:
$apiKey: '%env(NEW_RELIC_API_KEY)%'
$appId: '%env(NEW_RELIC_APP_ID)%'型変換 / フォールバックには Symfony 標準のプリフィックスが使えます。 %env(int:DB_PORT)%・%env(bool:FEATURE_FLAG)%・%env(default::API_TOKEN)% など。AppConfigurator から渡したい場合も同じプレースホルダを setParameter に入れます。
final class AppConfigurator extends Base
{
public function configure(ContainerBuilder $container): void
{
$container->setParameter('app.api_token', '%env(API_TOKEN)%');
}
}%env(VAR)% は dev / 本番のどちらでも解決します。dev は live ContainerBuilder がコンパイル時に現在の env を読み、container:compile (CLI 参照)でダンプ済みクラスを使う本番はその中の getEnv('VAR') 呼び出しがリクエストごとに現在の env を見にいきます。
container:compile とランタイム secret#
container:compile はデプロイ時に AppConfigurator::configure() を 1 回だけ走らせ、その時点のコンテナをダンプ済みクラスに焼き込みます。本番ブート時はダンプを require するだけで AppConfigurator は再実行されません。
%env(VAR)% プレースホルダはダンプ時に getEnv('VAR') 呼び出しとして書き出されるのでリクエスト時の env をそのまま反映しますが、AppConfigurator が $_ENV['API_TOKEN'] のように env を直読みして文字列として setParameter() に渡した値は別の扱いになります。文字列のままダンプに焼き込まれ、本番では env を再評価しません。
したがって、Fly secrets / Cloud Run env / サイドカー injector のように ランタイムにしか secret が来ない構成では、ビルド時に env が空ならその空文字がダンプにベイクされ、本番でも空文字が返ります — "なぜか空文字" の静かな障害になります。回避策は env の直読みをやめ、%env(VAR)% プレースホルダを渡すこと。
// アンチパターン — ビルド時に空ならベイクされて空のまま
$container->setParameter('app.api_token', $_ENV['API_TOKEN'] ?? '');
// 推奨 — ダンプは getEnv('API_TOKEN') を発行し、リクエスト時に解決
$container->setParameter('app.api_token', '%env(API_TOKEN)%');routes:compile はこの問題と無関係です(src/Pages/ を走査するだけで env を読まない)。container:compile だけが env を巻き込みます。どうしてもランタイム secret を扱えない事情があるなら、container:compile を外して routes:compile のみ採用し、リクエストごとの compile() コストを受け入れるのも妥当な選択です(dev は元から live ビルドなのでこの問題に当たりません)。
フレームワーク自身が読む
DATABASE_*/APP_LOCALEなどはこの仕組みとは別で、ContainerFactoryが PHP レベルで直接$_ENVを読んで素のパラメータ/コンストラクタ引数に変換します。container:compileを使う場合は同じくビルド時の値がベイクされるので、ランタイムにしか来ない値(Fly secrets のDATABASE_PASSWORD等)を渡す構成ではcontainer:compileを外す運用が必要です(環境変数と .env カスケード 参照)。
オートワイヤ#
ページ/API ハンドラの引数は 型でオートワイヤされます。 PageContext・Request・Identity、そして登録済みサービス。
PSX コンポーネントからのサービス取得#
src/Components/ の PSX コンポーネントはプレーンなクロージャなので、コンストラクタ/引数インジェクションが使えません。コンポーネントの中では Relayer::container() で DI コンテナからサービスを直接取得します。
<?php
// src/Components/TodoList.psx
namespace App\Components;
use Polidog\Relayer\Db\Database;
use Polidog\Relayer\Relayer;
use function Polidog\UsePhp\Runtime\fc;
return fc(function (array $props) {
$db = Relayer::container()->get(Database::class);
$todos = $db->fetchAll('SELECT id, title, done FROM todos ORDER BY id');
return <ul>{array_map(fn ($t) => <li>{$t['title']}</li>, $todos)}</ul>;
});Relayer::container() は boot() が構築した PSR-11 コンテナを返します(boot() より前に呼ぶと LogicException)。これはコンポーネント専用の逃げ道です。ページ・API ハンドラ・ルートクラスなど通常のサービスは、上のオートワイヤ/AppConfigurator でのコンストラクタインジェクションを使うのが正道——コンポーネントに値を渡したいときも、まずは props を検討してください。
変更履歴 (5)
- v0.24.0: PSX コンポーネントからの Relayer::container() サービス取得を追記
- v0.23.0: env別 convention-config(when@<env> / services.{env}.*)の『環境別の設定』を追記
- v0.20.0 の %env() dev 解決と container:compile ベイク警告を反映
- Document %env() placeholders in services.yaml + container:compile caveat (dev/live ContainerBuilder leaks env_<hash>_VAR_<hash>; production dumped container resolves via getEnv()). Add AppConfigurator
- 新規作成