Skip to content

Enforce strict Di::get() singleton contract with lazy registration #452

Description

@armanist

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.

Metadata

Metadata

Assignees

Labels

Type

No fields configured for Task.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions