class Dry::System::Container

Abstract container class to inherit from

Container class is treated as a global registry with all system components. Container can also import dependencies from other containers, which is useful in complex systems that are split into sub-systems.

Container can be finalized, which triggers loading of all the defined components within a system, after finalization it becomes frozen. This typically happens in cases like booting a web application.

Before finalization, Container can lazy-load components on demand. A component can be a simple class defined in a single file, or a complex component which has init/start/stop lifecycle, and it's defined in a boot file. Components which specify their dependencies using Import module can be safely required in complete isolation, and Container will resolve and load these dependencies automatically.

Furthermore, Container supports auto-registering components based on dir/file naming conventions. This reduces a lot of boilerplate code as all you have to do is to put your classes under configured directories and their instances will be automatically registered within a container.

Every container needs to be configured with following settings:

@example

class MyApp < Dry::System::Container
  configure do |config|
    config.name = :my_app

    # this will auto-register classes from 'lib/components'. ie if you add
    # `lib/components/repo.rb` which defines `Repo` class, then it's
    # instance will be automatically available as `MyApp['repo']`
    config.auto_register = %w(lib/components)
  end

  # this will configure $LOAD_PATH to include your `lib` dir
  add_dirs_to_load_paths!('lib')
end

@api public

Public Class Methods

add_to_load_path!(*dirs) click to toggle source

Adds the directories (relative to the container's root) to the Ruby load path

@example

class MyApp < Dry::System::Container
  configure do |config|
    # ...
  end

  add_to_load_path!('lib')
end

@param [Array<String>] dirs

@return [self]

@api public

# File lib/dry/system/container.rb, line 403
def add_to_load_path!(*dirs)
  dirs.reverse.map(&root.method(:join)).each do |path|
    $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
  end
  self
end
after(event, &block) click to toggle source

@api private

# File lib/dry/system/container.rb, line 554
def after(event, &block)
  hooks[:"after_#{event}"] << block
end
auto_registrar() click to toggle source

@api private

# File lib/dry/system/container.rb, line 539
def auto_registrar
  @auto_registrar ||= config.auto_registrar.new(self)
end
before(event, &block) click to toggle source
# File lib/dry/system/container.rb, line 558
def before(event, &block)
  hooks[:"before_#{event}"] << block
end
boot(name, **opts, &block) click to toggle source

Registers finalization function for a bootable component

By convention, boot files for components should be placed in a `bootable_dirs` entry and they will be loaded on demand when components are loaded in isolation, or during the finalization process.

@example

# system/container.rb
class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :core
    config.auto_register = %w(lib/apis lib/core)
  end

# system/boot/db.rb
#
# Simple component registration
MyApp.boot(:db) do |container|
  require 'db'

  container.register(:db, DB.new)
end

# system/boot/db.rb
#
# Component registration with lifecycle triggers
MyApp.boot(:db) do |container|
  init do
    require 'db'
    DB.configure(ENV['DB_URL'])
    container.register(:db, DB.new)
  end

  start do
    db.establish_connection
  end

  stop do
    db.close_connection
  end
end

# system/boot/db.rb
#
# Component registration which uses another bootable component
MyApp.boot(:db) do |container|
  use :logger

  start do
    require 'db'
    DB.configure(ENV['DB_URL'], logger: logger)
    container.register(:db, DB.new)
  end
end

# system/boot/db.rb
#
# Component registration under a namespace. This will register the
# db object under `persistence.db` key
MyApp.namespace(:persistence) do |persistence|
  require 'db'
  DB.configure(ENV['DB_URL'], logger: logger)
  persistence.register(:db, DB.new)
end

@param name [Symbol] a unique name for a bootable component

@see Lifecycle

@return [self]

@api public

# File lib/dry/system/container.rb, line 246
        def boot(name, **opts, &block)
          if components.key?(name)
            raise DuplicatedComponentKeyError, <<-STR
              Bootable component #{name.inspect} was already registered
            STR
          end

          component =
            if opts[:from]
              boot_external(name, **opts, &block)
            else
              boot_local(name, **opts, &block)
            end

          booter.register_component component

          components[name] = component
        end
boot_external(name, from:, key: nil, namespace: nil, &block) click to toggle source

@api private

# File lib/dry/system/container.rb, line 267
def boot_external(name, from:, key: nil, namespace: nil, &block)
  System.providers[from].component(
    name, key: key, namespace: namespace, finalize: block, container: self
  )
end
boot_local(name, namespace: nil, &block) click to toggle source

@api private

# File lib/dry/system/container.rb, line 274
def boot_local(name, namespace: nil, &block)
  Components::Bootable.new(name, container: self, namespace: namespace, &block)
end
boot_paths() click to toggle source

@api private

# File lib/dry/system/container.rb, line 526
def boot_paths
  config.bootable_dirs.map { |dir|
    dir = Pathname(dir)

    if dir.relative?
      root.join(dir)
    else
      dir
    end
  }
end
booter() click to toggle source

@api private

# File lib/dry/system/container.rb, line 521
def booter
  @booter ||= config.booter.new(boot_paths)
end
component_dirs() click to toggle source

@api private

# File lib/dry/system/container.rb, line 516
def component_dirs
  config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
end
configure(&block) click to toggle source

Configures the container

@example

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :my_app
    config.auto_register = %w(lib/apis lib/core)
  end
end

@return [self]

@api public

Calls superclass method
# File lib/dry/system/container.rb, line 128
def configure(&block)
  hooks[:before_configure].each { |hook| instance_eval(&hook) }
  super(&block)
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
  self
end
enable_stubs!() click to toggle source

Enables stubbing container's components

@example

require 'dry/system/stubs'

MyContainer.enable_stubs!
MyContainer.finalize!

MyContainer.stub('some.component', some_stub_object)

@return Container

@api public

Calls superclass method
# File lib/dry/system/stubs.rb, line 28
def self.enable_stubs!
  super
  extend ::Dry::System::Container::Stubs
  self
end
finalize!(freeze: true) { |self| ... } click to toggle source

Finalizes the container

This triggers importing components from other containers, booting registered components and auto-registering components. It should be called only in places where you want to finalize your system as a whole, ie when booting a web application

@example

# system/container.rb
class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :my_app
    config.auto_register = %w(lib/apis lib/core)
  end
end

# You can put finalization file anywhere you want, ie system/boot.rb
MyApp.finalize!

# If you need last-moment adjustments just before the finalization
# you can pass a block and do it there
MyApp.finalize! do |container|
  # stuff that only needs to happen for finalization
end

@return [self] frozen container

@api public

# File lib/dry/system/container.rb, line 316
def finalize!(freeze: true, &block)
  return self if finalized?

  yield(self) if block

  importer.finalize!
  booter.finalize!
  manual_registrar.finalize!
  auto_registrar.finalize!

  @__finalized__ = true

  self.freeze if freeze
  self
end
finalized?() click to toggle source

Return if a container was finalized

@return [TrueClass, FalseClass]

@api public

# File lib/dry/system/container.rb, line 283
def finalized?
  @__finalized__.equal?(true)
end
hooks() click to toggle source

@api private

# File lib/dry/system/container.rb, line 563
def hooks
  @hooks ||= Hash.new { |h, k| h[k] = [] }
end
import(other) click to toggle source

Registers another container for import

@example

# system/container.rb
class Core < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.auto_register = %w(lib/apis lib/core)
  end
end

# apps/my_app/system/container.rb
require 'system/container'

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.auto_register = %w(lib/apis lib/core)
  end

  import core: Core
end

@param other [Hash, Dry::Container::Namespace]

@api public

Calls superclass method
# File lib/dry/system/container.rb, line 161
        def import(other)
          case other
          when Hash then importer.register(other)
          when Dry::Container::Namespace then super
          else
            raise ArgumentError, <<-STR
              +other+ must be a hash of names and systems, or a Dry::Container namespace
            STR
          end
        end
importer() click to toggle source

@api private

# File lib/dry/system/container.rb, line 549
def importer
  @importer ||= config.importer.new(self)
end
inherited(klass) click to toggle source

@api private

Calls superclass method
# File lib/dry/system/container.rb, line 568
def inherited(klass)
  hooks.each do |event, blocks|
    klass.hooks[event].concat blocks.dup
  end

  klass.instance_variable_set(:@__finalized__, false)

  super
end
init(name) click to toggle source

Boots a specific component but calls only `init` lifecycle trigger

This way of booting is useful in places where a heavy dependency is needed but its started environment is not required

@example

MyApp.init(:persistence)

@param [Symbol] name The name of a registered bootable component

@return [self]

@api public

# File lib/dry/system/container.rb, line 362
def init(name)
  booter.init(name)
  self
end
injector(options = {strategies: strategies}) click to toggle source

Builds injector for this container

An injector is a useful mixin which injects dependencies into automatically defined constructor.

@example

# Define an injection mixin
#
# system/import.rb
Import = MyApp.injector

# Use it in your auto-registered classes
#
# lib/user_repo.rb
require 'import'

class UserRepo
  include Import['persistence.db']
end

MyApp['user_repo].db # instance under 'persistence.db' key

@param options [Hash] injector options

@api public

# File lib/dry/system/container.rb, line 441
def injector(options = {strategies: strategies})
  Dry::AutoInject(self, options)
end
key?(key) click to toggle source

Check if identifier is registered. If not, try to load the component

@param [String,Symbol] key Identifier @return [Boolean]

@api public

# File lib/dry/system/container.rb, line 506
def key?(key)
  if finalized?
    registered?(key)
  else
    registered?(key) || resolve(key) { return false }
    true
  end
end
Also aliased as: registered?
load_registrations!(name) click to toggle source

@api public

# File lib/dry/system/container.rb, line 411
def load_registrations!(name)
  manual_registrar.(name)
  self
end
manual_registrar() click to toggle source

@api private

# File lib/dry/system/container.rb, line 544
def manual_registrar
  @manual_registrar ||= config.manual_registrar.new(self)
end
registered?(key)
Alias for: key?
require_from_root(*paths) click to toggle source

Requires one or more files relative to the container's root

@example

# single file
MyApp.require_from_root('lib/core')

# glob
MyApp.require_from_root('lib/**/*')

@param paths [Array<String>] one or more paths, supports globs too

@api public

# File lib/dry/system/container.rb, line 457
def require_from_root(*paths)
  paths.flat_map { |path|
    path.to_s.include?("*") ? ::Dir[root.join(path)].sort : root.join(path)
  }.each { |path|
    Kernel.require path.to_s
  }
end
resolve(key) click to toggle source

@api public

Calls superclass method
# File lib/dry/system/container.rb, line 484
def resolve(key)
  load_component(key) unless finalized?

  super
end
root() click to toggle source

Returns container's root path

@example

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname('/my/app')
  end
end

MyApp.root # returns '/my/app' pathname

@return [Pathname]

@api public

# File lib/dry/system/container.rb, line 479
def root
  config.root
end
setting(name, default = Dry::Core::Constants::Undefined, **options, &block) click to toggle source

Define a new configuration setting

@see dry-rb.org/gems/dry-configurable

@api public

Calls superclass method
# File lib/dry/system/container.rb, line 107
def setting(name, default = Dry::Core::Constants::Undefined, **options, &block)
  super(name, default, **options, &block)
  # TODO: dry-configurable needs a public API for this
  config._settings << _settings[name]
  self
end
shutdown!() click to toggle source
# File lib/dry/system/container.rb, line 382
def shutdown!
  booter.shutdown
  self
end
start(name) click to toggle source

Boots a specific component

As a result, `init` and `start` lifecycle triggers are called

@example

MyApp.start(:persistence)

@param name [Symbol] the name of a registered bootable component

@return [self]

@api public

# File lib/dry/system/container.rb, line 344
def start(name)
  booter.start(name)
  self
end
stop(name) click to toggle source

Stop a specific component but calls only `stop` lifecycle trigger

@example

MyApp.stop(:persistence)

@param [Symbol] name The name of a registered bootable component

@return [self]

@api public

# File lib/dry/system/container.rb, line 377
def stop(name)
  booter.stop(name)
  self
end
strategies(value = nil) click to toggle source
# File lib/dry/system/container.rb, line 92
def strategies(value = nil)
  if value
    @strategies = value
  else
    @strategies ||= Dry::AutoInject::Strategies
  end
end

Protected Class Methods

load_component(key) click to toggle source

@api private

# File lib/dry/system/container.rb, line 581
def load_component(key)
  return self if registered?(key)

  if (bootable_component = booter.find_component(key))
    booter.start(bootable_component)
    return self
  end

  component = find_component(key)

  booter.boot_dependency(component)
  return self if registered?(key)

  if component.loadable?
    load_local_component(component)
  elsif manual_registrar.file_exists?(component)
    manual_registrar.(component)
  elsif importer.key?(component.identifier.root_key)
    load_imported_component(component.identifier)
  end

  self
end

Private Class Methods

find_component(key) click to toggle source
# File lib/dry/system/container.rb, line 623
def find_component(key)
  # Find the first matching component from within the configured component dirs.
  # If no matching component is found, return a null component; this fallback is
  # important because the component may still be loadable via the manual registrar
  # or an imported container.
  component_dirs.detect { |dir|
    if (component = dir.component_for_key(key))
      break component
    end
  } || IndirectComponent.new(Identifier.new(key, separator: config.namespace_separator))
end
load_imported_component(identifier) click to toggle source
# File lib/dry/system/container.rb, line 613
def load_imported_component(identifier)
  import_namespace = identifier.root_key

  container = importer[import_namespace]

  container.load_component(identifier.namespaced(from: import_namespace, to: nil).key)

  importer.(import_namespace, container)
end
load_local_component(component) click to toggle source
# File lib/dry/system/container.rb, line 607
def load_local_component(component)
  if component.auto_register?
    register(component.identifier, memoize: component.memoize?) { component.instance }
  end
end