This article assumes you have familiarity with Hanami and want to dive deeper into how dependencies work, are organized, and managed via containers. At a high level, containers allow you to define your dependencies once so you can quickly reference and use them throughout your application.
There are two primary categories to be aware of when thinking about containers in Hanami: Components and Providers. For example, here’s a quick and dirty way to see the differences:
# demo/app/aspects/demo.rb
module Demo
module Aspects
class Demo
end
end
end
# demo/config/providers/demo.rb
Hanami.app.register_provider :demo do
start { register :demo, Object.new }
end
With the above, we have a Demo application that has a core Demo component and a demo provider. Neither of these do anything since they are generic objects…and, yes, this is heavy use of demo terminology but bear with me because here’s how you’d access and use them when jumping into the Hanami console:
# Component (demo/app/aspects/demo.rb)
Hanami.app["aspects.demo"] # #<Demo::Aspects::Demo:0x00000001391987e8>
# Provider (demo/config/providers/demo.rb)
Hanami.app.start :demo
Hanami.app[:demo] # #<Object:0x000000013ad52ba0>
The rest of this article will unravel what the above is doing and why this is powerful.
Components
If you’ve spent any amount of time on this site, you know much has been written about containers, dependencies, and automatic injection over the years. The goal isn’t to repeat what’s already been written but to highlight and explain how this works in Hanami. For those that might need to get up speed, please read about Dependency Injection Containers first. Feel free to dive deeper by reading the documentation of these two gems as well:
-
Containable: Explains how containers work along with full implementation details.
-
Infusible: Explains how automatic injection builds upon what’s already registered within your containers.
When you combine the power of the above gems — and returning to the earlier code snippet — this means you can do the following:
# demo/app/aspects/demo.rb
module Demo
module Aspects
class Demo
end
end
end
# app/actions/dashboard/show.rb
module Demo
module Actions
module Dashboard
class Show < Demo::Action
include Deps[:demo]
end
end
end
end
Use of Deps which, confusingly, is short for Dependencies, not Departments, is the primary container for all of your dependencies within your Hanami application. This means, when you use include Deps[:demo] at the top of your class, you get automatic injection of the demo dependency. In other words, what the Containable and Infusible gems do for you in any Ruby application.
Defaults
By default, Hanami provides the following dependencies — made available via the Deps container — for immediate use:
-
assets: Allows you to access assets such as images, icons, and so forth. -
inflector: Allows you to customize object inflection so you can useAPI, for example, for your module and class names instead ofApi. -
logger: Allows you to log messages within your various objects. -
notifications: Allows you to instrumentation. -
routes: Provides named route URL helpers based on what is defined in your routes (i.e.routes.rb), -
settings: Allows you to provide custom settings unique to your application in a type safe manner.
Having default dependencies gets you off the ground and running quickly. Where this becomes problematic is when you want to swap them out with better implementations. Unfortunately, this is something you can’t do without a lot of effort. That said, one example of how you can achieve this is documented in Hanami Logging.
Registration
By default, Hanami will automatically register any object in the app directory for use as a dependency for future injection. This can also be disabled entirely or customized as befits your needs.
Automatic
The way automatic registration works — and is explained further in the Providers section — is by using each object’s path as a key for identification. For example, let’s say we have the following structure:
app
/repositories
/device
/firmware
/screen
The above would be registered as the following keys:
"repositories.device", "repositories.firmware", "repositories.screen"
Notice none of the keys use an "app" prefix. This is because automatic registration only works for files in the app directory and would be redundant to include so saves you some typing.
Now that you know what the keys are (which is essentially a path using dot notation), this means you can include any or all of these dependencies within another object. If we return to our earlier example, this means you could inject the "firmware" repository as follows:
# app/actions/dashboard/show.rb
module Demo
module Actions
module Dashboard
class Show < Demo::Action
include Deps["repositories.firmware"]
end
end
end
end
The above would give you immediate access to the firmware repository but you could inject the other repositories too by separating each with a comma.
Should you ever need to see the full list of all your dependencies, you can always jump into the Hanami console and run the following:
# Necessary to force all dependencies to be loaded (lazy loads by default).
Hanami.boot
# Lists all dependency keys (including providers).
Hanai.app.keys
Again, if you need more on this, read about Dependency Injection Containers if you haven’t already.
Disablement
There are times when you need to prevent Hanami from automatically registering an object. This can be for several reasons such as:
-
Any object you never want to inject into another object.
-
An abstract superclass that will never need to be instantiated directly and only exists to provide common functionality to all subclasses.
-
A
Procwhich will always yield this error:'Dry::System::Loader.call': undefined method 'new' for an instance of Proc (NoMethodError). -
Any object that can’t respond to
.newlike Dry Schema, proc, lambda, method, etc.
When you find yourself in any of the above situations, you can disable automatic registration by adding the following to the top of any file:
# auto_register: false
💡 See the Pragmater gem documentation if you’d like to learn more about pragmas.
If you find that you need to exclude an entire directory, you can update your app’s configuration as follows:
# config/app.rb
module Demo
class App < Hanami::App
config.no_auto_register_paths.append "schemas"
end
end
The above ensures all objects within the app/schemas directory (and subsequent sub-directories) are completely ignored.
Lastly, if none of the above is appealing, you can place your objects within the lib directory of your application since nothing that goes in that directory will be registered. This directory is still managed by Zeitwerk so you don’t have to add a require to the top of your files. The lib directory is also where you can create your own set of custom dependencies using Containable + Infusible which gives you a lot more control over behavior.
Memoization
By default, all application components are memoized. You can change this behavior by adding the following pragma to the top of your objects’s source file:
# memoize: false
The above will ensure your object is not memoized.
💡 See the Pragmater gem documentation if you’d like to learn more about pragmas.
Additionally, you can disable memoization by namespace. For example, you could add the following to your application configuration as follows:
# config/app.rb
class App < Hanami::App
config.no_memoize = %w[jobs schemas]
end
The above ensures any dependency that starts with "jobs" or "schemas" won’t be memoized. You add fine grained control by using a block:
class App < Hanami::App
config.no_memoize = -> component { component.key.start_with? "jobs" }
end
💡 Use of the memoize pragmet on an individual component always takes precedence over the no_memoize configuration.
Providers
Providers allow you to register additional components beyond the automatic registration of your application’s components with the following behavior:
-
Ability to register a specific instance of an object as a component for dependency injection.
-
Ability to configure a component that might require multiple steps or other dependencies in order to be setup correctly.
-
Ability to tap into the
prepare,start, andstophooks of the provider lifecycle.
Here’s a simple example where the Redis gem is defined for use as a dependency:
# config/providers/redis.rb
Hanami.app.register_provider :redis do
prepare { require "redis" }
start { register :redis, Redis.new(url: slice[:settings].redis_url) }
stop { slice[:redis].shutdown }
end
The above taps into all provider lifecycle steps. You’ll also notice that this provider is defined as redis.rb within the config/providers directory which allows Hanami to discover and use the provider. Each step of the lifecycle will be explained shortly.
Naming
The name of your provider is important because if there is a mismatch between the file name and the key used to register your provider, you’ll get an error. Consider the following:
# demo/config/providers/a_demo.rb
Hanami.app.register_provider :demo do
end
Notice the file name is a_demo.rb but demo is used when registering the provider. If you were to start this provider using the registered key as follows:
Hanami.app.start :demo
You’d end up with the following error:
Provider :demo not found (Dry::System::ProviderNotFoundError)
This can be confusing to debug so ensure your provider file name and key always match.
Loading
Provider load order isn’t guaranteed to be deterministic. For the most part, providers load in alphabetical order based on file name. For example, the following will be executed in the order listed:
a.rb b.rb c.rb
You don’t want to rely on this information because it might change but also because you can always force a specific provider dependency to be prepared or started within your own provider’s lifecycle which alleviates having to know when a provider is loaded.
Lifecycles
A lifecycle consists of the following steps:
-
Prepare
-
Start
-
Stop
You can trigger any of these steps by jumping into the Hanami console and running the following:
app = Hanami.app
app.prepare # Triggers all providers prepare blocks.
app.boot # Triggers all providers start blocks.
app.shutdown # Triggers all providers stop blocks.
Unfortunately, provider method names are not always consistent with the corresponding lifecycle steps and would definitely be nice to see this issue fixed in the future. Each lifecycle is explained in greater detail below which will highlight these inconsistencies further.
ℹ️ When in the development environment (i.e. console) or test environment (i.e. RSpec), the providers will not be available. This is by design because Hanami provides only the minimum by running the prepare lifecycle by default to remain fast. You can use Hanami.boot to fully load the application including all providers.
Prepare
This lifecycle step is meant for preparing providers for use which usually means using require to load dependencies. Example:
# config/providers/redis.rb
Hanami.app.register_provider :redis do
prepare { require "redis" }
end
If you jump into the Hanami console, you can then prepare the above redis provider multiple ways by using one of the following:
# Explicit require but more cumbersome to type.
require "hanami/prepare"
# Direct messaging of the app.
Hanami.app.prepare
# Syntactic sugar to prepare the entire app. Doesn't accept arguments.
Hanami.prepare
# Prepare a specific provider.
Hanami.app.prepare :redis
You can also check if your application is prepared or not:
Hanami.app.prepared?
Start
This lifecycle step is meant for configuring and registering your providers. Example:
# config/providers/redis.rb
Hanami.app.register_provider :redis do
start { register :redis, Redis.new(url: slice[:settings].redis_url) }
end
If you jump into the Hanami console, you can then start the above provider multiple ways by using one of the following:
# Explicit require but more cumbersome to type.
require "hanami/boot"
# Direct messaging of the app. Doesn't accept arguments.
Hanami.app.boot
# Syntactic sugar to prepare the entire app. Doesn't accept arguments.
Hanami.boot
# Start a specific provider. Yes, inconsistent method name.
Hanami.app.start :demo
⚠️ Starting/booting the app will also freeze the container(s) which means you can’t add anything further to the container after the start lifecycle has completed.
You can also check if your application is started or not:
Hanami.app.booted?
Stop
This lifecycle step is meant for stoping and cleaning up your providers. Example:
# config/providers/redis.rb
Hanami.app.register_provider :redis do
stop { slice[:redis].shutdown }
end
If you jump into the Hanami console, you can then stop the above provider multiple ways by using one of the following:
# Direct messaging of the app. Doesn't accept arguments.
Hanami.app.shutdown
# Syntactic sugar to stop the entire app. Doesn't accept arguments.
Hanami.shutdown
# Stops a single provider. Yes, inconsistent method name.
Hanami.app.stop :redis
Unlike the prepare or start steps, you can’t inquire if the application has been stopped but you’ll definitely get plenty of errors if you stopped the app and tried to continue running regardless.
Undefined
We’ve discussed all lifestyle steps but there is one more which is undefined. Example:
Hanami.app.register_provider :demo do
puts "I have no lifecycle step!"
end
The above is worth knowing about but is not recommended because whatever code you put here will be run outside of any step. In fact, the above will be run only once which can happen at any step which leads to confusing and indeterministic behavior. Again, don’t use.
Namespaces
To use namespaces, add namespace: true as a keyword argument. Example:
Hanami.app.register_provider :persistence, namespace: true do
start do
register "config", Hash.new
register "db", :database_demo
end
end
With the above, we’ve registered the persistence namespace which has both config and db defined within it. This is identical to how Containable namespaces work which allows you to organize multiple objects within the same namespace. To confirm, you can always jump into the Hanami console and see for yourself:
Hanami.app.start :persistence
Hanami.app.keys
# [
# "persistence.config",
# "persistence.db"
# ]
Had we not enabled a namespace, we’d end up with config and db as top-level keys. Example:
# Register without namespace
Hanami.app.register_provider :persistence do
start do
register "config", Hash.new
register "db", :database_demo
end
end
# Verify
Hanami.app.start :persistence
Hanami.app.keys
# [
# "config",
# "db"
# ]
As you can see, this is confusing, lacks readability, and doesn’t show these components are related.
There are situations where you might not want to use namespace: true and manually create the namespace yourself. This is especially useful when you need to register an object at root of your namespace. Example:
# config/providers/i18n.rb
Hanami.app.register_provider :i18n do
start do
register :i18n, I18n
register "i18n.t", I18n.method(:translate)
end
end
Notice the namespace key isn’t used but dependencies are registered via the i18n and i18n.t keys. Use of i18n means we can register an object at the namespace root (since we also register i18n.t within the namespace shortly after). See the i18n Example, below, for full details.
⚠️ One downside of not using a namespace means you have to use Hanami.start :i18n in IRB, at the top of your specs, an so forth in order to gain access to the i18n.t object because using Hanami.app["i18n.t"] won’t work as you’d expect without the starting the provider first. This isn’t a problem when using namespaces.
Dependencies
Once your providers are registered, associated dependencies can be accessed directly via the app like any other dependency. Example:
Hanami.app[:redis]
You can also inject provider dependencies like you would any other dependency. Example:
include Deps[:redis]
Locals
All providers have access to local variables that you can interact with during each lifecycle which can be inspected as follows:
# demo/config/providers/debug.rb
Hanami.app.register_provider :debug do
prepare do
config # #<Dry::Configurable::Config values={}>
slice # Demo::App
target # Demo::App
target_container # Demo::App
container # #<Dry::Core::Container:0x00000001389b49c8 @_container={}>
provider_container # #<Dry::Core::Container:0x00000001389b49c8 @_container={}>
callbacks # {before: {prepare: []}, after: {}}
end
end
Each of the above can be explained as follows:
-
config: This is a copy of the slice’s configuration which is copied at time of first access. -
slice: Provides access to the current slice’s container. For the main application, this meanssliceis a convenience wrapper aroundHanami.appwhich save you some typing. When in a slice other than the main application, then thesliceis relative to the slice that your provider is located in. You’ll notice thattargetandtarget_containerdo the same thing. Stick withsliceso you can use the same terminology regardless of the slice you are currently in. The other nice thing is you can usesliceto access a dependency either in the application or another provider. Example:slice[:logger]. This is most useful in thestartlifecycle step because if theloggerhasn’t been started, it will be and then made available for immediate use. -
container: Provides access to the provider container which only houses provider specific dependencies. Theprovider_containeryields the same result at the cost of being more to type. -
callbacks: These are misleading because they don’t work and appear to be vestiges of Dry System. If callbacks did work, you’d be able to definebeforeandaftercallbacks for theprepareandstartlifecycle steps.
Memoization
As with components, providers are memoized by default when you don’t provide a block to the register method. On the other hand, when using a block, you’ll need to specify memoization. For example, the following syntax is equivalent and will ensure memoization:
# Without block.
Hanami.app.register_provider :demo do
start do
register :demo, Object.new
end
end
# With block but with memoize set to true.
Hanami.app.register_provider :demo do
start do
register :demo, memoize: true do
Object.new
end
end
end
If you were to jump into the Hanami console — use either syntax from above — you’ll see the object ID remains the same:
Hanami.app[:demo].object_id # 19968
Hanami.app[:demo].object_id # 19968
Hanami.app[:demo].object_id # 19968
The only way to register a dependency without memoization is to use a block with memoize set to false. Example:
Hanami.app.register_provider :demo do
start do
register :demo, memoize: false do
Object.new
end
end
end
If you were to jump into the Hanami console — using the above provider — you’d see that you get a new instance each time:
Hanami.app[:demo].object_id # 9080
Hanami.app[:demo].object_id # 9784
Hanami.app[:demo].object_id # 10488
That fact that using or not using a block causes different behavior is bug. This includes the fact that you can’t disable memoization when not using a block. For example, this does not disable memoization: register :demo, Object.new, memoize: false.
Knowing how memoization works is important but be careful when using or not using blocks.
Conditionals
Providers can be conditionally registered using the if keyword argument as shown below:
Hanami.app.register_provider :demo, if: Hanami.env?(:production) do
end
With the above, the demo provider would only be registered if in a production environment. The if keyword argument only takes a value that evaluates to true and does not accept Proc objects.
If you are wondering if you could use a unless keyword argument, much like you’d do with native Ruby conditional logic, the answer is no. Only the if keyword is allowed for conditional logic.
Inheritance
In situations where your provider has more involved lifecycles and you want test coverage, you can create your own implementation. At a high level, this is what you need to know:
-
Use
app/providersas the namespace for quick lookup and organization of your implementations. -
Subclass
Hanami::Provider::Sourcein order to implement your custom solution. -
Each subclass expects a
provider_container,target_container, andsliceto be injected as keyword arguments. -
You still need to register your provider via
config/providers/demo.rb, for example, and is simple as adding a one-liner. Example:Hanami.app.register_provider :demo, source: Demo::Providers::Demo.
To illustrate, here’s an example of Cogger as a logger provider (and documented in greater detail via Hanami Logging):
# app/providers/logger.rb
# auto_register: false
module Demo
module Providers
class Logger < Hanami::Provider::Source
RESOLVER = proc { Object.const_get "Cogger" }
def initialize(environment: Hanami.env, resolver: RESOLVER, **)
@environment = environment
@resolver = resolver
@id = Hanami.app.namespace.to_s.downcase.to_sym
super(**)
end
def prepare = require "cogger"
def start
add_filters
register :logger, build_instance
end
private
attr_reader :environment, :resolver, :id
def add_filters = logger.add_filters :csrf, :password, :password_confirmation
def build_instance
io = "log/#{environment}.log"
case environment
when :test
logger.new(id:, io: StringIO.new, formatter: :json, level: :debug).add_stream io:
when :development then logger.new(id:).add_stream(io:, formatter: :json)
else logger.new id:, formatter: :json
end
end
def logger
@logger ||= resolver.call
end
end
end
end
RSpec Example
The following is the specification to the above implementation:
# spec/app/providers/logger_spec.rb
require "hanami_helper"
RSpec.describe Demo::Providers::Logger do
subject(:provider) { described_class.new provider_container:, target_container:, slice: }
let(:provider_container) { Dry::Core::Container.new }
let(:target_container) { Dry::Core::Container.new }
let(:slice) { Hanami.app }
describe "#prepare" do
it "answers false due to already being loaded" do
expect(provider.prepare).to be(false)
end
end
describe "#start" do
let(:cogger) { class_spy Cogger }
let(:hub) { instance_spy Cogger::Hub }
before { allow(cogger).to receive(:new).and_return hub }
it "adds filters" do
provider = described_class.new(
environment: :test,
resolver: proc { cogger },
provider_container:,
target_container:,
slice:
)
provider.start
expect(cogger).to have_received(:add_filters).with(any_args)
end
context "with test environment" do
subject :provider do
described_class.new environment: :test,
resolver: proc { cogger },
provider_container:,
target_container:,
slice:
end
it "initializes" do
provider.start
expect(cogger).to have_received(:new).with(
id: :demo,
io: kind_of(StringIO),
formatter: :json,
level: :debug
)
end
it "adds stream" do
provider.start
expect(hub).to have_received(:add_stream).with(io: "log/test.log")
end
end
context "with development environment" do
subject :provider do
described_class.new environment: :development,
resolver: proc { cogger },
provider_container:,
target_container:,
slice:
end
it "initializes" do
provider.start
expect(cogger).to have_received(:new).with(id: :demo)
end
it "adds stream" do
provider.start
expect(hub).to have_received(:add_stream).with(io: "log/development.log", formatter: :json)
end
end
context "with any other environment" do
subject :provider do
described_class.new environment: :production,
resolver: proc { cogger },
provider_container:,
target_container:,
slice:
end
it "initializes" do
provider.start
expect(cogger).to have_received(:new).with(id: :demo, formatter: :json)
end
end
it "registers logger" do
provider.start
expect(provider_container.key?(:logger)).to be(true)
end
end
end
To register the above implementation, use the following:
# config/providers/logger.rb
Hanami.app.register_provider :logger, source: Demo::Providers::Logger
The key takeaways with this implementation are:
-
The
# auto_register: falsepragma is important in order to avoid unnecessary components being registered that will never be used directly. -
The
RESOLVERconstant is injected to lazy resolve theCoggerconstant at runtime as theloggerinstance variable. This is important for a couple reasons:-
Ensures your
preparelifecycle step has time to require the Cogger dependency while also ensuring provider registration doesn’t fail with aNameErrordue toCoggernot being required during initialization. Otherwise, you’d be manually requiring the Cogger gem which defeats the purpose of thepreparestep. -
Allows you to use RSpec Test Doubles for testing purposes. Otherwise, testing your provider would be much more difficult.
-
-
The
environmentis injected which also aids testing so you can quickly write specs for each supported environment. -
You only need to implement the lifecycle methods for your public interface:
prepare,start, andstop. In this example,stopisn’t needed. -
The remaining logic is specific to Cogger setup which ensures proper encapsulation.
A few additional notes on the above:
-
You only need to inject empty
Dry::Core::Containerinstances for the provider and target containers. -
You can use
Hanami.appfor the slice which is the main application slice. If you were doing this in a specific slice, you’d use the slice, itself, instead ofHanami.app. -
The
preparespec doesn’t do a lot sinceCoggerwill be loaded by the time your test suite runs but nice to the kick the tires. -
The
startspec is where you are able to test the implementation by using spies. Again, this is made possible by being able to inject the resolver and the environment. Something you definitely can’t do without a ton of effort when not using a subclass.
You won’t always need a specialized provider class so you should default to using simple provider block registration syntax within the config/providers folder of your application. Otherwise, if your provider lifecycles are much more involved, definitely subclass to the app/providers folder so you can apply test coverage while still being able to register your custom provider.
Slices
Slices are out of scope for this article but is worth mentioning that they too can have their own providers by using the same directory structure within each slice. Example:
slices/demo/config/providers/demo.rb
Inspection
You can inspect your providers and associated objects by jumping into the Hanami console and use the following:
# List all providers.
Hanami.app.container.providers
# Acquire specific provider.
Hanami.app.container.providers[:htmx]
# Acquire specific provider's container.
Hanami.app.container.providers[:htmx].container
# List application specific files.
Hanami.app.container.providers.provider_files
You can also use Hanami.prepare, Hanami.boot, or Hanami.shutdown to inspect at different lifecycles.
Examples
The following is a collection of examples that might be of interest to learn about the various ways you can leverage different providers.
Alba
The following is an example of using Alba for serialization:
# config/providers/alba.rb
Hanami.app.register_provider :alba do
prepare { require "alba" }
start do
Alba.backend = :oj
Alba.enable_inference! with: :dry
end
end
Cogger
The following uses the Cogger gem to provide a custom — and enhanced — logger to replace Hanami’s default logger:
Hanami.app.register_provider :logger do
prepare { require "cogger" }
start do
Cogger.add_filters :csrf, :password, :password_confirmation
register :logger, Cogger.new(id: :demo)
end
end
The above is a simple example but, if you need more functionality, check out the Inheritance section above or Hanami Logging for a deeper dive.
HTMX
The following uses the HTMX gem for processing HTTP headers, building DOM elements, and more.
Hanami.app.register_provider :htmx do
prepare { require "htmx" }
start do
toggler = lambda do |request, default = "app"|
HTMX.request?(request.env, :request, "true") ? false : default
end
register :htmx, HTMX
register :htmx_defaults, {"defaultSwap" => "outerHTML"}.freeze
register :htmx_layout, toggler
end
end
HTTP
The following uses the HTTP gem for processing HTTP requests and responses.
Hanami.app.register_provider :http do
prepare do
require "connection_pool"
require "http"
end
start do
slice.start :logger
http = ConnectionPool::Wrapper.new size: ENV.fetch("HANAMI_MAX_THREADS", 5) do
HTTP.timeout(connect: 2, read: 10, write: 10)
.use(:auto_inflate)
.use(logging: {logger: slice[:logger]})
.headers("User-Agent" => "http.rb/#{HTTP::VERSION} (#{Hanami.app.app_name})")
end
register :http, http
end
stop { slice[:http].close }
end
The following uses the Mail gem to provide email functionality.
Hanami.app.register_provider :mail do
prepare { require "mail" }
start do
Mail.defaults do
case Hanami.env
when :development then delivery_method :smtp, address: "127.0.0.1", port: 1025
else delivery_method :test
end
end
register :mail, Mail
end
end
mail:
image: axllent/mailpit
ports:
- "1025:1025" # SMTP Server
- "8025:8025" # Web UI
restart: unless-stopped
Redis
The following is an example of adding Redis as a provider where you can leverage environment variables and settings to configure the provider.
Start by adding the following to your environment:
REDIS_PASSWORD=secret
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0
REDIS_URL="redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}"
Next, add a setting:
module Demo
class Settings < Hanami::Settings
setting :redis_url, constructor: Types::Params::String
end
end
Last, setup your provider by levering both the environment and setting:
# config/providers/redis.rb
Hanami.app.register_provider :redis do
prepare { require "redis" }
start { register :redis, Redis.new(url: slice[:settings].redis_url) }
stop { slice[:redis].shutdown }
end
Rollbar
Maybe you are using Rollbar for exception monitoring and would like to have it configured for use in production. Here’s an example of what that might look like:
# config/providers/rollbar.rb
Hanami.app.register_provider :rollbar, if: Hanami.env?(:production) do
prepare { require "rollbar" }
start do
Rollbar.configure do |config|
config.access_token = slice[:settings].rollbar_token
end
slice[:logger].add_backend Rollbar, log_if: :exception?
end
end
The above accomplishes the following:
-
Ensures Rollbar only starts in production.
-
Adds Rollbar as an additional logger.
Sequel
Hanami will automatically configure the database provider for you but you can customize the default provider by using the same key (i.e. :db) to alter and enhance further. In this case, we teach Sequel to default to the UTC time zone.
# config/providers/db.rb
Hanami.app.configure_provider(:db) { Sequel.default_timezone = :utc }
You could also ensure an in-memory SQLite database is automatically migrated when the database starts:
# config/providers/db.rb
Hanami.app.configure_provider :db do
after :start do
slice["db.gateways.default"].auto_migrate! slice["db.config"], inline: true
end
end
Delving further, you could register additional extensions for your database:
# config/providers/db.rb
Hanami.app.configure_provider :db do
config.gateway :default do |gateway|
gateway.adapter :sql do |adapter|
adapter.extension :pg_enum
end
end
end
Specs
Due to providers not being fully loaded when running your test suite, you might need to load on demand. One way to handle this is by adding the following configuration:
# spec/hanami_helper.rb
RSpec.configure do |config|
config.before :each, :with do |example|
containers = Hanami.app.slices.each.map(&:container).prepend Hanami.app.container
Array(example.metadata[:with]).each do |provider|
containers.each do |system|
system.start(provider) if system.providers.find_and_load_provider provider
end
end
end
end
The above would allow you to dynamically use any provider on demand. Example:
# Specification
it "is an example that needs Redis", with: :redis do
# Specification details go here.
end
Guidelines
When using providers, adhere to the following:
-
Avoid lifecycle blocks that are multiple lines long. These are meant to be lightweight and should not be overly complicated to implement. Otherwise, the provider you are trying to register is too complicated and needs to be simplified or extracted to it’s own implementation as discussed when using Inheritance.
-
Avoid using providers only for configuration logic as that should go in
app.rbor in an initializer. The other reason is providers execute in a non-deterministic order which can lead to order of operation issues.
Conclusion
Dependency Injection Containers are extremely powerful and now you know how to use them in Hanami. You’ve also learned how they are automatically registered, how you can disable them, and how you can use more sophisticated forms of dependency injection via providers. All of this combined together gives you a nice way to break out your dependencies into smaller, maintainable, and testable objects that can be reused throughout your application.