Skip to content

Commit dd4ad2f

Browse files
authored
Make Container hooks more useful for dynamic registration (#274)
* 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
1 parent 17dcff1 commit dd4ad2f

File tree

4 files changed

+135
-2
lines changed

4 files changed

+135
-2
lines changed

docsite/source/container.html.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
title: Container
33
layout: gem-single
44
name: dry-system
5+
sections:
6+
- hooks
57
---
68

79
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.
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: Hooks
3+
layout: gem-single
4+
name: dry-system
5+
---
6+
7+
There are a few lifecycle events that you can hook into if you need to ensure things happen in a particular order.
8+
9+
Hooks are executed within the context of the container instance.
10+
11+
### `configure` Event
12+
13+
You can register a callback to fire after the container is configured, which happens one of three ways:
14+
15+
1. The `configure` method is called on the container
16+
2. The `configured!` method is called
17+
3. The `finalize!` method is called when neither of the other two have been
18+
19+
```ruby
20+
class MyApp::Container < Dry::System::Container
21+
after(:configure) do
22+
# do something here
23+
end
24+
end
25+
```
26+
27+
### `register` Event
28+
29+
Most of the time, you will know what keys you are working with ahead of time. But for certain cases you may want to
30+
react to keys dynamically.
31+
32+
```ruby
33+
class MyApp::Container < Dry::System::Container
34+
use :monitoring
35+
36+
after(:register) do |key|
37+
next unless key.end_with?(".gateway")
38+
39+
monitor(key) do |event|
40+
resolve(:logger).debug(key:, method: event[:method], time: event[:time])
41+
end
42+
end
43+
end
44+
```
45+
46+
Now let's say you register `api_client.gateway` into your container. Your API methods will be automatically monitored
47+
and their timing measured and logged.
48+
49+
### `finalize` Event
50+
51+
Finalization is the point at which the container is made ready, such as booting a web application.
52+
53+
The following keys are loaded in sequence:
54+
55+
1. Providers
56+
2. Auto-registered components
57+
3. Manually-registered components
58+
4. Container imports
59+
60+
At the conclusion of this process, the container is frozen thus preventing any further changes. This makes the
61+
`finalize` event quite important: it's the last call before your container will disallow mutation.
62+
63+
Unlike the previous events, you can register before hooks in addition to after hooks.
64+
65+
The after hooks will run immediately prior to the container freeze. This allows you to enumerate the container keys
66+
while they can still be mutated, such as with `decorate` or `monitor`.
67+
68+
```ruby
69+
class MyApp::Container < Dry::System::Container
70+
before(:finalize) do
71+
# Before system boot, no keys registered yet
72+
end
73+
74+
after(:finalize) do
75+
# After system boot, all keys registered
76+
end
77+
end
78+
```

lib/dry/system/container.rb

+11-2
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,10 @@ def finalize!(freeze: true, &)
325325
[providers, auto_registrar, manifest_registrar, importer].each(&:finalize!)
326326

327327
@__finalized__ = true
328-
329-
self.freeze if freeze
330328
end
331329

330+
self.freeze if freeze
331+
332332
self
333333
end
334334

@@ -484,6 +484,15 @@ def root
484484
config.root
485485
end
486486

487+
# @api public
488+
def register(key, *)
489+
super
490+
491+
hooks[:after_register].each { |hook| instance_exec(key, &hook) }
492+
493+
self
494+
end
495+
487496
# @api public
488497
def resolve(key)
489498
load_component(key) unless finalized?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Dry::System::Container do
4+
subject(:system) do
5+
Class.new(described_class)
6+
end
7+
8+
describe "after_register hook" do
9+
it "executes after a new key is registered" do
10+
expect { |hook|
11+
system.after(:register, &hook)
12+
system.register(:foo) { "bar" }
13+
}.to yield_with_args(:foo)
14+
end
15+
16+
it "provides the fully-qualified key" do
17+
expect { |hook|
18+
system.after(:register, &hook)
19+
system.namespace :foo do
20+
register(:bar) { "baz" }
21+
end
22+
}.to yield_with_args("foo.bar")
23+
end
24+
end
25+
26+
describe "after_finalize hook" do
27+
it "executes after finalization" do
28+
expect { |hook|
29+
system.after(:finalize, &hook)
30+
system.finalize!
31+
}.to yield_control
32+
end
33+
34+
it "executes before the container is frozen" do
35+
is_frozen = nil
36+
37+
system.after(:finalize) { is_frozen = frozen? }
38+
system.finalize!
39+
40+
expect(is_frozen).to eq false
41+
expect(system).to be_frozen
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)