Services & DI#
Relayer uses Symfony's DI container, with autowire + public as the default. There are two registration methods, and they can be combined.
config/services.yaml#
services:
_defaults:
autowire: true
autoconfigure: true
public: true
App\Service\PdoUserRepository: ~
App\Service\UserRepository:
alias: App\Service\PdoUserRepositoryThe .yaml / .yml / .php formats are supported. A definition registered with no arguments automatically gets autowire and public (retrievable via PSR-11's get($id)).
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);
}
}Pass it like Relayer::boot(__DIR__ . '/..', new App\AppConfigurator(__DIR__ . '/..'))->run();. It runs after services.yaml, so it can override as well. The project root is available as $this->projectRoot, and the %app.project_root% parameter can also be used.
Factories (choosing the implementation at runtime)#
This site's DocStore is created with a factory in order to switch the connection target by environment variable.
services:
App\Docs\DocStore:
factory: ['App\Docs\DocStoreFactory', 'create']
arguments:
- '%app.project_root%'
autowire: falseDocStoreFactory::create() returns Turso if TURSO_DATABASE_URL is present, otherwise local SQLite. Pages and route.php only need to type-declare DocStore as an argument to have it injected.
Passing environment variables as service arguments (%env()%)#
In services.yaml you reference environment variables via the standard Symfony %env(VAR)% placeholder.
services:
App\Logger\NewRelic:
arguments:
$apiKey: '%env(NEW_RELIC_API_KEY)%'
$appId: '%env(NEW_RELIC_APP_ID)%'Symfony's standard prefixes give typed or fallback resolution: %env(int:DB_PORT)%, %env(bool:FEATURE_FLAG)%, %env(default::API_TOKEN)%, and so on. To pass an env value from AppConfigurator, hand the same placeholder to setParameter:
final class AppConfigurator extends Base
{
public function configure(ContainerBuilder $container): void
{
$container->setParameter('app.api_token', '%env(API_TOKEN)%');
}
}%env(VAR)% resolves in both dev and production. In dev the live ContainerBuilder reads the current env at compile time; in production under container:compile (see CLI) the dumped class issues a getEnv('VAR') call that reads the current env on every request.
container:compile and runtime secrets#
container:compile runs AppConfigurator::configure() exactly once, at deploy time, and bakes the resulting container into a dumped class. Production boot just requires the dump; AppConfigurator never runs again.
A %env(VAR)% placeholder is dumped as a getEnv('VAR') call, so it keeps tracking the live env on each request. But if AppConfigurator reads env directly and passes a plain string to setParameter() — e.g. $_ENV['API_TOKEN'] — that string is frozen into the dump and prod never re-evaluates the env.
So when a secret only becomes available at runtime — Fly secrets, Cloud Run env, sidecar injectors — and the env is empty at build time, the empty string is baked in and prod silently keeps returning empty. The fix is to stop reading env directly and pass the %env(VAR)% placeholder instead:
// Anti-pattern — empty at build time bakes empty forever
$container->setParameter('app.api_token', $_ENV['API_TOKEN'] ?? '');
// Recommended — dumped as getEnv('API_TOKEN'), resolved per request
$container->setParameter('app.api_token', '%env(API_TOKEN)%');routes:compile is unaffected (it only walks src/Pages/ and never touches env). Only container:compile interacts with env values. If runtime secrets cannot be expressed as %env()% for any reason, dropping container:compile and keeping just routes:compile — paying the per-request compile() cost — is a reasonable alternative (dev already builds live and never hits this problem).
Framework-owned variables like
DATABASE_*/APP_LOCALEfollow a different path:ContainerFactoryreads$_ENVdirectly in PHP and turns them into plain parameters / constructor arguments. Undercontainer:compilethey are baked at build time the same way, so if any of them is only injected at runtime (e.g. a Fly secret forDATABASE_PASSWORD),container:compilehas to be dropped (see Environment variables and .env cascade).
Autowiring#
Page / API handler arguments are autowired by type: PageContext, Request, Identity, and registered services.
Change history (3)
- v0.20.0 — %env() resolves in dev and prod; rewrite for container:compile bake warning
- 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
- Created