NAME
Catalyst::Seal::Construct - two things built per request that need not be
DESCRIPTION
A controller's BUILD on the context object
The application class's linearized ISA is
MyApp, Catalyst, Catalyst::Component, Moose::Object, Catalyst::Controller
so the per-request context object inherits Catalyst::Controller::BUILD, and BUILDALL runs it on a throwaway object every request:
my $attr_value = $self->merge_config_hashes($actions, $action);
$self->_controller_actions($attr_value);
$self->_all_actions_attributes; # trigger lazy builder
$self->_action_roles; # trigger lazy builder
Two lazy builders fired to compute values derived from class data that stopped changing at setup_finalize. 17.5 us per request inclusive.
The memo here is deliberately narrow. It runs the stock body once per class, and remembers the class only when all three results came out empty, which is the case for a context object built with no arguments. Then it stores *fresh* empty containers into each new instance rather than sharing the remembered ones: a shared actions hash that something wrote to would leak between requests, and _build__all_actions_attributes deletes a key from that hash as it goes.
Anything else, including a real controller being configured at setup with action or actions arguments, takes the stock body.
A Stats object built when stats are off
prepare does
$c->stats($class->stats_class->new)->enable($c->use_stats);
unconditionally, and Catalyst::Stats has tree as a required, non-lazy attribute defaulting to Tree::Simple->new({t => [gettimeofday]}). So a tree is built and the clock read on every request for an object that, with stats disabled, nothing reads again.
With stats enabled this changes nothing: the object is constructed eagerly, at the same point, because the timestamp in that default is the request start and deferring it would move the elapsed time that gets reported.
With stats disabled the construction is deferred to the first read of $c->stats, which usually never comes. It is not shared between requests: a request calling $c->stats->enable(1) would otherwise profile into a tree that outlives it and grows for the life of the worker.
The constructors
Three objects are built per request - the context, the request and the response - and Moose's inlined constructor asks the same questions of the same attributes every time. Best of five, in nanoseconds:
stock here
Catalyst::Request->new 2373 1010
Catalyst::Response->new 1914 1016
MyApp->new 3764 2275
bless {} (the floor) 292
The template is resolved once, at seal time: which key each attribute reads, whether it has a default and whether that default is a value, an empty container or a code reference to call, and which attributes Moose would do more than a store for.
A default that is sub { {} } or sub { [] } is made in C. That is decided by making two of them and looking - two distinct empty containers of the same kind - rather than by reading the code, because what matters is what the default produces and not how it is spelled.
What goes the long way
The escape hatch is the whole safety argument, and it is taken before anything is built, so there is never a half-built instance to hand over.
A value passed for an attribute with a trigger or a coercion. Both do something beyond storing, and a store that skipped them would be a different operation.
A value passed for a typed attribute that fails the constraint's own check. One that passes is stored here -
data_handlersis typed and is passed on every single request, and delegating on that alone would leave the request constructor never once taken. A value that fails goes to Moose, which raises the error it would have raised anyway and words it as Moose words it.A required attribute with no default and no builder, absent.
A subclass created after the seal, an odd argument list, or an
__INSTANCE__to build into.
A weak attribute is stored and then weakened, which is what Moose does, and _log on the request is the one that matters: a strong copy there would hold the application's logger through every request object that ever referred to it.
The BUILD methods are pinned
The bodies BUILDALL would call are resolved when the constructor is sealed, which is exactly what Moose's own inlined constructor does and for the same reason. They are read out of the stash rather than out of the metaclass, because half of this distribution installs a replacement by assigning a glob - the controller BUILD above is one - and that is what BUILDALL would find.
A modifier landing on a BUILD afterwards makes the pinned set wrong, so every sealed constructor is given back at that point rather than kept. A BUILD added to a class that had none is not noticed, which is the same contract Moose offers: it would want make_immutable run again too.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)