V2.0: Providers


Providers are a way to register components with your containers, outside of the automatic registration mechanism detailed in containers and components.

Providers are useful when:

  • you want to register a specific instance of an object as a component, and have that very same instance be available as a dependency
  • you need to set up a dependency that requires non-trivial configuration (often a third party library, or some library-like code in your lib directory)
  • you want to take advantage of provider lifecycle methods (prepare, start and stop)

Providers should be placed in the config/providers directory. Here’s an example provider for that registers a client for an imagined third-party Acme Email delivery service.

# config/providers/email_client.rb

Hanami.app.register_provider(:email_client) do
  prepare do
    require "acme_email/client"
  end

  start do
    client = AcmeEmail::Client.new(
      api_key: target["settings"].acme_api_key,
      default_from: "no-reply@bookshelf.example.com"
    )

    register "email_client", client
  end
end

The above provider creates an instance of Acme’s email client, using an API key from our application’s settings, then registers the client in the app container with the key "email_client".

The registered dependency can now become a dependency for other components, via include Deps["email_client"]:

# app/operations/send_welcome_email.rb

module Bookshelf
  module Operations
    class SendWelcomeEmail
      include Deps[
        "email_client",
        "renderers.welcome_email"
      ]

      def call(name:, email_address:)
        email_client.deliver(
          to: email_address,
          subject: "Welcome!",
          text_body: welcome_email.render_text(name: name),
          html_body: welcome_email.render_html(name: name)
        )
      end
    end
  end
end

Every provider has a name (Hanami.app.register_provider(:my_provider_name)) and will usually register one or more related components with the relevant container.

Registered components can be any kind of object - they can be classes too.

To register an item with the container, providers call register, which takes two arguments: the key to be used, and the item to register under it.

# config/providers/my_provider.rb

Hanami.app.register_provider(:my_provider) do
  start do
    register "my_thing", MyThing.new
    register "another.thing", AnotherThing.new
    register "thing", Thing
  end
end

Provider lifecycle

Providers offer a three-stage lifecycle: prepare, start, and stop. Each has a distinct purpose:

  • prepare - basic setup code, here you can require third-party code, or code from your lib directory, and perform basic configuration
  • start - code that needs to run for a component to be usable at runtime
  • stop - code that needs to run to stop a component, perhaps to close a database connection, or purge some artifacts.
# config/providers/database.rb

Hanami.app.register_provider(:database) do
  prepare do
    require "acme/db"

    register "database", Acme::DB.configure(target["settings"].database_url)
  end

  start do
    target["database"].establish_connection
  end

  stop do
    target["database"].close_connection
  end
end

A provider’s prepare and start steps will run as necessary when a component that the provider registers is used by another component at runtime.

Hanami.boot calls start on each of your application’s providers, meaning each of your providers is started automatically when your application boots. Similarly, Hanami.shutdown can be invoked to call stop on each provider.

You can also trigger lifecycle transitions directly by using Hanami.app.prepare(:provider_name), Hanami.app.start(:provider_name) and Hanami.app.stop(:provider_name).

Accessing the container via target

Within a provider, the target method (also available as target_container) can be used to access the app container.

This is useful if your provider needs to use other components within the container, for example the application’s settings or logger (via target["settings] and target["logger"]). It can also be used when a provider wants to ensure another provider has started before starting itself, via target.start(:provider_name):

Hanami.app.register_provider(:uploads_bucket) do
  prepare do
    require "aws-sdk-s3"
  end

  start do
    target.start(:metrics)

    uploads_bucket_name = target["settings"].uploads_bucket_name

    credentials = Aws::Credentials.new(
      target["settings"].aws_access_key_id,
      target["settings"].aws_secret_access_key,
    )

    uploads_bucket = Aws::S3::Resource.new(credentials: credentials).bucket(uploads_bucket_name)

    register "uploads_bucket", uploads_bucket
  end
end