Summary
Currently Di::get() auto-registers concrete instantiable classes on first access, effectively making the DI container a service locator where any class can be silently resolved as a singleton without prior registration. This blurs the contract between get() (singleton) and create() (transient).
Proposal
Remove auto-registration from Di::get() and keep it only in Di::create():
- Di::get() - strict singleton: throws if the class is not pre-registered. Requires explicit Di::register() or Di::set() before use.
- Di::create() - flexible transient: auto-resolves any concrete instantiable class. No registration required.
This aligns with how Di::resolveParameter() already works in the autowiring engine:
- Registered types (interfaces, explicit bindings) -> Di::get() -> singleton
- Unregistered concrete types -> Di::create() -> new transient instance
Migration strategy - lazy registration:
Each helper function and factory that calls Di::get() registers the class on first access, following the pattern already used by ServiceFactory::get() and cookie().
Affected areas (~40 call sites in src/)
- All factory self::class calls: LoggerFactory, MailerFactory, CacheFactory, AuthFactory, SessionFactory, RendererFactory, CaptchaFactory, ArchiveFactory, CryptorFactory, FileSystemFactory, LangFactory, ViewFactory
- Core service helpers: config(), server(), hook(), asset(), csrf(), view()
- Boot stages: SetupErrorHandlerStage, LoadHelpersStage
- Internal callers: ModuleManager, ModelFactory, MigrationManager, RelationalTrait, Config, UploadedFile
- Test files (~30 call sites)
Benefits
- Explicit singleton contract - singletons are intentional, not accidental
- Clear semantics - get() = cached singleton (must register), create() = transient (auto-resolves)
- Autowiring unaffected - resolveParameter() already distinguishes registered vs instantiable types
- Incremental migration - each helper/factory can be updated independently
Context
This emerged from the #373 App Bootstrapping and DI Ownership refactor. The auto-registration was introduced as a convenience during singleton-to-DI migration (#382), but it weakens the explicitness of the container contract.
Summary
Currently Di::get() auto-registers concrete instantiable classes on first access, effectively making the DI container a service locator where any class can be silently resolved as a singleton without prior registration. This blurs the contract between get() (singleton) and create() (transient).
Proposal
Remove auto-registration from Di::get() and keep it only in Di::create():
This aligns with how Di::resolveParameter() already works in the autowiring engine:
Migration strategy - lazy registration:
Each helper function and factory that calls Di::get() registers the class on first access, following the pattern already used by ServiceFactory::get() and cookie().
Affected areas (~40 call sites in src/)
Benefits
Context
This emerged from the #373 App Bootstrapping and DI Ownership refactor. The auto-registration was introduced as a convenience during singleton-to-DI migration (#382), but it weakens the explicitness of the container contract.