Service Container

The container resolves dependencies, holds the application's singletons, and autowires constructors from type annotations.

Binding

# `app` is the Application built in bootstrap/app.py
app.bind("mailer", lambda c: Mailer(c.make("config")))   # new instance each time
app.singleton("mailer", lambda c: Mailer())              # built once, reused
app.instance("mailer", existing_mailer)                  # register an object
app.alias("mailer", "mail")                              # second name

Resolve with make:

mailer = app.make("mailer")

An unbound string raises KeyError — the container never silently returns None.

Autowiring

A class with annotated constructor parameters is built without registration:

class Engine: ...
class Wheels: ...

class Car:
    def __init__(self, engine: Engine, wheels: Wheels):
        self.engine = engine
        self.wheels = wheels

car = app.make(Car)          # Engine and Wheels resolved automatically

Override any parameter explicitly:

app.make(Car, {"engine": my_engine})

If a parameter cannot be resolved and has a default, the default is used. Otherwise resolution raises rather than passing None.

The global instance

Facades, models and helpers reach the container through Container.getInstance(). The booted application claims that slot.

from craft.container.application import Application, Container

app = Application(base_path)             # claims the global if no app holds it
Application(base_path, bind_as_global=False)   # never claims it
Application(base_path, bind_as_global=True)    # always claims it

The default (None) claims the global unless another Application already holds it. A bare fallback Container is always displaced.

Constructing a container does not claim the global. It used to, which meant building a second one anywhere — a test fixture, a worker, a tenant scope — silently repointed every getInstance() call in the process.

Swap it temporarily with guaranteed restoration:

with Container.scoped_instance(tenant_app):
    ...   # facades and models resolve from tenant_app

Not to be confused with scoped(), which registers a request-scoped binding.

What the framework binds

| Key | Object | |---|---| | app | The Application itself | | config | ConfigRepository | | db | DatabaseManager | | schema | SchemaBuilder | | migrator | Migrator | | router | Router | | view | Forge | | auth | AuthManager | | gate | GateManager | | hash | Hash | | events | EventDispatcher | | queue | QueueManager | | cache | CacheManager | | log | logging.Logger | | exception_handler | ExceptionHandler | | module, plugin, setting, schedule | Framework subsystems | | pqc, captcha | Security utilities |

Service providers

Providers register bindings and then boot them. register() runs for every provider first, so boot() can rely on anything being bound.

# app/Providers/AppServiceProvider.py
from craft.providers import ServiceProvider


class AppServiceProvider(ServiceProvider):
    def register(self):
        self.app.singleton("mailer", lambda c: Mailer())

    def boot(self):
        self.app.make("view").share("app_name", self.app.make("config").get("app.APP_NAME"))

Register it in bootstrap/app.py:

app.register_provider(AppServiceProvider)

A provider registered after app.boot() boots immediately.

Facades

Facades are a static front for a container binding:

from craft.facades import DB, Auth, Cache, Config, Route, View

DB.statement("SELECT 1")
Auth.check()
Cache.get("key")

The facades that ship with the framework: Route, DB, Config, Auth, Event, Queue, Log, Cache, Hash, Migrator, Gate, View, Schema, Module, Plugin, Setting, PQC, Captcha, and Schedule. There is no App facade — reach the application through the app built in bootstrap/app.py, or Container.getInstance().

Each resolves its accessor from the container on every call, so swapping the binding in a test swaps what the facade talks to.

Facades never fabricate dunder or private attributes — DummyFacade.__wrapped__ raises AttributeError instead of resolving the container. That matters more than it sounds: introspection (pytest collection, inspect, copy) probes those names, and answering them resolved the container before the app booted.

Write your own by naming the binding:

from craft.facades.base import Facade


class Mail(Facade):
    @classmethod
    def get_facade_accessor(cls) -> str:
        return "mailer"