NAME
Catalyst::Seal::ClassData - constant accessors for class data that stopped changing
DESCRIPTION
Catalyst::ClassData::mk_classdata generates an accessor that calls Moose::Util::find_meta($pkg) on every read, then walks linearized_isa if the package's own scalar slot is empty. $meta is computed unconditionally even though only the write path and the inheritance walk use it, and the common case, a defined value in the package's own slot, needs neither.
On a bare application that is 84 find_meta calls, 84 class_of calls and 21 Class::MOP::Class::initialize calls per request, all to read values that have not changed since setup_finalize. The callers are _dispatcher, _engine, _log, _components, _stats_class, __composed_request_class, __composed_response_class and the rest of Catalyst's class data.
How they are found
mk_classdata installs two methods per attribute, $name and _${name}_accessor, sharing one closure. Nothing else in the tree has that shape, so a pair of methods in one package whose bodies are the same coderef and whose names differ by exactly that pattern is a class data accessor. Recognising them structurally rather than by a hardcoded list means a plugin's own mk_classdata attributes are sealed too.
The accessors live in the base classes, Catalyst, Catalyst::Component, Catalyst::Controller, and are shared by every application in the process, so the constant cannot be installed over them. It is installed into each concrete class instead, where it shadows the inherited closure for that class only.
What stays correct
A write unseals. The original accessor goes back into both globs, the write proceeds through it, and that attribute is never sealed again. This is not optional:
Catalyst::preparewritescontext_classand_finalized_psgi_appwrites_psgi_app, both after setup.context_classis pre-warmed at seal time with the same assignmentpreparewould make, so the one attribute Catalyst writes on every first request is already settled and stays sealed.A subclass created after sealing inherits the XSUB but not the answer. The accessor compares the invocant against the class the value was resolved for and delegates to the original when they differ, so a subclass with class data of its own still gets its own.
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)