-
-
Notifications
You must be signed in to change notification settings - Fork 0
Recipe Service Factories
Goal: build services that the container cannot autowire on its own — anything
that needs a DSN, an API key, a file path, or another value the reflector cannot
infer — using lazy Closure factories.
Autowiring works when every constructor argument is itself a resolvable class. As
soon as a constructor needs a scalar (or a union type, or any value), the
container has nothing to infer and would throw
DependencyHasNoDefaultValueException.
A factory closure supplies those values explicitly.
use InitPHP\Container\Container;
use Psr\Container\ContainerInterface;
class Database
{
public function __construct(public string $dsn, public string $user) {}
}
$container = new Container();
$container->set('db', function (ContainerInterface $c) {
return new Database('mysql:host=localhost;dbname=app', 'root');
});
$db = $container->get('db'); // closure runs here
$container->get('db') === $db; // true — cached, the closure runs onceSee Binding & Factories → Factories.
A factory receives the container, so it can read other entries. Keep raw configuration in the container and let factories assemble services from it:
$container->set('config', [
'db' => ['dsn' => 'pgsql:host=localhost;dbname=app', 'user' => 'app'],
'api' => ['key' => 'sk_live_123'],
]);
$container->set('db', function (ContainerInterface $c) {
$cfg = $c->get('config')['db'];
return new Database($cfg['dsn'], $cfg['user']);
});
$container->set(ApiClient::class, function (ContainerInterface $c) {
return new ApiClient($c->get('config')['api']['key']);
});Factories can resolve dependencies that are themselves autowired or factory-built, mixing both styles freely:
class Logger {} // autowirable
class AuditLog
{
public function __construct(public Logger $logger, public string $channel) {}
}
$container->set('audit', function (ContainerInterface $c) {
// Logger is autowired; the scalar channel is supplied here.
return new AuditLog($c->get(Logger::class), 'security');
});When the implementation needs configuration, bind the interface to a closure so consumers still depend on the abstraction:
interface QueueInterface {}
final class RedisQueue implements QueueInterface
{
public function __construct(public string $host, public int $port) {}
}
$container->set(QueueInterface::class, function (ContainerInterface $c) {
[$host, $port] = $c->get('config')['queue'] ?? ['127.0.0.1', 6379];
return new RedisQueue($host, $port);
});
$queue = $container->get(QueueInterface::class); // RedisQueue, configuredThis is the most common real-world binding: an interface key, a closure value. See Interface Binding.
The closure's return value is cached, so the factory executes a single time and
every subsequent get() returns the same instance:
$calls = 0;
$container->set('heavy', function () use (&$calls) {
$calls++;
return new HeavyService();
});
$container->get('heavy');
$container->get('heavy');
$calls; // 1Do not route per-call objects through get(). Register a factory object and
call its method each time you need a fresh instance:
final class ReportFactory
{
public function __construct(private Database $db) {}
public function create(string $period): Report
{
return new Report($this->db, $period); // fresh every call
}
}
$container->set(ReportFactory::class); // the factory is shared
$report = $container->get(ReportFactory::class)->create('2026-Q1'); // report is newSee Resolution & Caching → Need a fresh instance every time?.
-
Heavy work at registration time.
set()only stores the closure; nothing runs untilget(). Do not call the closure yourself in the bootstrap — let the firstget()trigger it so the cost is paid lazily. - Expecting fresh objects from a plain factory. A closure entry is cached. Use a factory object (above) for per-call instances.
- Catching the wrong exception. A throwing factory propagates its own exception; if you want a uniform type, wrap and re-throw — see Exceptions → Re-throwing from your own boundary.
- Binding & Factories — the registration reference.
- Interface Binding — depend on abstractions.
- Application Bootstrap — assemble it all at startup.
initphp/container · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core Usage
Reference
Practical Guides
Migration & Help