Skip to content

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

@armanist

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

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions