Service providers

A service provider is responsible for bootstrapping and configuring the services of an Expanse application. A service provider should be scoped to services that are related to a specific feature or a group of related services. For instance, Expanse has a DatabaseServiceProvider that is responsible for managing database connections and sessions.

Implementing a service provider

Service providers must inherit from the ServiceProvider class and implement the register() method and, optionally, a boot() method. There are two import points to remember when implementing a service provider:

  • The register() method should only be responsible for registering services to the service container. It'd also important to remember that you should not try to retrieve other services from the container here since you have no guarantee that they have been already bound to the container.
  • The boot() method is called after all services have been registered.

Service providers are located in the app/providers directory.

The register method

The register() method should only be responsible for registering services to the service container

Let's see an example with a simplified version of the DatabaseServiceProvider:

from collections.abc import Generator

from expanse.contracts.database.connection import Connection
from expanse.database.database_manager import DatabaseManager
from expanse.support.service_provider import ServiceProvider


class DatabaseServiceProvider(ServiceProvider):
    def register(self) -> None:
        self._container.singleton(DatabaseManager)
        self._container.scoped(Connection, self._create_connection)

    def _create_connection(
        self, db: DatabaseManager, name: str | None = None
    ) -> Generator[Connection]:
        connection = db.connection(name)

        yield connection

        connection.close()

If you are not familiar with service container, you can read more about it in its documentation, but, in short, the service provider is registering the DatabaseManager as a singleton — meaning only one instance will be created during the application lifecycle — and the Connection as a scoped service — meaning a new instance will be created for each request.

The boot method

The boot() method is called after all services have been registered. This is the place where you can interact with any services you need to provide additional functionality to your application. Let's say you want to add another function to your views:

from expanse.support.service_provider import ServiceProvider
from expanse.view.view_factory import ViewFactory


class MyServiceProvider(ServiceProvider):

    def boot(self, view: ViewFactory) -> None:
        view.register_global(my_new_function=self._my_new_function)

    def _my_new_function(self) -> str:
        return "Hello"

As you can see, you can type-hint any services in the boot() method and they will be automatically resolved and injected.

Registering a service provider

To register a service provider, you need to add it to the providers list in the config/app.py file:

providers = [
    # Other providers...
    "app.providers.my_service_provider.MyServiceProvider",
]