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:
-
`:name` - a unique container identifier
-
`:root` - a system root directory (defaults to `pwd`)
@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
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
@api private
# File lib/dry/system/container.rb, line 554 def after(event, &block) hooks[:"after_#{event}"] << block end
@api private
# File lib/dry/system/container.rb, line 539 def auto_registrar @auto_registrar ||= config.auto_registrar.new(self) end
# File lib/dry/system/container.rb, line 558 def before(event, &block) hooks[:"before_#{event}"] << block end
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 identifier for a bootable component
@see Lifecycle
@return [self]
@api public
# File lib/dry/system/container.rb, line 244 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
@api private
# File lib/dry/system/container.rb, line 265 def boot_external(identifier, from:, key: nil, namespace: nil, &block) System.providers[from].component( identifier, key: key, namespace: namespace, finalize: block, container: self ) end
@api private
# File lib/dry/system/container.rb, line 272 def boot_local(identifier, namespace: nil, &block) Components::Bootable.new( identifier, container: self, namespace: namespace, &block ) end
@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
@api private
# File lib/dry/system/container.rb, line 521 def booter @booter ||= config.booter.new(boot_paths) end
@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
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
# File lib/dry/system/container.rb, line 126 def configure(&block) hooks[:before_configure].each { |hook| instance_eval(&hook) } super(&block) hooks[:after_configure].each { |hook| instance_eval(&hook) } self end
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
# File lib/dry/system/stubs.rb, line 28 def self.enable_stubs! super extend ::Dry::System::Container::Stubs self end
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
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
@api private
# File lib/dry/system/container.rb, line 563 def hooks @hooks ||= Hash.new { |h, k| h[k] = [] } end
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
# File lib/dry/system/container.rb, line 159 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
@api private
# File lib/dry/system/container.rb, line 549 def importer @importer ||= config.importer.new(self) end
@api private
# 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
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
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
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
@api public
# File lib/dry/system/container.rb, line 411 def load_registrations!(name) manual_registrar.(name) self end
@api private
# File lib/dry/system/container.rb, line 544 def manual_registrar @manual_registrar ||= config.manual_registrar.new(self) end
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
@api public
# File lib/dry/system/container.rb, line 484 def resolve(key) load_component(key) unless finalized? super end
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
Define a new configuration setting
@see dry-rb.org/gems/dry-configurable
@api public
# File lib/dry/system/container.rb, line 105 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
# File lib/dry/system/container.rb, line 382 def shutdown! booter.shutdown self end
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 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
# File lib/dry/system/container.rb, line 90 def strategies(value = nil) if value @strategies = value else @strategies ||= Dry::AutoInject::Strategies end end
Protected Class Methods
@api private
# File lib/dry/system/container.rb, line 581 def load_component(key) return self if registered?(key) component = component(key) if component.bootable? booter.start(component) return self end booter.boot_dependency(component) return self if registered?(key) if component.file_exists? 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
# File lib/dry/system/container.rb, line 623 def component(identifier) if (bootable_component = booter.find_component(identifier)) return bootable_component end # Find the first matching component from within the configured component dirs. # If no matching component is found, return a plain component instance with no # associated file path. 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_identifier(identifier)) break component end } || Component.new(identifier) end
# 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.dequalified(import_namespace).key) importer.(import_namespace, container) end
# 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