• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 13938670467

19 Mar 2025 04:07AM UTC coverage: 84.559% (+0.004%) from 84.555%
13938670467

Pull #9476

github

web-flow
Merge c55a047d7 into 1cfe68cc1
Pull Request #9476: fix: Inconsistent directives value between Default and OPCache groups

2 of 2 new or added lines in 1 file covered. (100.0%)

57 existing lines in 10 files now uncovered.

20843 of 24649 relevant lines covered (84.56%)

191.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.33
/system/Config/Factories.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Config;
15

16
use CodeIgniter\Autoloader\FileLocatorInterface;
17
use CodeIgniter\Database\ConnectionInterface;
18
use CodeIgniter\Exceptions\InvalidArgumentException;
19
use CodeIgniter\Model;
20

21
/**
22
 * Factories for creating instances.
23
 *
24
 * Factories allow dynamic loading of components by their path
25
 * and name. The "shared instance" implementation provides a
26
 * large performance boost and helps keep code clean of lengthy
27
 * instantiation checks.
28
 *
29
 * @method static BaseConfig|null config(...$arguments)
30
 * @method static Model|null      models(string $alias, array $options = [], ?ConnectionInterface &$conn = null)
31
 * @see \CodeIgniter\Config\FactoriesTest
32
 */
33
final class Factories
34
{
35
    /**
36
     * Store of component-specific options, usually
37
     * from CodeIgniter\Config\Factory.
38
     *
39
     * @var array<string, array<string, bool|string|null>>
40
     */
41
    private static array $options = [];
42

43
    /**
44
     * Explicit options for the Config
45
     * component to prevent logic loops.
46
     *
47
     * @var array<string, bool|string|null>
48
     */
49
    private static array $configOptions = [
50
        'component'  => 'config',
51
        'path'       => 'Config',
52
        'instanceOf' => null,
53
        'getShared'  => true,
54
        'preferApp'  => true,
55
    ];
56

57
    /**
58
     * Mapping of class aliases to their true Fully Qualified Class Name (FQCN).
59
     *
60
     * Class aliases can be:
61
     *     - FQCN. E.g., 'App\Lib\SomeLib'
62
     *     - short classname. E.g., 'SomeLib'
63
     *     - short classname with sub-directories. E.g., 'Sub/SomeLib'
64
     *
65
     * [component => [alias => FQCN]]
66
     *
67
     * @var array<string, array<string, class-string>>
68
     */
69
    private static array $aliases = [];
70

71
    /**
72
     * Store for instances of any component that
73
     * has been requested as "shared".
74
     *
75
     * A multi-dimensional array with components as
76
     * keys to the array of name-indexed instances.
77
     *
78
     * [component => [FQCN => instance]]
79
     *
80
     * @var array<string, array<class-string, object>>
81
     */
82
    private static array $instances = [];
83

84
    /**
85
     * Whether the component instances are updated?
86
     *
87
     * @var array<string, true> [component => true]
88
     *
89
     * @internal For caching only
90
     */
91
    private static array $updated = [];
92

93
    /**
94
     * Define the class to load. You can *override* the concrete class.
95
     *
96
     * @param string       $component Lowercase, plural component name
97
     * @param string       $alias     Class alias. See the $aliases property.
98
     * @param class-string $classname FQCN to be loaded
99
     */
100
    public static function define(string $component, string $alias, string $classname): void
101
    {
102
        $component = strtolower($component);
6✔
103

104
        if (isset(self::$aliases[$component][$alias])) {
6✔
105
            if (self::$aliases[$component][$alias] === $classname) {
3✔
106
                return;
1✔
107
            }
108

109
            throw new InvalidArgumentException(
2✔
110
                'Already defined in Factories: ' . $component . ' ' . $alias . ' -> ' . self::$aliases[$component][$alias],
2✔
111
            );
2✔
112
        }
113

114
        if (! class_exists($classname)) {
5✔
115
            throw new InvalidArgumentException('No such class: ' . $classname);
1✔
116
        }
117

118
        // Force a configuration to exist for this component.
119
        // Otherwise, getOptions() will reset the component.
120
        self::getOptions($component);
4✔
121

122
        self::$aliases[$component][$alias] = $classname;
4✔
123
        self::$updated[$component]         = true;
4✔
124
    }
125

126
    /**
127
     * Loads instances based on the method component name. Either
128
     * creates a new instance or returns an existing shared instance.
129
     *
130
     * @return object|null
131
     */
132
    public static function __callStatic(string $component, array $arguments)
133
    {
134
        $component = strtolower($component);
6,589✔
135

136
        // First argument is the class alias, second is options
137
        $alias   = trim(array_shift($arguments), '\\ ');
6,589✔
138
        $options = array_shift($arguments) ?? [];
6,589✔
139

140
        // Determine the component-specific options
141
        $options = array_merge(self::getOptions($component), $options);
6,589✔
142

143
        if (! $options['getShared']) {
6,589✔
144
            if (isset(self::$aliases[$options['component']][$alias])) {
82✔
145
                $class = self::$aliases[$options['component']][$alias];
1✔
146

147
                return new $class(...$arguments);
1✔
148
            }
149

150
            // Try to locate the class
151
            $class = self::locateClass($options, $alias);
81✔
152
            if ($class !== null) {
81✔
153
                return new $class(...$arguments);
77✔
154
            }
155

156
            return null;
4✔
157
        }
158

159
        // Check for an existing definition
160
        $instance = self::getDefinedInstance($options, $alias, $arguments);
6,589✔
161
        if ($instance !== null) {
6,589✔
162
            return $instance;
76✔
163
        }
164

165
        // Try to locate the class
166
        if (($class = self::locateClass($options, $alias)) === null) {
6,589✔
167
            return null;
4✔
168
        }
169

170
        self::createInstance($options['component'], $class, $arguments);
6,589✔
171
        self::setAlias($options['component'], $alias, $class);
6,589✔
172

173
        return self::$instances[$options['component']][$class];
6,589✔
174
    }
175

176
    /**
177
     * Simple method to get the shared instance fast.
178
     */
179
    public static function get(string $component, string $alias): ?object
180
    {
181
        if (isset(self::$aliases[$component][$alias])) {
6,647✔
182
            $class = self::$aliases[$component][$alias];
6,645✔
183

184
            if (isset(self::$instances[$component][$class])) {
6,645✔
185
                return self::$instances[$component][$class];
6,645✔
186
            }
187
        }
188

189
        return self::__callStatic($component, [$alias]);
6,589✔
190
    }
191

192
    /**
193
     * Gets the defined instance. If not exists, creates new one.
194
     *
195
     * @return object|null
196
     */
197
    private static function getDefinedInstance(array $options, string $alias, array $arguments)
198
    {
199
        // The alias is already defined.
200
        if (isset(self::$aliases[$options['component']][$alias])) {
6,589✔
201
            $class = self::$aliases[$options['component']][$alias];
13✔
202

203
            // Need to verify if the shared instance matches the request
204
            if (self::verifyInstanceOf($options, $class)) {
13✔
205
                // Check for an existing instance
206
                if (isset(self::$instances[$options['component']][$class])) {
12✔
207
                    return self::$instances[$options['component']][$class];
9✔
208
                }
209

210
                self::createInstance($options['component'], $class, $arguments);
3✔
211

212
                return self::$instances[$options['component']][$class];
3✔
213
            }
214
        }
215

216
        // Try to locate the class
217
        if (($class = self::locateClass($options, $alias)) === null) {
6,589✔
218
            return null;
4✔
219
        }
220

221
        // Check for an existing instance for the class
222
        if (isset(self::$instances[$options['component']][$class])) {
6,589✔
223
            self::setAlias($options['component'], $alias, $class);
64✔
224

225
            return self::$instances[$options['component']][$class];
64✔
226
        }
227

228
        return null;
6,589✔
229
    }
230

231
    /**
232
     * Creates the shared instance.
233
     */
234
    private static function createInstance(string $component, string $class, array $arguments): void
235
    {
236
        self::$instances[$component][$class] = new $class(...$arguments);
6,589✔
237
        self::$updated[$component]           = true;
6,589✔
238
    }
239

240
    /**
241
     * Sets alias
242
     */
243
    private static function setAlias(string $component, string $alias, string $class): void
244
    {
245
        self::$aliases[$component][$alias] = $class;
6,589✔
246
        self::$updated[$component]         = true;
6,589✔
247

248
        // If a short classname is specified, also register FQCN to share the instance.
249
        if (! isset(self::$aliases[$component][$class]) && ! self::isNamespaced($alias)) {
6,589✔
250
            self::$aliases[$component][$class] = $class;
197✔
251
        }
252
    }
253

254
    /**
255
     * Is the component Config?
256
     *
257
     * @param string $component Lowercase, plural component name
258
     */
259
    private static function isConfig(string $component): bool
260
    {
261
        return $component === 'config';
6,589✔
262
    }
263

264
    /**
265
     * Finds a component class
266
     *
267
     * @param array  $options The array of component-specific directives
268
     * @param string $alias   Class alias. See the $aliases property.
269
     */
270
    private static function locateClass(array $options, string $alias): ?string
271
    {
272
        // Check for low-hanging fruit
273
        if (
274
            class_exists($alias, false)
6,589✔
275
            && self::verifyPreferApp($options, $alias)
6,589✔
276
            && self::verifyInstanceOf($options, $alias)
6,589✔
277
        ) {
278
            return $alias;
6,589✔
279
        }
280

281
        // Determine the relative class names we need
282
        $basename = self::getBasename($alias);
473✔
283
        $appname  = self::isConfig($options['component'])
473✔
284
            ? 'Config\\' . $basename
469✔
285
            : rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
99✔
286

287
        // If an App version was requested then see if it verifies
288
        if (
289
            // preferApp is used only for no namespaced class.
290
            ! self::isNamespaced($alias)
473✔
291
            && $options['preferApp'] && class_exists($appname)
473✔
292
            && self::verifyInstanceOf($options, $alias)
473✔
293
        ) {
294
            return $appname;
154✔
295
        }
296

297
        // If we have ruled out an App version and the class exists then try it
298
        if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) {
375✔
299
            return $alias;
349✔
300
        }
301

302
        // Have to do this the hard way...
303
        /** @var FileLocatorInterface */
304
        $locator = service('locator');
110✔
305

306
        // Check if the class alias was namespaced
307
        if (self::isNamespaced($alias)) {
110✔
308
            if (! $file = $locator->locateFile($alias, $options['path'])) {
3✔
309
                return null;
3✔
310
            }
UNCOV
311
            $files = [$file];
×
312
        }
313
        // No namespace? Search for it
314
        // Check all namespaces, prioritizing App and modules
315
        elseif (($files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $alias)) === []) {
110✔
316
            return null;
3✔
317
        }
318

319
        // Check all files for a valid class
320
        foreach ($files as $file) {
108✔
321
            $class = $locator->findQualifiedNameFromPath($file);
108✔
322

323
            if ($class !== false && self::verifyInstanceOf($options, $class)) {
108✔
324
                return $class;
108✔
325
            }
326
        }
327

328
        return null;
2✔
329
    }
330

331
    /**
332
     * Is the class alias namespaced or not?
333
     *
334
     * @param string $alias Class alias. See the $aliases property.
335
     */
336
    private static function isNamespaced(string $alias): bool
337
    {
338
        return str_contains($alias, '\\');
860✔
339
    }
340

341
    /**
342
     * Verifies that a class & config satisfy the "preferApp" option
343
     *
344
     * @param array  $options The array of component-specific directives
345
     * @param string $alias   Class alias. See the $aliases property.
346
     */
347
    private static function verifyPreferApp(array $options, string $alias): bool
348
    {
349
        // Anything without that restriction passes
350
        if (! $options['preferApp']) {
6,589✔
351
            return true;
1✔
352
        }
353

354
        // Special case for Config since its App namespace is actually \Config
355
        if (self::isConfig($options['component'])) {
6,589✔
356
            return str_starts_with($alias, 'Config');
6,589✔
357
        }
358

359
        return str_starts_with($alias, APP_NAMESPACE);
69✔
360
    }
361

362
    /**
363
     * Verifies that a class & config satisfy the "instanceOf" option
364
     *
365
     * @param array  $options The array of component-specific directives
366
     * @param string $alias   Class alias. See the $aliases property.
367
     */
368
    private static function verifyInstanceOf(array $options, string $alias): bool
369
    {
370
        // Anything without that restriction passes
371
        if (! $options['instanceOf']) {
6,589✔
372
            return true;
6,589✔
373
        }
374

375
        return is_a($alias, $options['instanceOf'], true);
2✔
376
    }
377

378
    /**
379
     * Returns the component-specific configuration
380
     *
381
     * @param string $component Lowercase, plural component name
382
     *
383
     * @return array<string, bool|string|null>
384
     *
385
     * @internal For testing only
386
     * @testTag
387
     */
388
    public static function getOptions(string $component): array
389
    {
390
        $component = strtolower($component);
6,589✔
391

392
        // Check for a stored version
393
        if (isset(self::$options[$component])) {
6,589✔
394
            return self::$options[$component];
6,589✔
395
        }
396

397
        $values = self::isConfig($component)
6,585✔
398
            // Handle Config as a special case to prevent logic loops
6,585✔
399
            ? self::$configOptions
6,585✔
400
            // Load values from the best Factory configuration (will include Registrars)
6,585✔
401
            : config('Factory')->{$component} ?? [];
105✔
402

403
        // The setOptions() reset the component. So getOptions() may reset
404
        // the component.
405
        return self::setOptions($component, $values);
6,585✔
406
    }
407

408
    /**
409
     * Normalizes, stores, and returns the configuration for a specific component
410
     *
411
     * @param string $component Lowercase, plural component name
412
     * @param array  $values    option values
413
     *
414
     * @return array<string, bool|string|null> The result after applying defaults and normalization
415
     */
416
    public static function setOptions(string $component, array $values): array
417
    {
418
        $component = strtolower($component);
6,585✔
419

420
        // Allow the config to replace the component name, to support "aliases"
421
        $values['component'] = strtolower($values['component'] ?? $component);
6,585✔
422

423
        // Reset this component so instances can be rediscovered with the updated config
424
        self::reset($values['component']);
6,585✔
425

426
        // If no path was available then use the component
427
        $values['path'] = trim($values['path'] ?? ucfirst($values['component']), '\\ ');
6,585✔
428

429
        // Add defaults for any missing values
430
        $values = array_merge(Factory::$default, $values);
6,585✔
431

432
        // Store the result to the supplied name and potential alias
433
        self::$options[$component]           = $values;
6,585✔
434
        self::$options[$values['component']] = $values;
6,585✔
435

436
        return $values;
6,585✔
437
    }
438

439
    /**
440
     * Resets the static arrays, optionally just for one component
441
     *
442
     * @param string|null $component Lowercase, plural component name
443
     *
444
     * @return void
445
     */
446
    public static function reset(?string $component = null)
447
    {
448
        if ($component !== null) {
6,585✔
449
            unset(
6,585✔
450
                self::$options[$component],
6,585✔
451
                self::$aliases[$component],
6,585✔
452
                self::$instances[$component],
6,585✔
453
                self::$updated[$component],
6,585✔
454
            );
6,585✔
455

456
            return;
6,585✔
457
        }
458

459
        self::$options   = [];
6,585✔
460
        self::$aliases   = [];
6,585✔
461
        self::$instances = [];
6,585✔
462
        self::$updated   = [];
6,585✔
463
    }
464

465
    /**
466
     * Helper method for injecting mock instances
467
     *
468
     * @param string $component Lowercase, plural component name
469
     * @param string $alias     Class alias. See the $aliases property.
470
     *
471
     * @return void
472
     *
473
     * @internal For testing only
474
     * @testTag
475
     */
476
    public static function injectMock(string $component, string $alias, object $instance)
477
    {
478
        $component = strtolower($component);
480✔
479

480
        // Force a configuration to exist for this component
481
        self::getOptions($component);
480✔
482

483
        $class = $instance::class;
480✔
484

485
        self::$instances[$component][$class] = $instance;
480✔
486
        self::$aliases[$component][$alias]   = $class;
480✔
487

488
        if (self::isConfig($component)) {
480✔
489
            if (self::isNamespaced($alias)) {
476✔
490
                self::$aliases[$component][self::getBasename($alias)] = $class;
32✔
491
            } else {
492
                self::$aliases[$component]['Config\\' . $alias] = $class;
448✔
493
            }
494
        }
495
    }
496

497
    /**
498
     * Gets a basename from a class alias, namespaced or not.
499
     *
500
     * @internal For testing only
501
     * @testTag
502
     */
503
    public static function getBasename(string $alias): string
504
    {
505
        // Determine the basename
506
        if ($basename = strrchr($alias, '\\')) {
505✔
507
            return substr($basename, 1);
383✔
508
        }
509

510
        return $alias;
263✔
511
    }
512

513
    /**
514
     * Gets component data for caching.
515
     *
516
     * @internal For caching only
517
     */
518
    public static function getComponentInstances(string $component): array
519
    {
520
        if (! isset(self::$aliases[$component])) {
6✔
521
            return [
1✔
522
                'options'   => [],
1✔
523
                'aliases'   => [],
1✔
524
                'instances' => [],
1✔
525
            ];
1✔
526
        }
527

528
        return [
6✔
529
            'options'   => self::$options[$component],
6✔
530
            'aliases'   => self::$aliases[$component],
6✔
531
            'instances' => self::$instances[$component],
6✔
532
        ];
6✔
533
    }
534

535
    /**
536
     * Sets component data
537
     *
538
     * @internal For caching only
539
     */
540
    public static function setComponentInstances(string $component, array $data): void
541
    {
542
        self::$options[$component]   = $data['options'];
4✔
543
        self::$aliases[$component]   = $data['aliases'];
4✔
544
        self::$instances[$component] = $data['instances'];
4✔
545

546
        unset(self::$updated[$component]);
4✔
547
    }
548

549
    /**
550
     * Whether the component instances are updated?
551
     *
552
     * @internal For caching only
553
     */
554
    public static function isUpdated(string $component): bool
555
    {
556
        return isset(self::$updated[$component]);
5✔
557
    }
558
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc