Skip to content

Commit

Permalink
Make Container hooks more useful for dynamic registration (#274)
Browse files Browse the repository at this point in the history
* Introduce after_register hook

Allows you to take action when a new key is registered, so that you can
decorate the key without knowing its name ahead of time.

See also hanami/hanami#1360

* Run after_finalize hooks before freeze

This hook is much less useful that it might be if you could still make
changes to the container when it ran.

* Add docs for before, after hooks
  • Loading branch information
alassek authored Mar 9, 2025
1 parent 17dcff1 commit dd4ad2f
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docsite/source/container.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Container
layout: gem-single
name: dry-system
sections:
- hooks
---

The main API of dry-system is the abstract container that you inherit from. It allows you to configure basic settings and exposes APIs for requiring files easily. Container is the entry point to your application, and it encapsulates application's state.
Expand Down
78 changes: 78 additions & 0 deletions docsite/source/container/hooks.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Hooks
layout: gem-single
name: dry-system
---

There are a few lifecycle events that you can hook into if you need to ensure things happen in a particular order.

Hooks are executed within the context of the container instance.

### `configure` Event

You can register a callback to fire after the container is configured, which happens one of three ways:

1. The `configure` method is called on the container
2. The `configured!` method is called
3. The `finalize!` method is called when neither of the other two have been

```ruby
class MyApp::Container < Dry::System::Container
after(:configure) do
# do something here
end
end
```

### `register` Event

Most of the time, you will know what keys you are working with ahead of time. But for certain cases you may want to
react to keys dynamically.

```ruby
class MyApp::Container < Dry::System::Container
use :monitoring

after(:register) do |key|
next unless key.end_with?(".gateway")

monitor(key) do |event|
resolve(:logger).debug(key:, method: event[:method], time: event[:time])
end
end
end
```

Now let's say you register `api_client.gateway` into your container. Your API methods will be automatically monitored
and their timing measured and logged.

### `finalize` Event

Finalization is the point at which the container is made ready, such as booting a web application.

The following keys are loaded in sequence:

1. Providers
2. Auto-registered components
3. Manually-registered components
4. Container imports

At the conclusion of this process, the container is frozen thus preventing any further changes. This makes the
`finalize` event quite important: it's the last call before your container will disallow mutation.

Unlike the previous events, you can register before hooks in addition to after hooks.

The after hooks will run immediately prior to the container freeze. This allows you to enumerate the container keys
while they can still be mutated, such as with `decorate` or `monitor`.

```ruby
class MyApp::Container < Dry::System::Container
before(:finalize) do
# Before system boot, no keys registered yet
end

after(:finalize) do
# After system boot, all keys registered
end
end
```
13 changes: 11 additions & 2 deletions lib/dry/system/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,10 @@ def finalize!(freeze: true, &)
[providers, auto_registrar, manifest_registrar, importer].each(&:finalize!)

@__finalized__ = true

self.freeze if freeze
end

self.freeze if freeze

self
end

Expand Down Expand Up @@ -484,6 +484,15 @@ def root
config.root
end

# @api public
def register(key, *)
super

hooks[:after_register].each { |hook| instance_exec(key, &hook) }

self
end

# @api public
def resolve(key)
load_component(key) unless finalized?
Expand Down
44 changes: 44 additions & 0 deletions spec/unit/container/hooks/after_hooks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

RSpec.describe Dry::System::Container do
subject(:system) do
Class.new(described_class)
end

describe "after_register hook" do
it "executes after a new key is registered" do
expect { |hook|
system.after(:register, &hook)
system.register(:foo) { "bar" }
}.to yield_with_args(:foo)
end

it "provides the fully-qualified key" do
expect { |hook|
system.after(:register, &hook)
system.namespace :foo do
register(:bar) { "baz" }
end
}.to yield_with_args("foo.bar")
end
end

describe "after_finalize hook" do
it "executes after finalization" do
expect { |hook|
system.after(:finalize, &hook)
system.finalize!
}.to yield_control
end

it "executes before the container is frozen" do
is_frozen = nil

system.after(:finalize) { is_frozen = frozen? }
system.finalize!

expect(is_frozen).to eq false
expect(system).to be_frozen
end
end
end

0 comments on commit dd4ad2f

Please sign in to comment.