Class: Dry::System::Container

Inherits:
Object
  • Object
show all
Extended by:
Core::Container::Mixin, Plugins
Defined in:
lib/dry/system/container.rb,
lib/dry/system/stubs.rb

Overview

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:

  • ‘:name` - a unique container name

  • ‘:root` - a system root directory (defaults to `pwd`)

Examples:

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

Defined Under Namespace

Modules: Stubs

Class Method Summary collapse

Methods included from Plugins

enabled_plugins, inherited, loaded_dependencies, register, registry, use

Class Method Details

._configurable_finalize!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Finalizes the config for this container



141
# File 'lib/dry/system/container.rb', line 141

alias_method :_configurable_finalize!, :finalize!

.add_to_load_path!(*dirs) ⇒ self

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

Examples:

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

  add_to_load_path!('lib')
end

Parameters:

  • dirs (Array<String>)

Returns:

  • (self)


409
410
411
412
413
414
# File 'lib/dry/system/container.rb', line 409

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) ⇒ self

Registers a callback hook to run after container lifecycle events.

The supported events are:

  • ‘:configured`, called when you run configure or configured!, or when running finalize! and neither of the prior two methods have been called.

  • ‘:finalized`, called when you run finalize!.

When the given block is called, ‘self` is the container class, and no block arguments are given.

Parameters:

  • event (Symbol)

    the event name

  • block (Proc)

    the callback hook to run

Returns:

  • (self)


582
583
584
585
# File 'lib/dry/system/container.rb', line 582

def after(event, &block)
  hooks[:"after_#{event}"] << block
  self
end

.auto_registrarObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



532
533
534
# File 'lib/dry/system/container.rb', line 532

def auto_registrar
  @auto_registrar ||= config.auto_registrar.new(self)
end

.before(event, &block) ⇒ self

Registers a callback hook to run before container lifecycle events.

Currently, the only supported event is ‘:finalized`. This hook is called when you run `finalize!`.

When the given block is called, ‘self` is the container class, and no block arguments are given.

Parameters:

  • event (Symbol)

    the event name

  • block (Proc)

    the callback hook to run

Returns:

  • (self)


560
561
562
563
# File 'lib/dry/system/container.rb', line 560

def before(event, &block)
  hooks[:"before_#{event}"] << block
  self
end

.component_dirsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



522
523
524
# File 'lib/dry/system/container.rb', line 522

def component_dirs
  config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
end

.configDry::Configurable::Config

Returns the configuration for the container

Examples:

container.config.root = "/path/to/app"
container.config.root # => #<Pathname:/path/to/app>

Returns:

  • (Dry::Configurable::Config)


# File 'lib/dry/system/container.rb', line 74

.configure(finalize_config: true, &block) ⇒ self

Yields a configuration object for the container, which you can use to modify the configuration, then runs the after-‘configured` hooks and finalizes (freezes) the config.

Does not finalize the config when given ‘finalize_config: false`

Examples:

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

Parameters:

  • finalize_config (Boolean) (defaults to: true)

Returns:

  • (self)

See Also:



106
107
108
109
# File 'lib/dry/system/container.rb', line 106

def configure(finalize_config: true, &block)
  super(&block)
  configured!(finalize_config: finalize_config)
end

.configured!(finalize_config: true) ⇒ self

Marks the container as configured, runs the after-‘configured` hooks, then finalizes (freezes) the config.

This method is useful to call if you’re modifying the container’s config directly, rather than via the config object yielded when calling configure.

Does not finalize the config if given ‘finalize_config: false`.

Parameters:

  • finalize_config (Boolean) (defaults to: true)

Returns:

  • (self)

See Also:



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/dry/system/container.rb', line 126

def configured!(finalize_config: true)
  return self if configured?

  hooks[:after_configure].each { |hook| instance_eval(&hook) }

  _configurable_finalize! if finalize_config

  @__configured__ = true

  self
end

.configured?Boolean

Returns:

  • (Boolean)


143
144
145
# File 'lib/dry/system/container.rb', line 143

def configured?
  @__configured__.equal?(true)
end

.enable_stubs!Object

Enables stubbing container’s components

Examples:

require 'dry/system/stubs'

MyContainer.enable_stubs!
MyContainer.finalize!

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

Returns:

  • Container



32
33
34
35
36
# File 'lib/dry/system/stubs.rb', line 32

def self.enable_stubs!
  super
  extend ::Dry::System::Container::Stubs
  self
end

.finalize!(freeze: true) {|_self| ... } ⇒ self

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

Examples:

# 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

Yields:

  • (_self)

Yield Parameters:

Returns:

  • (self)

    frozen container



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/dry/system/container.rb', line 317

def finalize!(freeze: true, &block)
  return self if finalized?

  configured!

  hooks[:before_finalize].each { |hook| instance_eval(&hook) }
  yield(self) if block

  providers.finalize!
  auto_registrar.finalize!
  manifest_registrar.finalize!
  importer.finalize!

  @__finalized__ = true

  self.freeze if freeze
  hooks[:after_finalize].each { |hook| instance_eval(&hook) }
  self
end

.finalized?TrueClass, FalseClass

Return if a container was finalized

Returns:

  • (TrueClass, FalseClass)


284
285
286
# File 'lib/dry/system/container.rb', line 284

def finalized?
  @__finalized__.equal?(true)
end

.hooksObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



588
589
590
# File 'lib/dry/system/container.rb', line 588

def hooks
  @hooks ||= Hash.new { |h, k| h[k] = [] }
end

.import(from:, as:, keys: nil) ⇒ Object

Registers another container for import

Examples:

# system/container.rb
require "dry/system"
require "logger"

class Core < Dry::System::Container
  register("logger", Logger.new($stdout))
end

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

class MyApp < Dry::System::Container
  import(from: Core, as: :core)
end

MyApp.import(keys: ["logger"], from: Core, as: :core2)

MyApp["core.logger"].info("Test")
MyApp["core2.logger"].info("Test2")

Parameters:

  • keys (Array<String>) (defaults to: nil)

    Keys for the components to import

  • from (Class)

    The container to import from

  • as (Symbol)

    Namespace to use for the components of the imported container

Raises:



178
179
180
181
182
183
184
# File 'lib/dry/system/container.rb', line 178

def import(from:, as:, keys: nil)
  raise Dry::System::ContainerAlreadyFinalizedError if finalized?

  importer.register(container: from, namespace: as, keys: keys)

  self
end

.importerObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



542
543
544
# File 'lib/dry/system/container.rb', line 542

def importer
  @importer ||= config.importer.new(self)
end

.inherited(klass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



593
594
595
596
597
598
599
600
601
602
# File 'lib/dry/system/container.rb', line 593

def inherited(klass)
  hooks.each do |event, blocks|
    klass.hooks[event].concat blocks.dup
  end

  klass.instance_variable_set(:@__configured__, false)
  klass.instance_variable_set(:@__finalized__, false)

  super
end

.injector(**options) ⇒ Object

Builds injector for this container

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

Examples:

# 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

Parameters:

  • options (Hash)

    injector options



447
448
449
# File 'lib/dry/system/container.rb', line 447

def injector(**options)
  Dry::AutoInject(self, **options)
end

.key?(key) ⇒ Boolean

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

Parameters:

  • key (String, Symbol)

    Identifier

Returns:

  • (Boolean)


512
513
514
515
516
517
518
519
# File 'lib/dry/system/container.rb', line 512

def key?(key)
  if finalized?
    registered?(key)
  else
    registered?(key) || resolve(key) { return false }
    true
  end
end

.load_registrations!(name) ⇒ Object



417
418
419
420
# File 'lib/dry/system/container.rb', line 417

def load_registrations!(name)
  manifest_registrar.(name)
  self
end

.manifest_registrarObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



537
538
539
# File 'lib/dry/system/container.rb', line 537

def manifest_registrar
  @manifest_registrar ||= config.manifest_registrar.new(self)
end

.prepare(name) ⇒ self

Prepares a provider using its ‘prepare` lifecycle trigger

Preparing (as opposed to starting) a provider is useful in places where some aspects of a heavier dependency are needed, but its fully started environment

Examples:

MyApp.prepare(:persistence)

Parameters:

  • name (Symbol)

    The name of the registered provider to prepare

Returns:

  • (self)


367
368
369
370
# File 'lib/dry/system/container.rb', line 367

def prepare(name)
  providers.prepare(name)
  self
end

.providersObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



527
528
529
# File 'lib/dry/system/container.rb', line 527

def providers
  @providers ||= config.provider_registrar.new(self)
end

.register_provider(name, namespace: nil, from: nil, source: nil) ⇒ self

Registers a provider and its lifecycle hooks

By convention, you should place a file for each provider in one of the configured ‘provider_dirs`, and they will be loaded on demand when components are loaded in isolation, or during container finalization.

Examples:

# system/container.rb
class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
  end
end

# system/providers/db.rb
#
# Simple provider registration
MyApp.register_provider(:db) do
  start do
    require "db"
    register("db", DB.new)
  end
end

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

  start do
    container["db"].establish_connection
  end

  stop do
    container["db"].close_connection
  end
end

# system/providers/db.rb
#
# Provider registration which uses another provider
MyApp.register_provider(:db) do |container|
  start do
    use :logger

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

# system/providers/db.rb
#
# Provider registration under a namespace. This will register the
# db object with the "persistence.db" key
MyApp.register_provider(:persistence, namespace: "db") do
  start do
    require "db"
    DB.configure(ENV["DB_URL"])
    register("db", DB.new)
  end
end

Parameters:

  • name (Symbol)

    a unique name for the provider

  • namespace (String, nil) (defaults to: nil)

    the key namespace to use for any registrations made during the provider’s lifecycle

  • from (Symbol, nil) (defaults to: nil)

    the group for the external provider source (with the provider source name inferred from ‘name` or passsed explicitly as `source:`)

  • source (Symbol, nil) (defaults to: nil)

    the name of the external provider source to use (if different from the value provided as ‘name`)

  • if (Boolean)

    a boolean to determine whether to register the provider

Returns:

  • (self)

See Also:



273
274
275
# File 'lib/dry/system/container.rb', line 273

def register_provider(...)
  providers.register_provider(...)
end

.registered?(key) ⇒ Boolean

Whether a key is registered (doesn’t trigger loading)

Parameters:

  • key (String, Symbol)

    The key

Returns:

  • (Boolean)


497
# File 'lib/dry/system/container.rb', line 497

alias_method :registered?, :key?

.require_from_root(*paths) ⇒ Object

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

Examples:

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

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

Parameters:

  • paths (Array<String>)

    one or more paths, supports globs too



463
464
465
466
467
468
469
# File 'lib/dry/system/container.rb', line 463

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) ⇒ Object



490
491
492
493
494
# File 'lib/dry/system/container.rb', line 490

def resolve(key)
  load_component(key) unless finalized?

  super
end

.rootPathname

Returns container’s root path

Examples:

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

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

Returns:

  • (Pathname)


485
486
487
# File 'lib/dry/system/container.rb', line 485

def root
  config.root
end

.shutdown!Object



388
389
390
391
# File 'lib/dry/system/container.rb', line 388

def shutdown!
  providers.shutdown
  self
end

.start(name) ⇒ self

Starts a provider

As a result, the provider’s ‘prepare` and `start` lifecycle triggers are called

Examples:

MyApp.start(:persistence)

Parameters:

  • name (Symbol)

    the name of a registered provider to start

Returns:

  • (self)


349
350
351
352
# File 'lib/dry/system/container.rb', line 349

def start(name)
  providers.start(name)
  self
end

.stop(name) ⇒ self

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

Examples:

MyApp.stop(:persistence)

Parameters:

  • name (Symbol)

    The name of a registered bootable component

Returns:

  • (self)


382
383
384
385
# File 'lib/dry/system/container.rb', line 382

def stop(name)
  providers.stop(name)
  self
end