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.

  1. # config/providers/email_client.rb
  2. Hanami.app.register_provider(:email_client) do
  3. prepare do
  4. require "acme_email/client"
  5. end
  6. start do
  7. client = AcmeEmail::Client.new(
  8. api_key: target["settings"].acme_api_key,
  9. default_from: "no-reply@bookshelf.example.com"
  10. )
  11. register "email_client", client
  12. end
  13. 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"]:

  1. # app/operations/send_welcome_email.rb
  2. module Bookshelf
  3. module Operations
  4. class SendWelcomeEmail
  5. include Deps[
  6. "email_client",
  7. "renderers.welcome_email"
  8. ]
  9. def call(name:, email_address:)
  10. email_client.deliver(
  11. to: email_address,
  12. subject: "Welcome!",
  13. text_body: welcome_email.render_text(name: name),
  14. html_body: welcome_email.render_html(name: name)
  15. )
  16. end
  17. end
  18. end
  19. 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.

  1. # config/providers/my_provider.rb
  2. Hanami.app.register_provider(:my_provider) do
  3. start do
  4. register "my_thing", MyThing.new
  5. register "another.thing", AnotherThing.new
  6. register "thing", Thing
  7. end
  8. 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.
  1. # config/providers/database.rb
  2. Hanami.app.register_provider(:database) do
  3. prepare do
  4. require "acme/db"
  5. register "database", Acme::DB.configure(target["settings"].database_url)
  6. end
  7. start do
  8. target["database"].establish_connection
  9. end
  10. stop do
  11. target["database"].close_connection
  12. end
  13. 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):

  1. Hanami.app.register_provider(:uploads_bucket) do
  2. prepare do
  3. require "aws-sdk-s3"
  4. end
  5. start do
  6. target.start(:metrics)
  7. uploads_bucket_name = target["settings"].uploads_bucket_name
  8. credentials = Aws::Credentials.new(
  9. target["settings"].aws_access_key_id,
  10. target["settings"].aws_secret_access_key,
  11. )
  12. uploads_bucket = Aws::S3::Resource.new(credentials: credentials).bucket(uploads_bucket_name)
  13. register "uploads_bucket", uploads_bucket
  14. end
  15. end