@@ -1058,6 +1058,120 @@ return local.$wheels;
10581058 return local .rv ;
10591059 }
10601060
1061+ /**
1062+ * Internal. Returns a cached "integration plan" for a folder of mixin
1063+ * components (e.g. `wheels.model`, `wheels.controller`, `wheels.mapper`): an
1064+ * ordered array of `{instance, methods, fullName}` where `instance` is a
1065+ * single shared, stateless method-holder component and `methods` is its
1066+ * `getMetaData().functions` array.
1067+ *
1068+ * The directory scan, the per-file `createObject`, and the `getMetaData`
1069+ * calls are the expensive — and completely invariant — part of
1070+ * `$integrateComponents`: they produce the same result for every object of a
1071+ * given type. Before this cache they were re-paid on EVERY model, controller,
1072+ * and mapper materialization (every `new()` and every finder row goes through
1073+ * `$createInstance` -> `init()` -> `$integrateComponents`), which dominated
1074+ * test-suite and request time (issue #3213). Now they run once per path and
1075+ * the cheap per-instance work (copying function references into the target's
1076+ * `variables`/`this`) is all that remains on the hot path.
1077+ *
1078+ * The plan is cached in `application.wheels.integrationPlans`, so a reload —
1079+ * which rebuilds `application.wheels` — re-scans, the same lifetime contract
1080+ * as the schema column cache. The cached method-holder components carry no
1081+ * instance state (they are never `init()`'d) and CFML methods bind to the
1082+ * object they are invoked on, so sharing their function references across many
1083+ * target instances and across concurrent requests is safe.
1084+ */
1085+ public array function $componentIntegrationPlan (required string path ) {
1086+ // During early bootstrap (before application.wheels exists) fall back to
1087+ // an uncached build so behavior is identical to the pre-cache code path.
1088+ if (! StructKeyExists (application , " wheels" )) {
1089+ return $buildComponentIntegrationPlan (arguments .path );
1090+ }
1091+ if (! StructKeyExists (application .wheels , " integrationPlans" )) {
1092+ lock name = " wheels.integrationPlans.#application .applicationName #" type = " exclusive" timeout = " 10" {
1093+ if (! StructKeyExists (application .wheels , " integrationPlans" )) {
1094+ application .wheels .integrationPlans = {};
1095+ }
1096+ }
1097+ }
1098+ if (! StructKeyExists (application .wheels .integrationPlans , arguments .path )) {
1099+ local .plan = $buildComponentIntegrationPlan (arguments .path );
1100+ lock name = " wheels.integrationPlans.#application .applicationName #" type = " exclusive" timeout = " 10" {
1101+ application .wheels .integrationPlans [arguments .path ] = local .plan ;
1102+ }
1103+ }
1104+ return application .wheels .integrationPlans [arguments .path ];
1105+ }
1106+
1107+ /**
1108+ * Internal. Builds (without caching) the integration plan for a path — the
1109+ * directory scan + per-file createObject + getMetaData that
1110+ * $componentIntegrationPlan memoizes. The DirectoryList call mirrors the
1111+ * original $integrateComponents exactly so file (and therefore override)
1112+ * order is unchanged.
1113+ */
1114+ public array function $buildComponentIntegrationPlan (required string path ) {
1115+ local .folderPath = ExpandPath (" /#Replace (arguments .path , " ." , " /" , " all" ) #" );
1116+ local .fileList = DirectoryList (local .folderPath , false , " name" , " *.cfc" );
1117+ local .rv = [];
1118+ for (local .fileName in local .fileList ) {
1119+ local .componentName = Replace (local .fileName , " .cfc" , " " , " all" );
1120+ local .instance = CreateObject (" component" , " #arguments .path #.#local .componentName #" );
1121+ local .meta = GetMetaData (local .instance );
1122+ local .fns = StructKeyExists (local .meta , " functions" ) ? local .meta .functions : [];
1123+ // Pre-resolve the PUBLIC method references once. On the hot path
1124+ // (every materialized object) this removes both the per-method
1125+ // `.access` filtering and the `instance[name]` scope lookup; only the
1126+ // reference assignment into the target remains (issue #3213). Function
1127+ // references are late-bound to the object they are invoked on, so the
1128+ // shared, cached reference works correctly on every target instance.
1129+ local .publicMethods = [];
1130+ local .fEnd = ArrayLen (local .fns );
1131+ for (local .f = 1 ; local .f <= local .fEnd ; local .f ++ ) {
1132+ if (local .fns [local .f ].access == " public" ) {
1133+ ArrayAppend (local .publicMethods , {
1134+ name = local .fns [local .f ].name ,
1135+ ref = local .instance [local .fns [local .f ].name ]
1136+ });
1137+ }
1138+ }
1139+ ArrayAppend (local .rv , {
1140+ instance = local .instance ,
1141+ methods = local .fns ,
1142+ publicMethods = local .publicMethods ,
1143+ fullName = StructKeyExists (local .meta , " fullName" ) ? local .meta .fullName : " #arguments .path #.#local .componentName #"
1144+ });
1145+ }
1146+ return local .rv ;
1147+ }
1148+
1149+ /**
1150+ * Internal. Returns a struct whose KEYS are the function names that a
1151+ * registered plugin/package mixin will override for the given component type
1152+ * (plus the always-checked "global" type). Empty — the common case, no mixins
1153+ * registered — when there are none. Computed from the app-scoped, reload-stable
1154+ * application.wheels.mixins so the per-method $willBeOverriddenByMixin function
1155+ * call can be replaced by an O(1) struct-membership test on the hot path (#3213).
1156+ */
1157+ public struct function $mixinOverrideSet (required string primaryType ) {
1158+ local .rv = {};
1159+ if (
1160+ ! StructKeyExists (application , " wheels" )
1161+ || ! StructKeyExists (application .wheels , " mixins" )
1162+ || StructIsEmpty (application .wheels .mixins )
1163+ ) {
1164+ return local .rv ;
1165+ }
1166+ local .types = [arguments .primaryType , " global" ];
1167+ for (local .t in local .types ) {
1168+ if (StructKeyExists (application .wheels .mixins , local .t ) && IsStruct (application .wheels .mixins [local .t ])) {
1169+ StructAppend (local .rv , application .wheels .mixins [local .t ], false );
1170+ }
1171+ }
1172+ return local .rv ;
1173+ }
1174+
10611175 /**
10621176 * Internal function.
10631177 */
0 commit comments