callbacks
Callbacks
Callbacks hook into the build lifecycle so a factory can mutate its instance
or trigger side-effects at well-defined points. The built-in events are
after build, before create, after create, and after stub; custom
names can be invoked by hand from other callbacks or custom build
strategies.
define {
.factory: 'user', {
.fname: 'Greg';
.after: 'build', -> $u, $eval { $u.fname = $u.fname.uc };
};
};
ORM::Factory.build('user').fname; # 'GREG'Built-in events
| Event | Fired during | Phase |
|---|---|---|
after build | build, create | after |
before create | create (before persistence) | before |
after create | create (after persistence) | after |
after stub | build-stubbed | after |
attributes-for runs no callbacks ā it returns the attribute hash without
ever instantiating the model.
define {
.factory: 'user', {
.fname: 'Greg';
.before: 'create', -> $u, $eval { $u.fname = "[$u.fname()]" };
.after: 'create', -> $u, $eval { say "persisted user: $u.fname()" };
.after: 'stub', -> $u, $eval { $u.fname = '<stubbed>' };
};
};Callback signature
The block can take zero, one, or two arguments:
0 ā for fire-and-forget side effects
1 ā receives the instance under construction
2 ā receives the instance and the evaluator, so the callback can read any persisted or transient attribute (including overrides)
.after: 'build', -> $u, $eval {
$u.greeting = "hello, {$eval.salute}"; # transient via evaluator
};Multiple callbacks for one event
A factory may declare several callbacks for the same event. They fire in declaration order:
.factory: 'user', {
.after: 'build', -> $u, $e { $u.events.push: 'one' };
.after: 'build', -> $u, $e { $u.events.push: 'two' };
.after: 'build', -> $u, $e { $u.events.push: 'three' };
};
# events: ['one', 'two', 'three']Inheritance
Parent callbacks run before child callbacks. Each factory contributes its own callbacks at its position in the chain:
define {
.factory: 'user', {
.after: 'build', -> $u, $e { $u.events.push: 'parent' };
.factory: 'admin', {
.role: 'admin';
.after: 'build', -> $u, $e { $u.events.push: 'child' };
};
};
};
ORM::Factory.build('admin').events; # ['parent', 'child']Variants
A variant can register its own callbacks. When the variant is applied, its callbacks are appended after the factory's own:
define {
.factory: 'user', {
.after: 'build', -> $u, $e { $u.events.push: 'base' };
.variant: 'noisy', {
.after: 'build', -> $u, $e { $u.events.push: 'noisy' };
};
};
};
ORM::Factory.build('user').events; # ['base']
ORM::Factory.build('user', 'noisy').events; # ['base', 'noisy']Global callbacks
A before / after at the top of a define block ā without a
surrounding factory ā applies to every factory. Global callbacks fire
before any factory-specific callbacks for the same event:
define {
.after: 'build', -> $i, $e { $i.events.push: 'global' };
.factory: 'user', {
.fname: 'Greg';
.after: 'build', -> $u, $e { $u.events.push: 'user' };
};
};
ORM::Factory.build('user').events; # ['global', 'user']ORM::Factory.global-callbacks returns the list of registered globals.
ORM::Factory.reload clears them along with everything else.
Custom callbacks
callback 'name' registers a callback under an arbitrary event name. Custom
callbacks do not fire on any built-in event ā they only fire when something
asks for them explicitly via evaluator.run-callbacks('name'). The natural
caller is a custom build strategy, but built-in callbacks can
also chain to a custom name:
define {
.factory: 'user', {
.fname: 'Greg';
.callback: 'shouted', -> $u, $e { $u.fname = $u.fname.uc };
.after: 'build', -> $u, $e { $e.run-callbacks('shouted') };
};
};
ORM::Factory.build('user').fname; # 'GREG'has_many-style collections
A transient attribute plus an after build callback gives
a count knob and a side-effect that builds the children:
define {
.factory: 'user', { .fname: 'Greg' };
.factory: 'post', {
.title: 'Hello';
.transient: {
.comments-count: 0;
};
.after: 'build', -> $post, $eval {
for ^$eval.comments-count -> $i {
$post.comments.push: ORM::Factory.build('comment', :post($post));
}
};
};
};
ORM::Factory.build('post', :comments-count(3)).comments.elems; # 3The transient comments-count is excluded from attributes-for and never
reaches the model constructor ā only the callback sees it via the evaluator.
Reset between tests
ORM::Factory.reload clears every factory, alias, sequence, global variant,
and global callback. Per-test reset is identical to the rest of the
library:
before-each {
ORM::Factory.reload;
ORM::Factory.reset-persistence;
};