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

codeigniter4 / CodeIgniter4 / 28519386026

01 Jul 2026 01:00PM UTC coverage: 88.704% (-0.003%) from 88.707%
28519386026

Pull #10364

github

web-flow
Merge 223e98b79 into 01da624a8
Pull Request #10364: fix: satisfy PHPStan 2.2.3

4 of 5 new or added lines in 1 file covered. (80.0%)

22271 of 25107 relevant lines covered (88.7%)

214.1 hits per line

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

97.5
/system/Common.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
use CodeIgniter\Cache\CacheInterface;
15
use CodeIgniter\Config\BaseConfig;
16
use CodeIgniter\Config\Factories;
17
use CodeIgniter\Cookie\Cookie;
18
use CodeIgniter\Cookie\CookieStore;
19
use CodeIgniter\Cookie\Exceptions\CookieException;
20
use CodeIgniter\Database\BaseConnection;
21
use CodeIgniter\Database\ConnectionInterface;
22
use CodeIgniter\Debug\Timer;
23
use CodeIgniter\Exceptions\InvalidArgumentException;
24
use CodeIgniter\Exceptions\RuntimeException;
25
use CodeIgniter\Files\Exceptions\FileNotFoundException;
26
use CodeIgniter\HTTP\CLIRequest;
27
use CodeIgniter\HTTP\Exceptions\HTTPException;
28
use CodeIgniter\HTTP\Exceptions\RedirectException;
29
use CodeIgniter\HTTP\IncomingRequest;
30
use CodeIgniter\HTTP\RedirectResponse;
31
use CodeIgniter\HTTP\RequestInterface;
32
use CodeIgniter\HTTP\ResponseInterface;
33
use CodeIgniter\Language\Language;
34
use CodeIgniter\Model;
35
use CodeIgniter\Session\Session;
36
use CodeIgniter\Test\TestLogger;
37
use Config\App;
38
use Config\Database;
39
use Config\DocTypes;
40
use Config\Logger;
41
use Config\Services;
42
use Config\View;
43
use Laminas\Escaper\Escaper;
44

45
// Services Convenience Functions
46

47
if (! function_exists('app_timezone')) {
48
    /**
49
     * Returns the timezone the application has been set to display
50
     * dates in. This might be different than the timezone set
51
     * at the server level, as you often want to stores dates in UTC
52
     * and convert them on the fly for the user.
53
     */
54
    function app_timezone(): string
55
    {
56
        $config = config(App::class);
5✔
57

58
        return $config->appTimezone;
5✔
59
    }
60
}
61

62
if (! function_exists('cache')) {
63
    /**
64
     * A convenience method that provides access to the Cache
65
     * object. If no parameter is provided, will return the object,
66
     * otherwise, will attempt to return the cached value.
67
     *
68
     * Examples:
69
     *    cache()->save('foo', 'bar');
70
     *    $foo = cache('bar');
71
     *
72
     * @return ($key is null ? CacheInterface : mixed)
73
     */
74
    function cache(?string $key = null)
75
    {
76
        $cache = service('cache');
20✔
77

78
        // No params - return cache object
79
        if ($key === null) {
20✔
80
            return $cache;
20✔
81
        }
82

83
        // Still here? Retrieve the value.
84
        return $cache->get($key);
12✔
85
    }
86
}
87

88
if (! function_exists('clean_path')) {
89
    /**
90
     * A convenience method to clean paths for
91
     * a nicer looking output. Useful for exception
92
     * handling, error logging, etc.
93
     */
94
    function clean_path(string $path): string
95
    {
96
        // Resolve relative paths
97
        try {
98
            $path = realpath($path) ?: $path;
183✔
99
        } catch (ErrorException|ValueError) {
×
100
            $path = 'error file path: ' . urlencode($path);
×
101
        }
102

103
        return match (true) {
104
            str_starts_with($path, APPPATH)                             => 'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH)),
183✔
105
            str_starts_with($path, SYSTEMPATH)                          => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH)),
107✔
106
            str_starts_with($path, FCPATH)                              => 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH)),
96✔
107
            defined('VENDORPATH') && str_starts_with($path, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH)),
95✔
108
            str_starts_with($path, ROOTPATH)                            => 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH)),
93✔
109
            default                                                     => $path,
183✔
110
        };
111
    }
112
}
113

114
if (! function_exists('command')) {
115
    /**
116
     * Runs a single command.
117
     * Input expected in a single string as would
118
     * be used on the command line itself:
119
     *
120
     *  > command('migrate:create SomeMigration');
121
     *
122
     * @return false|string
123
     */
124
    function command(string $command)
125
    {
126
        $regexString = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
170✔
127
        $regexQuoted = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
170✔
128

129
        $args   = [];
170✔
130
        $length = strlen($command);
170✔
131
        $cursor = 0;
170✔
132

133
        /**
134
         * Adopted from Symfony's `StringInput::tokenize()` with few changes.
135
         *
136
         * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Input/StringInput.php
137
         */
138
        while ($cursor < $length) {
170✔
139
            if (preg_match('/\s+/A', $command, $match, 0, $cursor)) {
170✔
140
                // nothing to do
141
            } elseif (preg_match('/' . $regexQuoted . '/A', $command, $match, 0, $cursor)) {
170✔
142
                $args[] = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2));
×
143
            } elseif (preg_match('/' . $regexString . '/A', $command, $match, 0, $cursor)) {
170✔
144
                $args[] = stripcslashes($match[1]);
170✔
145
            } else {
146
                // @codeCoverageIgnoreStart
147
                throw new InvalidArgumentException(sprintf(
148
                    'Unable to parse input near "... %s ...".',
149
                    substr($command, $cursor, 10),
150
                ));
151
                // @codeCoverageIgnoreEnd
152
            }
153

154
            $cursor += strlen($match[0]);
170✔
155
        }
156

157
        /** @var array<int|string, string|null> */
158
        $params      = [];
170✔
159
        $command     = array_shift($args);
170✔
160
        $optionValue = false;
170✔
161

162
        foreach ($args as $i => $arg) {
170✔
163
            if (mb_strpos($arg, '-') !== 0) {
135✔
164
                if ($optionValue) {
113✔
165
                    // if this was an option value, it was already
166
                    // included in the previous iteration
167
                    $optionValue = false;
50✔
168
                } else {
169
                    // add to segments if not starting with '-'
170
                    // and not an option value
171
                    $params[] = $arg;
84✔
172
                }
173

174
                continue;
113✔
175
            }
176

177
            $arg   = ltrim($arg, '-');
91✔
178
            $value = null;
91✔
179

180
            if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
91✔
181
                $value       = $args[$i + 1];
50✔
182
                $optionValue = true;
50✔
183
            }
184

185
            $params[$arg] = $value;
91✔
186
        }
187

188
        $bufferLevel = ob_get_level();
170✔
189

190
        try {
191
            ob_start();
170✔
192
            service('commands')->run($command, $params);
170✔
193

194
            if (ob_get_level() <= $bufferLevel) {
168✔
NEW
195
                return false;
×
196
            }
197

198
            return ob_get_contents();
168✔
199
        } finally {
200
            while (ob_get_level() > $bufferLevel) {
170✔
201
                ob_end_clean();
170✔
202
            }
203
        }
204
    }
205
}
206

207
if (! function_exists('config')) {
208
    /**
209
     * More simple way of getting config instances from Factories
210
     *
211
     * @template ConfigTemplate of BaseConfig
212
     *
213
     * @param class-string<ConfigTemplate>|string $name
214
     *
215
     * @return ($name is class-string<ConfigTemplate> ? ConfigTemplate : object|null)
216
     */
217
    function config(string $name, bool $getShared = true)
218
    {
219
        if ($getShared) {
7,475✔
220
            return Factories::get('config', $name);
7,475✔
221
        }
222

223
        return Factories::config($name, ['getShared' => $getShared]);
×
224
    }
225
}
226

227
if (! function_exists('cookie')) {
228
    /**
229
     * Simpler way to create a new Cookie instance.
230
     *
231
     * @param string $name  Name of the cookie
232
     * @param string $value Value of the cookie
233
     * @param array{
234
     *     prefix?: string,
235
     *     max-age?: int|numeric-string,
236
     *     expires?: DateTimeInterface|int|string,
237
     *     path?: string,
238
     *     domain?: string,
239
     *     secure?: bool,
240
     *     httponly?: bool,
241
     *     samesite?: string,
242
     *     raw?: bool
243
     * } $options Cookie configuration options
244
     *
245
     * @throws CookieException
246
     */
247
    function cookie(string $name, string $value = '', array $options = []): Cookie
248
    {
249
        return new Cookie($name, $value, $options);
×
250
    }
251
}
252

253
if (! function_exists('cookies')) {
254
    /**
255
     * Fetches the global `CookieStore` instance held by `Response`.
256
     *
257
     * @param list<Cookie> $cookies   If `getGlobal` is false, this is passed to CookieStore's constructor
258
     * @param bool         $getGlobal If false, creates a new instance of CookieStore
259
     */
260
    function cookies(array $cookies = [], bool $getGlobal = true): CookieStore
261
    {
262
        if ($getGlobal) {
1✔
263
            return service('response')->getCookieStore();
1✔
264
        }
265

266
        return new CookieStore($cookies);
1✔
267
    }
268
}
269

270
if (! function_exists('csrf_token')) {
271
    /**
272
     * Returns the CSRF token name.
273
     * Can be used in Views when building hidden inputs manually,
274
     * or used in javascript vars when using APIs.
275
     */
276
    function csrf_token(): string
277
    {
278
        return service('security')->getTokenName();
7✔
279
    }
280
}
281

282
if (! function_exists('csrf_header')) {
283
    /**
284
     * Returns the CSRF header name.
285
     * Can be used in Views by adding it to the meta tag
286
     * or used in javascript to define a header name when using APIs.
287
     */
288
    function csrf_header(): string
289
    {
290
        return service('security')->getHeaderName();
2✔
291
    }
292
}
293

294
if (! function_exists('csrf_hash')) {
295
    /**
296
     * Returns the current hash value for the CSRF protection.
297
     * Can be used in Views when building hidden inputs manually,
298
     * or used in javascript vars for API usage.
299
     */
300
    function csrf_hash(): string
301
    {
302
        return service('security')->getHash();
8✔
303
    }
304
}
305

306
if (! function_exists('csrf_field')) {
307
    /**
308
     * Generates a hidden input field for use within manually generated forms.
309
     *
310
     * @param non-empty-string|null $id
311
     */
312
    function csrf_field(?string $id = null): string
313
    {
314
        return '<input type="hidden"' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_token() . '" value="' . csrf_hash() . '"' . _solidus() . '>';
6✔
315
    }
316
}
317

318
if (! function_exists('csrf_meta')) {
319
    /**
320
     * Generates a meta tag for use within javascript calls.
321
     *
322
     * @param non-empty-string|null $id
323
     */
324
    function csrf_meta(?string $id = null): string
325
    {
326
        return '<meta' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_header() . '" content="' . csrf_hash() . '"' . _solidus() . '>';
1✔
327
    }
328
}
329

330
if (! function_exists('csp_style_nonce')) {
331
    /**
332
     * Generates a nonce attribute for style tag.
333
     */
334
    function csp_style_nonce(): string
335
    {
336
        $csp = service('csp');
4✔
337

338
        if (! $csp->enabled()) {
4✔
339
            return '';
2✔
340
        }
341

342
        return 'nonce="' . $csp->getStyleNonce() . '"';
2✔
343
    }
344
}
345

346
if (! function_exists('csp_script_nonce')) {
347
    /**
348
     * Generates a nonce attribute for script tag.
349
     */
350
    function csp_script_nonce(): string
351
    {
352
        $csp = service('csp');
24✔
353

354
        if (! $csp->enabled()) {
24✔
355
            return '';
20✔
356
        }
357

358
        return 'nonce="' . $csp->getScriptNonce() . '"';
4✔
359
    }
360
}
361

362
if (! function_exists('db_connect')) {
363
    /**
364
     * Grabs a database connection and returns it to the user.
365
     *
366
     * This is a convenience wrapper for \Config\Database::connect()
367
     * and supports the same parameters. Namely:
368
     *
369
     * When passing in $db, you may pass any of the following to connect:
370
     * - group name
371
     * - existing connection instance
372
     * - array of database configuration values
373
     *
374
     * If $getShared === false then a new connection instance will be provided,
375
     * otherwise it will all calls will return the same instance.
376
     *
377
     * @param array{
378
     *     DSN?: string,
379
     *     hostname?: string,
380
     *     username?: string,
381
     *     password?: string,
382
     *     database?: string,
383
     *     DBDriver?: 'MySQLi'|'OCI8'|'Postgre'|'SQLite3'|'SQLSRV',
384
     *     DBPrefix?: string,
385
     *     pConnect?: bool,
386
     *     DBDebug?: bool,
387
     *     charset?: string,
388
     *     DBCollat?: string,
389
     *     swapPre?: string,
390
     *     encrypt?: bool,
391
     *     compress?: bool,
392
     *     strictOn?: bool,
393
     *     failover?: array<string, mixed>,
394
     *     port?: int,
395
     *     dateFormat?: array<string, string>,
396
     *     foreignKeys?: bool
397
     * }|ConnectionInterface|string|null $db
398
     *
399
     * @return BaseConnection
400
     */
401
    function db_connect($db = null, bool $getShared = true)
402
    {
403
        return Database::connect($db, $getShared);
841✔
404
    }
405
}
406

407
if (! function_exists('env')) {
408
    /**
409
     * Allows user to retrieve values from the environment
410
     * variables that have been set. Especially useful for
411
     * retrieving values set from the .env file for
412
     * use in config files.
413
     *
414
     * @param mixed $default
415
     *
416
     * @return mixed
417
     */
418
    function env(string $key, $default = null)
419
    {
420
        $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
941✔
421

422
        // Not found? Return the default value
423
        if ($value === false) {
941✔
424
            return $default;
10✔
425
        }
426

427
        // Non-string values (e.g. $_SERVER['argc'] is int, $_SERVER['argv'] is array in CLI)
428
        // must be returned as-is to avoid TypeError from strtolower().
429
        if (! is_string($value)) {
935✔
430
            return $value;
7✔
431
        }
432

433
        // Handle any boolean values
434
        return match (strtolower($value)) {
928✔
435
            'true'  => true,
1✔
436
            'false' => false,
1✔
437
            'empty' => '',
1✔
438
            'null'  => null,
1✔
439
            default => $value,
928✔
440
        };
928✔
441
    }
442
}
443

444
if (! function_exists('esc')) {
445
    /**
446
     * Performs simple auto-escaping of data for security reasons.
447
     * Might consider making this more complex at a later date.
448
     *
449
     * If $data is a string, then it simply escapes and returns it.
450
     * If $data is an array, then it loops over it, escaping each
451
     * 'value' of the key/value pairs.
452
     *
453
     * @param array<int|string, array<int|string, mixed>|string>|string $data
454
     * @param 'attr'|'css'|'html'|'js'|'raw'|'url'                      $context
455
     * @param string|null                                               $encoding Current encoding for escaping.
456
     *                                                                            If not UTF-8, we convert strings from this encoding
457
     *                                                                            pre-escaping and back to this encoding post-escaping.
458
     *
459
     * @return ($data is string ? string : array<int|string, array<int|string, mixed>|string>)
460
     *
461
     * @throws InvalidArgumentException
462
     */
463
    function esc($data, string $context = 'html', ?string $encoding = null)
464
    {
465
        $context = strtolower($context);
784✔
466

467
        // Provide a way to NOT escape data since
468
        // this could be called automatically by
469
        // the View library.
470
        if ($context === 'raw') {
784✔
471
            return $data;
81✔
472
        }
473

474
        if (is_array($data)) {
717✔
475
            foreach ($data as &$value) {
11✔
476
                $value = esc($value, $context, $encoding);
11✔
477
            }
478

479
            return $data;
9✔
480
        }
481

482
        if (is_string($data)) {
717✔
483
            if (! in_array($context, ['html', 'js', 'css', 'url', 'attr'], true)) {
715✔
484
                throw new InvalidArgumentException('Invalid escape context provided.');
2✔
485
            }
486

487
            $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);
713✔
488

489
            static $escapers = [];
713✔
490
            $cacheKey        = strtolower($encoding ?? 'utf-8');
713✔
491

492
            if (! isset($escapers[$cacheKey])) {
713✔
493
                $escapers[$cacheKey] = new Escaper($encoding);
2✔
494
            }
495

496
            $data = $escapers[$cacheKey]->{$method}($data);
712✔
497
        }
498

499
        return $data;
714✔
500
    }
501
}
502

503
if (! function_exists('force_https')) {
504
    /**
505
     * Used to force a page to be accessed in via HTTPS.
506
     * Uses a standard redirect, plus will set the HSTS header
507
     * for modern browsers that support, which gives best
508
     * protection against man-in-the-middle attacks.
509
     *
510
     * @see https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
511
     *
512
     * @param int $duration How long should the SSL header be set for? (in seconds)
513
     *                      Defaults to 1 year.
514
     *
515
     * @throws HTTPException
516
     * @throws RedirectException
517
     */
518
    function force_https(
519
        int $duration = 31_536_000,
520
        ?RequestInterface $request = null,
521
        ?ResponseInterface $response = null,
522
    ): void {
523
        $request ??= service('request');
5✔
524

525
        if (! $request instanceof IncomingRequest) {
5✔
526
            return;
×
527
        }
528

529
        $response ??= service('response');
5✔
530

531
        if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure()))
532
            || $request->getServer('HTTPS') === 'test'
5✔
533
        ) {
534
            return; // @codeCoverageIgnore
535
        }
536

537
        // If the session status is active, we should regenerate
538
        // the session ID for safety sake.
539
        if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) {
4✔
540
            service('session')->regenerate(); // @codeCoverageIgnore
541
        }
542

543
        $uri = $request->getUri()->withScheme('https');
4✔
544

545
        // Set an HSTS header
546
        $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration)
4✔
547
            ->redirect((string) $uri)
4✔
548
            ->setStatusCode(307)
4✔
549
            ->setBody('')
4✔
550
            ->getCookieStore()
4✔
551
            ->clear();
4✔
552

553
        throw new RedirectException($response);
4✔
554
    }
555
}
556

557
if (! function_exists('function_usable')) {
558
    /**
559
     * Function usable
560
     *
561
     * Executes a function_exists() check, and if the Suhosin PHP
562
     * extension is loaded - checks whether the function that is
563
     * checked might be disabled in there as well.
564
     *
565
     * This is useful as function_exists() will return FALSE for
566
     * functions disabled via the *disable_functions* php.ini
567
     * setting, but not for *suhosin.executor.func.blacklist* and
568
     * *suhosin.executor.disable_eval*. These settings will just
569
     * terminate script execution if a disabled function is executed.
570
     *
571
     * The above described behavior turned out to be a bug in Suhosin,
572
     * but even though a fix was committed for 0.9.34 on 2012-02-12,
573
     * that version is yet to be released. This function will therefore
574
     * be just temporary, but would probably be kept for a few years.
575
     *
576
     * @see   http://www.hardened-php.net/suhosin/
577
     *
578
     * @param string $functionName Function to check for
579
     *
580
     * @return bool TRUE if the function exists and is safe to call,
581
     *              FALSE otherwise.
582
     *
583
     * @codeCoverageIgnore This is too exotic
584
     */
585
    function function_usable(string $functionName): bool
586
    {
587
        static $_suhosin_func_blacklist;
588

589
        if (function_exists($functionName)) {
590
            if (! isset($_suhosin_func_blacklist)) {
591
                $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : [];
592
            }
593

594
            return ! in_array($functionName, $_suhosin_func_blacklist, true);
595
        }
596

597
        return false;
598
    }
599
}
600

601
if (! function_exists('helper')) {
602
    /**
603
     * Loads a helper file into memory. Supports namespaced helpers,
604
     * both in and out of the 'Helpers' directory of a namespaced directory.
605
     *
606
     * Will load ALL helpers of the matching name, in the following order:
607
     *   1. app/Helpers
608
     *   2. {namespace}/Helpers
609
     *   3. system/Helpers
610
     *
611
     * @param list<string>|string $filenames
612
     *
613
     * @throws FileNotFoundException
614
     */
615
    function helper($filenames): void
616
    {
617
        static $loaded = [];
7,457✔
618

619
        $loader = service('locator');
7,457✔
620

621
        if (! is_array($filenames)) {
7,457✔
622
            $filenames = [$filenames];
7,457✔
623
        }
624

625
        // Store a list of all files to include...
626
        $includes = [];
7,457✔
627

628
        foreach ($filenames as $filename) {
7,457✔
629
            // Store our system and application helper
630
            // versions so that we can control the load ordering.
631
            $systemHelper  = '';
7,457✔
632
            $appHelper     = '';
7,457✔
633
            $localIncludes = [];
7,457✔
634

635
            if (! str_contains($filename, '_helper')) {
7,457✔
636
                $filename .= '_helper';
7,457✔
637
            }
638

639
            // Check if this helper has already been loaded
640
            if (in_array($filename, $loaded, true)) {
7,457✔
641
                continue;
7,457✔
642
            }
643

644
            // If the file is namespaced, we'll just grab that
645
            // file and not search for any others
646
            if (str_contains($filename, '\\')) {
287✔
647
                $path = $loader->locateFile($filename, 'Helpers');
2✔
648

649
                if ($path === false) {
2✔
650
                    throw FileNotFoundException::forFileNotFound($filename);
1✔
651
                }
652

653
                $includes[] = $path;
1✔
654
                $loaded[]   = $filename;
1✔
655
            } else {
656
                // No namespaces, so search in all available locations
657
                $paths = $loader->search('Helpers/' . $filename);
285✔
658

659
                foreach ($paths as $path) {
284✔
660
                    if (str_starts_with($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
284✔
661
                        $appHelper = $path;
1✔
662
                    } elseif (str_starts_with($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
284✔
663
                        $systemHelper = $path;
283✔
664
                    } else {
665
                        $localIncludes[] = $path;
1✔
666
                        $loaded[]        = $filename;
1✔
667
                    }
668
                }
669

670
                // App-level helpers should override all others
671
                if ($appHelper !== '') {
284✔
672
                    $includes[] = $appHelper;
1✔
673
                    $loaded[]   = $filename;
1✔
674
                }
675

676
                // All namespaced files get added in next
677
                $includes = [...$includes, ...$localIncludes];
284✔
678

679
                // And the system default one should be added in last.
680
                if ($systemHelper !== '') {
284✔
681
                    $includes[] = $systemHelper;
283✔
682
                    $loaded[]   = $filename;
283✔
683
                }
684
            }
685
        }
686

687
        // Now actually include all of the files
688
        foreach ($includes as $path) {
7,457✔
689
            include_once $path;
285✔
690
        }
691
    }
692
}
693

694
if (! function_exists('is_cli')) {
695
    /**
696
     * Check if PHP was invoked from the command line.
697
     *
698
     * @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli
699
     */
700
    function is_cli(): bool
701
    {
702
        if (in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
703
            return true;
704
        }
705

706
        // PHP_SAPI could be 'cgi-fcgi', 'fpm-fcgi'.
707
        // See https://github.com/codeigniter4/CodeIgniter4/pull/5393
708
        return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service), codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
709
    }
710
}
711

712
if (! function_exists('is_really_writable')) {
713
    /**
714
     * Tests for file writability
715
     *
716
     * is_writable() returns TRUE on Windows servers when you really can't write to
717
     * the file, based on the read-only attribute. is_writable() is also unreliable
718
     * on Unix servers if safe_mode is on.
719
     *
720
     * @see https://bugs.php.net/bug.php?id=54709
721
     *
722
     * @throws Exception
723
     *
724
     * @codeCoverageIgnore Not practical to test, as travis runs on linux
725
     */
726
    function is_really_writable(string $file): bool
727
    {
728
        // If we're on a Unix server we call is_writable
729
        if (! is_windows()) {
730
            return is_writable($file);
731
        }
732

733
        /* For Windows servers and safe_mode "on" installations we'll actually
734
         * write a file then read it. Bah...
735
         */
736
        if (is_dir($file)) {
737
            $file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16));
738
            if (($fp = @fopen($file, 'ab')) === false) {
739
                return false;
740
            }
741

742
            fclose($fp);
743
            @chmod($file, 0777);
744
            @unlink($file);
745

746
            return true;
747
        }
748

749
        if (! is_file($file) || ($fp = @fopen($file, 'ab')) === false) {
750
            return false;
751
        }
752

753
        fclose($fp);
754

755
        return true;
756
    }
757
}
758

759
if (! function_exists('is_windows')) {
760
    /**
761
     * Detect if platform is running in Windows.
762
     */
763
    function is_windows(?bool $mock = null): bool
764
    {
765
        static $mocked;
2,231✔
766

767
        if (func_num_args() === 1) {
2,231✔
768
            $mocked = $mock;
1✔
769
        }
770

771
        return $mocked ?? DIRECTORY_SEPARATOR === '\\';
2,231✔
772
    }
773
}
774

775
if (! function_exists('lang')) {
776
    /**
777
     * A convenience method to translate a string or array of them and format
778
     * the result with the intl extension's MessageFormatter.
779
     *
780
     * @param array<array-key, float|int|string> $args
781
     *
782
     * @return list<string>|string
783
     */
784
    function lang(string $line, array $args = [], ?string $locale = null)
785
    {
786
        /** @var Language $language */
787
        $language = service('language');
1,898✔
788

789
        // Get active locale
790
        $activeLocale = $language->getLocale();
1,898✔
791

792
        if ((string) $locale !== '' && $locale !== $activeLocale) {
1,898✔
793
            $language->setLocale($locale);
3✔
794
        }
795

796
        $lines = $language->getLine($line, $args);
1,898✔
797

798
        if ((string) $locale !== '' && $locale !== $activeLocale) {
1,898✔
799
            // Reset to active locale
800
            $language->setLocale($activeLocale);
3✔
801
        }
802

803
        return $lines;
1,898✔
804
    }
805
}
806

807
if (! function_exists('log_message')) {
808
    /**
809
     * A convenience/compatibility method for logging events through
810
     * the Log system.
811
     *
812
     * Allowed log levels are:
813
     *  - emergency
814
     *  - alert
815
     *  - critical
816
     *  - error
817
     *  - warning
818
     *  - notice
819
     *  - info
820
     *  - debug
821
     */
822
    function log_message(string $level, string $message, array $context = []): void
823
    {
824
        // When running tests, we want to always ensure that the
825
        // TestLogger is running, which provides utilities for
826
        // for asserting that logs were called in the test code.
827
        if (ENVIRONMENT === 'testing') {
84✔
828
            $logger = new TestLogger(new Logger());
84✔
829

830
            $logger->log($level, $message, $context);
84✔
831

832
            return;
84✔
833
        }
834

835
        service('logger')->log($level, $message, $context); // @codeCoverageIgnore
836
    }
837
}
838

839
if (! function_exists('model')) {
840
    /**
841
     * More simple way of getting model instances from Factories
842
     *
843
     * @template ModelTemplate of Model
844
     *
845
     * @param class-string<ModelTemplate>|string $name
846
     *
847
     * @return ($name is class-string<ModelTemplate> ? ModelTemplate : object|null)
848
     */
849
    function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null)
850
    {
851
        return Factories::models($name, ['getShared' => $getShared], $conn);
53✔
852
    }
853
}
854

855
if (! function_exists('old')) {
856
    /**
857
     * Provides access to "old input" that was set in the session
858
     * during a redirect()->withInput().
859
     *
860
     * @param string|null                                $default
861
     * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|false $escape
862
     *
863
     * @return array|string|null
864
     */
865
    function old(string $key, $default = null, $escape = 'html')
866
    {
867
        // Ensure the session is loaded
868
        if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') {
3✔
869
            session(); // @codeCoverageIgnore
870
        }
871

872
        $request = service('request');
3✔
873

874
        $value = $request->getOldInput($key);
3✔
875

876
        // Return the default value if nothing
877
        // found in the old input.
878
        if ($value === null) {
3✔
879
            return $default;
1✔
880
        }
881

882
        return $escape === false ? $value : esc($value, $escape);
3✔
883
    }
884
}
885

886
if (! function_exists('redirect')) {
887
    /**
888
     * Convenience method that works with the current global $request and
889
     * $router instances to redirect using named/reverse-routed routes
890
     * to determine the URL to go to.
891
     *
892
     * If more control is needed, you must use $response->redirect explicitly.
893
     *
894
     * @param non-empty-string|null $route Route name or Controller::method
895
     */
896
    function redirect(?string $route = null): RedirectResponse
897
    {
898
        $response = service('redirectresponse');
8✔
899

900
        if ((string) $route !== '') {
8✔
901
            return $response->route($route);
1✔
902
        }
903

904
        return $response;
7✔
905
    }
906
}
907

908
if (! function_exists('_solidus')) {
909
    /**
910
     * Generates the solidus character (`/`) depending on the HTML5 compatibility flag in `Config\DocTypes`
911
     *
912
     * @param DocTypes|null $docTypesConfig New config. For testing purpose only.
913
     *
914
     * @internal
915
     */
916
    function _solidus(?DocTypes $docTypesConfig = null): string
917
    {
918
        static $docTypes = null;
96✔
919

920
        if ($docTypesConfig instanceof DocTypes) {
96✔
921
            $docTypes = $docTypesConfig;
14✔
922
        }
923

924
        $docTypes ??= new DocTypes();
96✔
925

926
        if ($docTypes->html5 ?? false) {
96✔
927
            return '';
96✔
928
        }
929

930
        return ' /';
14✔
931
    }
932
}
933

934
if (! function_exists('remove_invisible_characters')) {
935
    /**
936
     * Remove Invisible Characters
937
     *
938
     * This prevents sandwiching null characters
939
     * between ascii characters, like Java\0script.
940
     */
941
    function remove_invisible_characters(string $str, bool $urlEncoded = true): string
942
    {
943
        $nonDisplayables = [];
858✔
944

945
        // every control character except newline (dec 10),
946
        // carriage return (dec 13) and horizontal tab (dec 09)
947
        if ($urlEncoded) {
858✔
948
            $nonDisplayables[] = '/%0[0-8bcef]/';  // url encoded 00-08, 11, 12, 14, 15
2✔
949
            $nonDisplayables[] = '/%1[0-9a-f]/';   // url encoded 16-31
2✔
950
        }
951

952
        $nonDisplayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S';   // 00-08, 11, 12, 14-31, 127
858✔
953

954
        do {
955
            $str = preg_replace($nonDisplayables, '', $str, -1, $count);
858✔
956
        } while ($count);
858✔
957

958
        return $str;
858✔
959
    }
960
}
961

962
if (! function_exists('render_backtrace')) {
963
    /**
964
     * Renders a backtrace in a nice string format.
965
     *
966
     * @param list<array{
967
     *  file?: string,
968
     *  line?: int,
969
     *  class?: string,
970
     *  type?: string,
971
     *  function: string,
972
     *  args?: list<mixed>
973
     * }> $backtrace
974
     */
975
    function render_backtrace(array $backtrace): string
976
    {
977
        $backtraces = [];
38✔
978

979
        foreach ($backtrace as $index => $trace) {
38✔
980
            $frame = $trace + ['file' => '[internal function]', 'line' => 0, 'class' => '', 'type' => '', 'args' => []];
38✔
981

982
            if ($frame['file'] !== '[internal function]') {
38✔
983
                $frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']);
38✔
984
            }
985

986
            unset($frame['line']);
38✔
987
            $idx = $index;
38✔
988
            $idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT);
38✔
989

990
            $args = implode(', ', array_map(static fn ($value): string => match (true) {
38✔
991
                is_object($value)   => sprintf('Object(%s)', $value::class),
38✔
992
                is_array($value)    => $value !== [] ? '[...]' : '[]',
38✔
993
                $value === null     => 'null',
38✔
994
                is_resource($value) => sprintf('resource (%s)', get_resource_type($value)),
38✔
995
                default             => var_export($value, true),
38✔
996
            }, $frame['args']));
38✔
997

998
            $backtraces[] = sprintf(
38✔
999
                '%s %s: %s%s%s(%s)',
38✔
1000
                $idx,
38✔
1001
                clean_path($frame['file']),
38✔
1002
                $frame['class'],
38✔
1003
                $frame['type'],
38✔
1004
                $frame['function'],
38✔
1005
                $args,
38✔
1006
            );
38✔
1007
        }
1008

1009
        return implode("\n", $backtraces);
38✔
1010
    }
1011
}
1012

1013
if (! function_exists('request')) {
1014
    /**
1015
     * Returns the shared Request.
1016
     *
1017
     * @return CLIRequest|IncomingRequest
1018
     */
1019
    function request()
1020
    {
1021
        return service('request');
12✔
1022
    }
1023
}
1024

1025
if (! function_exists('response')) {
1026
    /**
1027
     * Returns the shared Response.
1028
     */
1029
    function response(): ResponseInterface
1030
    {
1031
        return service('response');
3✔
1032
    }
1033
}
1034

1035
if (! function_exists('route_to')) {
1036
    /**
1037
     * Given a route name or controller/method string and any params,
1038
     * will attempt to build the relative URL to the
1039
     * matching route.
1040
     *
1041
     * NOTE: This requires the controller/method to
1042
     * have a route defined in the routes Config file.
1043
     *
1044
     * @param string     $method    Route name or Controller::method
1045
     * @param int|string ...$params One or more parameters to be passed to the route.
1046
     *                              The last parameter allows you to set the locale.
1047
     *
1048
     * @return false|string The route (URI path relative to baseURL) or false if not found.
1049
     */
1050
    function route_to(string $method, ...$params)
1051
    {
1052
        return service('routes')->reverseRoute($method, ...$params);
13✔
1053
    }
1054
}
1055

1056
if (! function_exists('session')) {
1057
    /**
1058
     * A convenience method for accessing the session instance,
1059
     * or an item that has been set in the session.
1060
     *
1061
     * Examples:
1062
     *    session()->set('foo', 'bar');
1063
     *    $foo = session('bar');
1064
     *
1065
     * @return ($val is null ? Session : mixed)
1066
     */
1067
    function session(?string $val = null)
1068
    {
1069
        $session = service('session');
97✔
1070

1071
        // Returning a single item?
1072
        if (is_string($val)) {
97✔
1073
            return $session->get($val);
32✔
1074
        }
1075

1076
        return $session;
67✔
1077
    }
1078
}
1079

1080
if (! function_exists('service')) {
1081
    /**
1082
     * Allows cleaner access to the Services Config file.
1083
     * Always returns a SHARED instance of the class, so
1084
     * calling the function multiple times should always
1085
     * return the same instance.
1086
     *
1087
     * These are equal:
1088
     *  - $timer = service('timer')
1089
     *  - $timer = \CodeIgniter\Config\Services::timer();
1090
     *
1091
     * @param mixed ...$params
1092
     */
1093
    function service(string $name, ...$params): ?object
1094
    {
1095
        if ($params === []) {
7,477✔
1096
            return Services::get($name);
7,477✔
1097
        }
1098

1099
        return Services::$name(...$params);
1,036✔
1100
    }
1101
}
1102

1103
if (! function_exists('single_service')) {
1104
    /**
1105
     * Always returns a new instance of the class.
1106
     *
1107
     * @param mixed ...$params
1108
     */
1109
    function single_service(string $name, ...$params): ?object
1110
    {
1111
        $service = Services::serviceExists($name);
80✔
1112

1113
        if ($service === null) {
80✔
1114
            // The service is not defined anywhere so just return.
1115
            return null;
1✔
1116
        }
1117

1118
        $method = new ReflectionMethod($service, $name);
79✔
1119
        $count  = $method->getNumberOfParameters();
79✔
1120
        $mParam = $method->getParameters();
79✔
1121

1122
        if ($count === 1) {
79✔
1123
            // This service needs only one argument, which is the shared
1124
            // instance flag, so let's wrap up and pass false here.
1125
            return $service::$name(false);
20✔
1126
        }
1127

1128
        // Fill in the params with the defaults, but stop before the last
1129
        for ($startIndex = count($params); $startIndex <= $count - 2; $startIndex++) {
59✔
1130
            $params[$startIndex] = $mParam[$startIndex]->getDefaultValue();
39✔
1131
        }
1132

1133
        // Ensure the last argument will not create a shared instance
1134
        $params[$count - 1] = false;
59✔
1135

1136
        return $service::$name(...$params);
59✔
1137
    }
1138
}
1139

1140
if (! function_exists('slash_item')) {
1141
    // Unlike CI3, this function is placed here because
1142
    // it's not a config, or part of a config.
1143
    /**
1144
     * Fetch a config file item with slash appended (if not empty)
1145
     *
1146
     * @param string $item Config item name
1147
     *
1148
     * @return string|null The configuration item or NULL if
1149
     *                     the item doesn't exist
1150
     */
1151
    function slash_item(string $item): ?string
1152
    {
1153
        $config = config(App::class);
28✔
1154

1155
        if (! property_exists($config, $item)) {
28✔
1156
            return null;
1✔
1157
        }
1158

1159
        $configItem = $config->{$item};
27✔
1160

1161
        if (! is_scalar($configItem)) {
27✔
1162
            throw new RuntimeException(sprintf(
1✔
1163
                'Cannot convert "%s::$%s" of type "%s" to type "string".',
1✔
1164
                App::class,
1✔
1165
                $item,
1✔
1166
                gettype($configItem),
1✔
1167
            ));
1✔
1168
        }
1169

1170
        $configItem = trim((string) $configItem);
26✔
1171

1172
        if ($configItem === '') {
26✔
1173
            return $configItem;
1✔
1174
        }
1175

1176
        return rtrim($configItem, '/') . '/';
26✔
1177
    }
1178
}
1179

1180
if (! function_exists('stringify_attributes')) {
1181
    /**
1182
     * Stringify attributes for use in HTML tags.
1183
     *
1184
     * Helper function used to convert a string, array, or object
1185
     * of attributes to a string.
1186
     *
1187
     * @param array|object|string $attributes string, array, object that can be cast to array
1188
     */
1189
    function stringify_attributes($attributes, bool $js = false): string
1190
    {
1191
        $atts = '';
93✔
1192

1193
        if (in_array($attributes, ['', [], null], true)) {
93✔
1194
            return $atts;
50✔
1195
        }
1196

1197
        if (is_string($attributes)) {
50✔
1198
            return ' ' . $attributes;
15✔
1199
        }
1200

1201
        $attributes = (array) $attributes;
38✔
1202

1203
        foreach ($attributes as $key => $val) {
38✔
1204
            $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . esc($val) . '"';
38✔
1205
        }
1206

1207
        return rtrim($atts, ',');
38✔
1208
    }
1209
}
1210

1211
if (! function_exists('timer')) {
1212
    /**
1213
     * A convenience method for working with the timer.
1214
     * If no parameter is passed, it will return the timer instance.
1215
     * If callable is passed, it measures time of callable and
1216
     * returns its return value if any.
1217
     * Otherwise will start or stop the timer intelligently.
1218
     *
1219
     * @param non-empty-string|null    $name
1220
     * @param (callable(): mixed)|null $callable
1221
     *
1222
     * @return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer))
1223
     */
1224
    function timer(?string $name = null, ?callable $callable = null)
1225
    {
1226
        $timer = service('timer');
6✔
1227

1228
        if ($name === null) {
6✔
1229
            return $timer;
5✔
1230
        }
1231

1232
        if ($callable !== null) {
4✔
1233
            return $timer->record($name, $callable);
2✔
1234
        }
1235

1236
        if ($timer->has($name)) {
2✔
1237
            return $timer->stop($name);
1✔
1238
        }
1239

1240
        return $timer->start($name);
2✔
1241
    }
1242
}
1243

1244
if (! function_exists('view')) {
1245
    /**
1246
     * Grabs the current RendererInterface-compatible class
1247
     * and tells it to render the specified view. Simply provides
1248
     * a convenience method that can be used in Controllers,
1249
     * libraries, and routed closures.
1250
     *
1251
     * NOTE: Does not provide any escaping of the data, so that must
1252
     * all be handled manually by the developer.
1253
     *
1254
     * @param array $options Options for saveData or third-party extensions.
1255
     */
1256
    function view(string $name, array $data = [], array $options = []): string
1257
    {
1258
        $renderer = service('renderer');
80✔
1259

1260
        $config   = config(View::class);
80✔
1261
        $saveData = $config->saveData;
80✔
1262

1263
        if (array_key_exists('saveData', $options)) {
80✔
1264
            $saveData = (bool) $options['saveData'];
2✔
1265
            unset($options['saveData']);
2✔
1266
        }
1267

1268
        return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
80✔
1269
    }
1270
}
1271

1272
if (! function_exists('view_cell')) {
1273
    /**
1274
     * View cells are used within views to insert HTML chunks that are managed
1275
     * by other classes.
1276
     *
1277
     * @param array|string|null $params
1278
     *
1279
     * @throws ReflectionException
1280
     */
1281
    function view_cell(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string
1282
    {
1283
        return service('viewcell')
18✔
1284
            ->render($library, $params, $ttl, $cacheName);
18✔
1285
    }
1286
}
1287

1288
/**
1289
 * These helpers come from Laravel so will not be
1290
 * re-tested and can be ignored safely.
1291
 *
1292
 * @see https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/helpers.php
1293
 */
1294
if (! function_exists('class_basename')) {
1295
    /**
1296
     * Get the class "basename" of the given object / class.
1297
     *
1298
     * @param class-string|object $class
1299
     *
1300
     * @return string
1301
     *
1302
     * @codeCoverageIgnore
1303
     */
1304
    function class_basename($class)
1305
    {
1306
        $class = is_object($class) ? $class::class : $class;
1307

1308
        return basename(str_replace('\\', '/', $class));
1309
    }
1310
}
1311

1312
if (! function_exists('class_uses_recursive')) {
1313
    /**
1314
     * Returns all traits used by a class, its parent classes and trait of their traits.
1315
     *
1316
     * @param class-string|object $class
1317
     *
1318
     * @return array<class-string, class-string>
1319
     *
1320
     * @codeCoverageIgnore
1321
     */
1322
    function class_uses_recursive($class)
1323
    {
1324
        if (is_object($class)) {
1325
            $class = $class::class;
1326
        }
1327

1328
        $results = [];
1329

1330
        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
1331
            $results += trait_uses_recursive($class);
1332
        }
1333

1334
        return array_unique($results);
1335
    }
1336
}
1337

1338
if (! function_exists('trait_uses_recursive')) {
1339
    /**
1340
     * Returns all traits used by a trait and its traits.
1341
     *
1342
     * @param class-string $trait
1343
     *
1344
     * @return array<class-string, class-string>
1345
     *
1346
     * @codeCoverageIgnore
1347
     */
1348
    function trait_uses_recursive($trait)
1349
    {
1350
        $traits = class_uses($trait) ?: [];
1351

1352
        foreach ($traits as $trait) {
1353
            $traits += trait_uses_recursive($trait);
1354
        }
1355

1356
        return $traits;
1357
    }
1358
}
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