1414use CodeIgniter \Database \ConnectionInterface ;
1515use CodeIgniter \Model ;
1616use Config \Services ;
17+ use InvalidArgumentException ;
1718
1819/**
1920 * Factories for creating instances.
@@ -51,9 +52,11 @@ class Factories
5152 ];
5253
5354 /**
54- * Mapping of class basenames (no namespace) to
55+ * Mapping of classnames (with or without namespace) to
5556 * their instances.
5657 *
58+ * [component => [name => FQCN]]
59+ *
5760 * @var array<string, array<string, string>>
5861 * @phpstan-var array<string, array<string, class-string>>
5962 */
@@ -72,6 +75,37 @@ class Factories
7275 */
7376 protected static $ instances = [];
7477
78+ /**
79+ * Define the class to load. You can *override* the concrete class.
80+ *
81+ * @param string $component Lowercase, plural component name
82+ * @param string $name Classname. The first parameter of Factories magic method
83+ * @param string $classname FQCN to load
84+ * @phpstan-param class-string $classname FQCN to load
85+ */
86+ public static function define (string $ component , string $ name , string $ classname ): void
87+ {
88+ if (isset (self ::$ basenames [$ component ][$ name ])) {
89+ if (self ::$ basenames [$ component ][$ name ] === $ classname ) {
90+ return ;
91+ }
92+
93+ throw new InvalidArgumentException (
94+ 'Already defined in Factories: ' . $ component . ' ' . $ name . ' -> ' . self ::$ basenames [$ component ][$ name ]
95+ );
96+ }
97+
98+ if (! class_exists ($ classname )) {
99+ throw new InvalidArgumentException ('No such class: ' . $ classname );
100+ }
101+
102+ // Force a configuration to exist for this component.
103+ // Otherwise, getOptions() will reset the component.
104+ self ::getOptions ($ component );
105+
106+ self ::$ basenames [$ component ][$ name ] = $ classname ;
107+ }
108+
75109 /**
76110 * Loads instances based on the method component name. Either
77111 * creates a new instance or returns an existing shared instance.
@@ -88,22 +122,52 @@ public static function __callStatic(string $component, array $arguments)
88122 $ options = array_merge (self ::getOptions (strtolower ($ component )), $ options );
89123
90124 if (! $ options ['getShared ' ]) {
125+ if (isset (self ::$ basenames [$ component ][$ name ])) {
126+ $ class = self ::$ basenames [$ component ][$ name ];
127+
128+ return new $ class (...$ arguments );
129+ }
130+
91131 if ($ class = self ::locateClass ($ options , $ name )) {
92132 return new $ class (...$ arguments );
93133 }
94134
95135 return null ;
96136 }
97137
98- $ basename = self ::getBasename ($ name );
99-
100138 // Check for an existing instance
101- if (isset (self ::$ basenames [$ options ['component ' ]][$ basename ])) {
102- $ class = self ::$ basenames [$ options ['component ' ]][$ basename ];
139+ if (isset (self ::$ basenames [$ options ['component ' ]][$ name ])) {
140+ $ class = self ::$ basenames [$ options ['component ' ]][$ name ];
103141
104142 // Need to verify if the shared instance matches the request
105143 if (self ::verifyInstanceOf ($ options , $ class )) {
144+ if (isset (self ::$ instances [$ options ['component ' ]][$ class ])) {
145+ return self ::$ instances [$ options ['component ' ]][$ class ];
146+ }
147+ self ::$ instances [$ options ['component ' ]][$ class ] = new $ class (...$ arguments );
148+
106149 return self ::$ instances [$ options ['component ' ]][$ class ];
150+
151+ }
152+ }
153+
154+ // Check for an existing Config instance with basename.
155+ if (self ::isConfig ($ options ['component ' ])) {
156+ $ basename = self ::getBasename ($ name );
157+
158+ if (isset (self ::$ basenames [$ options ['component ' ]][$ basename ])) {
159+ $ class = self ::$ basenames [$ options ['component ' ]][$ basename ];
160+
161+ // Need to verify if the shared instance matches the request
162+ if (self ::verifyInstanceOf ($ options , $ class )) {
163+ if (isset (self ::$ instances [$ options ['component ' ]][$ class ])) {
164+ return self ::$ instances [$ options ['component ' ]][$ class ];
165+ }
166+ self ::$ instances [$ options ['component ' ]][$ class ] = new $ class (...$ arguments );
167+
168+ return self ::$ instances [$ options ['component ' ]][$ class ];
169+
170+ }
107171 }
108172 }
109173
@@ -112,8 +176,13 @@ public static function __callStatic(string $component, array $arguments)
112176 return null ;
113177 }
114178
115- self ::$ instances [$ options ['component ' ]][$ class ] = new $ class (...$ arguments );
116- self ::$ basenames [$ options ['component ' ]][$ basename ] = $ class ;
179+ self ::$ instances [$ options ['component ' ]][$ class ] = new $ class (...$ arguments );
180+ self ::$ basenames [$ options ['component ' ]][$ name ] = $ class ;
181+
182+ // If a short classname is specified, also register FQCN to share the instance.
183+ if (! isset (self ::$ basenames [$ options ['component ' ]][$ class ])) {
184+ self ::$ basenames [$ options ['component ' ]][$ class ] = $ class ;
185+ }
117186
118187 return self ::$ instances [$ options ['component ' ]][$ class ];
119188 }
@@ -153,7 +222,9 @@ class_exists($name, false)
153222
154223 // If an App version was requested then see if it verifies
155224 if (
156- $ options ['preferApp ' ] && class_exists ($ appname )
225+ // preferApp is used only for no namespace class or Config class.
226+ (strpos ($ name , '\\' ) === false || self ::isConfig ($ options ['component ' ]))
227+ && $ options ['preferApp ' ] && class_exists ($ appname )
157228 && self ::verifyInstanceOf ($ options , $ name )
158229 ) {
159230 return $ appname ;
@@ -326,8 +397,12 @@ public static function injectMock(string $component, string $name, object $insta
326397 $ class = get_class ($ instance );
327398 $ basename = self ::getBasename ($ name );
328399
329- self ::$ instances [$ component ][$ class ] = $ instance ;
330- self ::$ basenames [$ component ][$ basename ] = $ class ;
400+ self ::$ instances [$ component ][$ class ] = $ instance ;
401+ self ::$ basenames [$ component ][$ name ] = $ class ;
402+
403+ if (self ::isConfig ($ component )) {
404+ self ::$ basenames [$ component ][$ basename ] = $ class ;
405+ }
331406 }
332407
333408 /**
0 commit comments