From b382689ff27b7920061af25cba021f4d822e5103 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 16:34:32 +0800 Subject: [PATCH 1/6] add `EnvironmentDetector` utility class --- system/Config/Services.php | 16 +++ system/EnvironmentDetector.php | 72 +++++++++++++ tests/system/EnvironmentDetectorTest.php | 130 +++++++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 system/EnvironmentDetector.php create mode 100644 tests/system/EnvironmentDetectorTest.php diff --git a/system/Config/Services.php b/system/Config/Services.php index 82025723d48c..76ba47f81350 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -28,6 +28,7 @@ use CodeIgniter\Email\Email; use CodeIgniter\Encryption\EncrypterInterface; use CodeIgniter\Encryption\Encryption; +use CodeIgniter\EnvironmentDetector; use CodeIgniter\Filters\Filters; use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; @@ -259,6 +260,21 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared = return $encryption->initialize($config); } + /** + * Provides a simple way to determine the current environment + * of the application. + * + * @return EnvironmentDetector + */ + public static function environmentdetector(?string $environment = null, bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('environmentdetector', $environment); + } + + return new EnvironmentDetector($environment); + } + /** * The Exceptions class holds the methods that handle: * diff --git a/system/EnvironmentDetector.php b/system/EnvironmentDetector.php new file mode 100644 index 000000000000..a3e90c96fb3d --- /dev/null +++ b/system/EnvironmentDetector.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter; + +use CodeIgniter\Exceptions\InvalidArgumentException; + +/** + * Provides a simple way to determine the current environment of the application. + * + * For custom environment names beyond the built-in production/development/testing, + * use {@see self::is()}. + * + * @see \CodeIgniter\EnvironmentDetectorTest + */ +final class EnvironmentDetector +{ + private readonly string $environment; + + /** + * @param non-empty-string|null $environment The environment to use, or null to + * fall back to the `ENVIRONMENT` constant. + */ + public function __construct(?string $environment = null) + { + if ($environment !== null && trim($environment) === '') { + throw new InvalidArgumentException('Environment cannot be an empty string.'); + } + + $this->environment = $environment !== null ? trim($environment) : ENVIRONMENT; + } + + public function get(): string + { + return $this->environment; + } + + /** + * Checks if the current environment matches any of the given environments. + * + * @param string ...$environments One or more environment names to check against. + */ + public function is(string ...$environments): bool + { + return in_array($this->environment, $environments, true); + } + + public function isProduction(): bool + { + return $this->is('production'); + } + + public function isDevelopment(): bool + { + return $this->is('development'); + } + + public function isTesting(): bool + { + return $this->is('testing'); + } +} diff --git a/tests/system/EnvironmentDetectorTest.php b/tests/system/EnvironmentDetectorTest.php new file mode 100644 index 000000000000..d0a0860387f3 --- /dev/null +++ b/tests/system/EnvironmentDetectorTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter; + +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class EnvironmentDetectorTest extends CIUnitTestCase +{ + public function testDefaultsToEnvironmentConstant(): void + { + $detector = new EnvironmentDetector(); + + $this->assertSame(ENVIRONMENT, $detector->get()); + } + + public function testExplicitEnvironmentOverridesConstant(): void + { + $detector = new EnvironmentDetector('production'); + + $this->assertSame('production', $detector->get()); + } + + public function testTrimsSurroundingWhitespace(): void + { + $detector = new EnvironmentDetector(" production\n"); + + $this->assertSame('production', $detector->get()); + } + + public function testRejectsEmptyString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Environment cannot be an empty string.'); + + new EnvironmentDetector(''); // @phpstan-ignore argument.type + } + + public function testRejectsWhitespaceOnlyString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Environment cannot be an empty string.'); + + new EnvironmentDetector(" \t\n"); + } + + public function testIsMatchesSingleEnvironment(): void + { + $detector = new EnvironmentDetector('staging'); + + $this->assertTrue($detector->is('staging')); + $this->assertFalse($detector->is('production')); + } + + public function testIsMatchesAnyOfSeveralEnvironments(): void + { + $detector = new EnvironmentDetector('staging'); + + $this->assertTrue($detector->is('production', 'staging', 'development')); + $this->assertFalse($detector->is('production', 'development', 'testing')); + } + + public function testIsReturnsFalseWhenNoEnvironmentsGiven(): void + { + $detector = new EnvironmentDetector('production'); + + $this->assertFalse($detector->is()); + } + + public function testIsIsCaseSensitive(): void + { + $detector = new EnvironmentDetector('production'); + + $this->assertFalse($detector->is('Production')); + $this->assertFalse($detector->is('PRODUCTION')); + } + + /** + * @param non-empty-string $environment + */ + #[DataProvider('provideBuiltInEnvironmentHelpers')] + public function testBuiltInEnvironmentHelpers(string $environment, bool $isProduction, bool $isDevelopment, bool $isTesting): void + { + $detector = new EnvironmentDetector($environment); + + $this->assertSame($isProduction, $detector->isProduction()); + $this->assertSame($isDevelopment, $detector->isDevelopment()); + $this->assertSame($isTesting, $detector->isTesting()); + } + + /** + * @return iterable + */ + public static function provideBuiltInEnvironmentHelpers(): iterable + { + yield 'production' => ['production', true, false, false]; + + yield 'development' => ['development', false, true, false]; + + yield 'testing' => ['testing', false, false, true]; + + yield 'custom' => ['staging', false, false, false]; + } + + public function testResolvesAsSharedService(): void + { + $first = service('environmentdetector'); + $second = service('environmentdetector'); + + $this->assertInstanceOf(EnvironmentDetector::class, $first); + $this->assertSame($first, $second); + } +} From e605c536938188657d0fbce16f28512f58515cb4 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 17:13:16 +0800 Subject: [PATCH 2/6] swap manual checks on ENVIRONMENT --- system/CLI/CLI.php | 2 +- system/CLI/InputOutput.php | 2 +- system/Commands/Database/CreateDatabase.php | 2 +- system/Commands/Database/MigrateRefresh.php | 2 +- system/Commands/Database/MigrateRollback.php | 2 +- system/Commands/Database/MigrateStatus.php | 2 +- .../Translation/LocalizationFinder.php | 2 +- .../Commands/Translation/LocalizationSync.php | 2 +- system/Database/Config.php | 2 +- system/Database/MigrationRunner.php | 4 +-- system/Debug/ExceptionHandler.php | 4 +-- system/Debug/Toolbar.php | 2 +- system/Debug/Toolbar/Collectors/Config.php | 2 +- system/Format/JSONFormatter.php | 2 +- system/HTTP/DownloadResponse.php | 2 +- system/HTTP/SSEResponse.php | 4 +-- system/Helpers/form_helper.php | 2 +- system/Log/Logger.php | 2 +- system/Router/Attributes/Restrict.php | 2 +- system/Router/RouteCollection.php | 2 +- system/Session/Session.php | 8 +++--- system/Validation/StrictRules/FileRules.php | 19 ++++++-------- tests/system/EnvironmentDetectorTest.php | 1 + tests/system/Format/JSONFormatterTest.php | 15 +++++++++++ .../Validation/StrictRules/FileRulesTest.php | 25 +++++++++++++++---- 25 files changed, 71 insertions(+), 43 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 583b9f0dda35..427cea12e9c0 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -656,7 +656,7 @@ public static function strlen(?string $string): int */ public static function streamSupports(string $function, $resource): bool { - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { // In the current setup of the tests we cannot fully check // if the stream supports the function since we are using // filtered streams. diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index b69c19e2eee1..9fd701682085 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -42,7 +42,7 @@ public function __construct() public function input(?string $prefix = null): string { // readline() can't be tested. - if ($this->readlineSupport && ENVIRONMENT !== 'testing') { + if ($this->readlineSupport && ! service('environmentdetector')->isTesting()) { return readline($prefix); // @codeCoverageIgnore } diff --git a/system/Commands/Database/CreateDatabase.php b/system/Commands/Database/CreateDatabase.php index cfc5656617d3..6a219d270b33 100644 --- a/system/Commands/Database/CreateDatabase.php +++ b/system/Commands/Database/CreateDatabase.php @@ -87,7 +87,7 @@ public function run(array $params) $config = config(Database::class); // Set to an empty database to prevent connection errors. - $group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup; + $group = service('environmentdetector')->isTesting() ? 'tests' : $config->defaultGroup; $config->{$group}['database'] = ''; diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index 9496d83062bb..60ae788290e3 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -74,7 +74,7 @@ public function run(array $params) { $params['b'] = 0; - if (ENVIRONMENT === 'production') { + if (service('environmentdetector')->isProduction()) { // @codeCoverageIgnoreStart $force = array_key_exists('f', $params) || CLI::getOption('f'); diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index d2ff23be36df..8b478180b028 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -72,7 +72,7 @@ class MigrateRollback extends BaseCommand */ public function run(array $params) { - if (ENVIRONMENT === 'production') { + if (service('environmentdetector')->isProduction()) { // @codeCoverageIgnoreStart $force = array_key_exists('f', $params) || CLI::getOption('f'); diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 39d2564a7bf2..9e1b9580f24c 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -92,7 +92,7 @@ public function run(array $params) $status = []; foreach (array_keys($namespaces) as $namespace) { - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { // Make Tests\\Support discoverable for testing $this->ignoredNamespaces[] = 'Tests\Support'; // @codeCoverageIgnore } diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index e6557b74d1c4..676d47094f70 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -61,7 +61,7 @@ public function run(array $params) $currentDir = APPPATH; $this->languagePath = $currentDir . 'Language'; - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { $currentDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR; $this->languagePath = SUPPORTPATH . 'Language'; } diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index f54df45c364c..1b46b1798c12 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -85,7 +85,7 @@ public function run(array $params) return EXIT_USER_INPUT; } - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { $this->languagePath = SUPPORTPATH . 'Language'; } diff --git a/system/Database/Config.php b/system/Database/Config.php index 413d0b3b5de8..fd7163cbd12b 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -61,7 +61,7 @@ public static function connect($group = null, bool $getShared = true) $dbConfig = config(DbConfig::class); if ($group === null) { - $group = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup; + $group = service('environmentdetector')->isTesting() ? 'tests' : $dbConfig->defaultGroup; } assert(is_string($group)); diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index df5ee049f0b7..e385972f7e58 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -448,7 +448,7 @@ public function findMigrations(): array $migrations = []; foreach ($namespaces as $namespace) { - if (ENVIRONMENT !== 'testing' && $namespace === 'Tests\Support') { + if (! service('environmentdetector')->isTesting() && $namespace === 'Tests\Support') { continue; } @@ -985,7 +985,7 @@ protected function migrate($direction, $migration): bool $instance = new $class(Database::forge($this->db)); $group = $instance->getDBGroup() ?? $this->group; - if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') { + if (! service('environmentdetector')->isTesting() && $group === 'tests' && $this->groupFilter !== 'tests') { // @codeCoverageIgnoreStart $this->groupSkip = true; diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 2caa109b7754..a4fdb5658d03 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -94,7 +94,7 @@ public function handle( $this->respond($data, $statusCode)->send(); - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { exit($exitCode); // @codeCoverageIgnore } @@ -123,7 +123,7 @@ public function handle( // Displays the HTML or CLI error code. $this->render($exception, $statusCode, $viewFile); - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { exit($exitCode); // @codeCoverageIgnore } } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index b58352cf1f8a..1dfa18ebcf7e 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -457,7 +457,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r */ public function respond(): void { - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { return; } diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php index 80673f979f6c..e3687750c1c4 100644 --- a/system/Debug/Toolbar/Collectors/Config.php +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -32,7 +32,7 @@ public static function display(): array 'ciVersion' => CodeIgniter::CI_VERSION, 'phpVersion' => PHP_VERSION, 'phpSAPI' => PHP_SAPI, - 'environment' => ENVIRONMENT, + 'environment' => service('environmentdetector')->get(), 'baseURL' => $config->baseURL, 'timezone' => app_timezone(), 'locale' => service('request')->getLocale(), diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index ac237fcc7af3..dfa939799338 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -37,7 +37,7 @@ public function format($data) $options = $config->formatterOptions['application/json'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; - if (ENVIRONMENT !== 'production') { + if (! service('environmentdetector')->isProduction()) { $options |= JSON_PRETTY_PRINT; } diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index 4273d8fe1413..bd16d40a2449 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -247,7 +247,7 @@ public function noCache(): self public function send() { // Turn off output buffering completely, even if php.ini output_buffering is not off - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { while (ob_get_level() > 0) { ob_end_clean(); } diff --git a/system/HTTP/SSEResponse.php b/system/HTTP/SSEResponse.php index 1137b8805283..c3be970965d7 100644 --- a/system/HTTP/SSEResponse.php +++ b/system/HTTP/SSEResponse.php @@ -137,7 +137,7 @@ private function write(string $output): bool { echo $output; - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { if (ob_get_level() > 0) { ob_flush(); } @@ -156,7 +156,7 @@ private function write(string $output): bool public function send() { // Turn off output buffering completely, even if php.ini output_buffering is not off - if (ENVIRONMENT !== 'testing') { + if (! service('environmentdetector')->isTesting()) { set_time_limit(0); ini_set('zlib.output_compression', 'Off'); diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index 6fa4777e2454..fc4aa4977829 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -703,7 +703,7 @@ function validation_errors() // Check the session to see if any were // passed along from a redirect withErrors() request. - if ($errors !== null && (ENVIRONMENT === 'testing' || ! is_cli())) { + if ($errors !== null && (service('environmentdetector')->isTesting() || ! is_cli())) { return $errors; } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 1f2fdea876b8..4cbde05622d0 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -401,7 +401,7 @@ protected function interpolate($message, array $context = []) $replace['{post_vars}'] = '$_POST: ' . print_r(service('superglobals')->getPostArray(), true); $replace['{get_vars}'] = '$_GET: ' . print_r(service('superglobals')->getGetArray(), true); - $replace['{env}'] = ENVIRONMENT; + $replace['{env}'] = service('environmentdetector')->get(); // Allow us to log the file/line that we are logging from if (str_contains($message, '{file}') || str_contains($message, '{line}')) { diff --git a/system/Router/Attributes/Restrict.php b/system/Router/Attributes/Restrict.php index 79344a7c6982..ea9f40f27a4c 100644 --- a/system/Router/Attributes/Restrict.php +++ b/system/Router/Attributes/Restrict.php @@ -69,7 +69,7 @@ protected function checkEnvironment(): void return; } - $currentEnv = ENVIRONMENT; + $currentEnv = service('environmentdetector')->get(); $allowed = []; $denied = []; diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 0ca1291b9120..160b90d8c0cd 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1163,7 +1163,7 @@ public function view(string $from, string $view, ?array $options = null): RouteC */ public function environment(string $env, Closure $callback): RouteCollectionInterface { - if ($env === ENVIRONMENT) { + if (service('environmentdetector')->is($env)) { $callback($this); } diff --git a/system/Session/Session.php b/system/Session/Session.php index 1c3824928775..8f7285996181 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -86,7 +86,7 @@ public function __construct(SessionHandlerInterface $driver, SessionConfig $conf */ public function start() { - if (is_cli() && ENVIRONMENT !== 'testing') { + if (is_cli() && ! service('environmentdetector')->isTesting()) { // @codeCoverageIgnoreStart $this->logger->debug('Session: Initialization under CLI aborted.'); @@ -273,7 +273,7 @@ private function removeOldSessionCookie(): void public function destroy() { - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { return; } @@ -287,7 +287,7 @@ public function destroy() */ public function close() { - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { return; } @@ -616,7 +616,7 @@ protected function setSaveHandler() */ protected function startSession() { - if (ENVIRONMENT === 'testing') { + if (service('environmentdetector')->isTesting()) { $_SESSION = []; return; diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index 9176716ef561..d238ba92fe4b 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -62,17 +62,14 @@ public function uploaded(?string $blank, string $name): bool return false; } - if (ENVIRONMENT === 'testing') { - if ($file->getError() !== 0) { - return false; - } - } else { - // Note: cannot unit test this; no way to over-ride ENVIRONMENT? - // @codeCoverageIgnoreStart - if (! $file->isValid()) { - return false; - } - // @codeCoverageIgnoreEnd + // In the testing env is_uploaded_file() always returns false + // (fixtures aren't real HTTP uploads), so check the error code directly. + $isValid = service('environmentdetector')->isTesting() + ? $file->getError() === UPLOAD_ERR_OK + : $file->isValid(); + + if (! $isValid) { + return false; } } diff --git a/tests/system/EnvironmentDetectorTest.php b/tests/system/EnvironmentDetectorTest.php index d0a0860387f3..cd334c34f7b8 100644 --- a/tests/system/EnvironmentDetectorTest.php +++ b/tests/system/EnvironmentDetectorTest.php @@ -126,5 +126,6 @@ public function testResolvesAsSharedService(): void $this->assertInstanceOf(EnvironmentDetector::class, $first); $this->assertSame($first, $second); + $this->assertSame(ENVIRONMENT, $first->get()); } } diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index 027c618dc4f0..5fedad36b8f0 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -13,6 +13,8 @@ namespace CodeIgniter\Format; +use CodeIgniter\Config\Services; +use CodeIgniter\EnvironmentDetector; use CodeIgniter\Format\Exceptions\FormatException; use CodeIgniter\Test\CIUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -32,6 +34,12 @@ protected function setUp(): void $this->jsonFormatter = new JSONFormatter(); } + protected function tearDown(): void + { + parent::tearDown(); + Services::resetSingle('environmentdetector'); + } + /** * @param array $data */ @@ -62,4 +70,11 @@ public function testJSONFormatterThrowsError(): void $this->assertSame('Boom', $this->jsonFormatter->format(["\xB1\x31"])); } + + public function testFormattingToJsonIsCompactInProduction(): void + { + Services::injectMock('environmentdetector', new EnvironmentDetector('production')); + + $this->assertSame('{"foo":"bar"}', $this->jsonFormatter->format(['foo' => 'bar'])); + } } diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index 5f848c22db60..c49c5ba25c02 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -13,6 +13,8 @@ namespace CodeIgniter\Validation\StrictRules; +use CodeIgniter\Config\Services; +use CodeIgniter\EnvironmentDetector; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Validation\Validation; @@ -64,7 +66,7 @@ protected function setUp(): void 'name' => 'my-avatar.png', 'size' => 4614, 'type' => 'image/png', - 'error' => 0, + 'error' => UPLOAD_ERR_OK, 'width' => 640, 'height' => 400, ], @@ -153,27 +155,40 @@ protected function tearDown(): void { parent::tearDown(); service('superglobals')->setFilesArray([]); + Services::resetSingle('environmentdetector'); } - public function testUploadedTrue(): void + public function testUploadedPassesForSingleValidFile(): void { $this->validation->setRules(['avatar' => 'uploaded[avatar]']); $this->assertTrue($this->validation->run([])); } - public function testUploadedFalse(): void + public function testUploadedFailsInProductionWhenFileWasNotHttpUpload(): void + { + // Counterpart to testUploadedPassesForSingleValidFile: the same fixture + // passes in the testing env but must fail in production, where isValid() + // enforces is_uploaded_file(). + Services::injectMock('environmentdetector', new EnvironmentDetector('production')); + + $this->validation->setRules(['avatar' => 'uploaded[avatar]']); + + $this->assertFalse($this->validation->run([])); + } + + public function testUploadedFailsWhenFileIsMissingFromRequest(): void { $this->validation->setRules(['avatar' => 'uploaded[userfile]']); $this->assertFalse($this->validation->run([])); } - public function testUploadedArrayReturnsTrue(): void + public function testUploadedPassesWhenAllFilesInArrayAreValid(): void { $this->validation->setRules(['images' => 'uploaded[images]']); $this->assertTrue($this->validation->run([])); } - public function testUploadedArrayReturnsFalse(): void + public function testUploadedFailsWhenAnyFileInArrayHasUploadError(): void { $this->validation->setRules(['photos' => 'uploaded[photos]']); $this->assertFalse($this->validation->run([])); From 63847ef67306c9a6706f63afe9ccf9b6dff7939b Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 17:27:34 +0800 Subject: [PATCH 3/6] add docs --- user_guide_src/source/changelogs/v4.8.0.rst | 2 ++ .../source/general/environments.rst | 20 +++++++++++++++++++ .../source/general/environments/001.php | 14 +++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 user_guide_src/source/general/environments/001.php diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index a9a33ee50165..a835dfa51072 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -263,6 +263,8 @@ Others - **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities. - Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided. +- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environmentdetector`` service as a mockable wrapper around the ``ENVIRONMENT`` constant. + Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`. *************** Message Changes diff --git a/user_guide_src/source/general/environments.rst b/user_guide_src/source/general/environments.rst index 428894ad75b5..7897518b0e45 100644 --- a/user_guide_src/source/general/environments.rst +++ b/user_guide_src/source/general/environments.rst @@ -145,6 +145,26 @@ You can also check the current environment by ``spark env`` command: php spark env +.. _environment-detector-service: + +The ``environmentdetector`` service +=================================== + +.. versionadded:: 4.8.0 + +As an alternative to reading the ``ENVIRONMENT`` constant directly, CodeIgniter +provides the ``environmentdetector`` service, backed by the +:php:class:`CodeIgniter\\EnvironmentDetector` class. Because it is a shared +service, it can be mocked in tests (via ``Services::injectMock()``) to exercise +environment-specific branches without having to redefine the ``ENVIRONMENT`` +constant. + +.. literalinclude:: environments/001.php + +Passing a value to the constructor overrides the detected environment; passing +``null`` (the default) falls back to the ``ENVIRONMENT`` constant. An empty or +whitespace-only string throws ``CodeIgniter\Exceptions\InvalidArgumentException``. + ************************************* Effects on Default Framework Behavior ************************************* diff --git a/user_guide_src/source/general/environments/001.php b/user_guide_src/source/general/environments/001.php new file mode 100644 index 000000000000..916395f89469 --- /dev/null +++ b/user_guide_src/source/general/environments/001.php @@ -0,0 +1,14 @@ +get(); + +// Check against the three built-in environments. +$env->isProduction(); +$env->isDevelopment(); +$env->isTesting(); + +// Match any one of several environments (useful for custom names). +$env->is('production', 'staging'); From 623a106f9e1e2cdc2b924752ac187131d23a811b Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 17:39:25 +0800 Subject: [PATCH 4/6] fix rector --- system/EnvironmentDetector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/EnvironmentDetector.php b/system/EnvironmentDetector.php index a3e90c96fb3d..f45c352deb74 100644 --- a/system/EnvironmentDetector.php +++ b/system/EnvironmentDetector.php @@ -23,9 +23,9 @@ * * @see \CodeIgniter\EnvironmentDetectorTest */ -final class EnvironmentDetector +final readonly class EnvironmentDetector { - private readonly string $environment; + private string $environment; /** * @param non-empty-string|null $environment The environment to use, or null to From 9783bc29088634669b5528a405f970a0c5a84c1f Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 18:40:59 +0800 Subject: [PATCH 5/6] fix test --- tests/system/Validation/StrictRules/FileRulesTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index c49c5ba25c02..50fd91575aec 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -19,6 +19,8 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Validation\Validation; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\PreserveGlobalState; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use Tests\Support\Validation\TestRules; /** @@ -164,11 +166,15 @@ public function testUploadedPassesForSingleValidFile(): void $this->assertTrue($this->validation->run([])); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] public function testUploadedFailsInProductionWhenFileWasNotHttpUpload(): void { // Counterpart to testUploadedPassesForSingleValidFile: the same fixture // passes in the testing env but must fail in production, where isValid() - // enforces is_uploaded_file(). + // enforces is_uploaded_file(). Runs in a separate process because the + // namespace-level is_uploaded_file() override in FileMovingTest.php would + // otherwise leak in and make the fixture appear to be a valid upload. Services::injectMock('environmentdetector', new EnvironmentDetector('production')); $this->validation->setRules(['avatar' => 'uploaded[avatar]']); From 054946907c692550e61286e83a021e8ce4db1de2 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 23 Apr 2026 23:27:17 +0800 Subject: [PATCH 6/6] address review suggestions --- system/CLI/CLI.php | 2 +- system/CLI/InputOutput.php | 2 +- system/Commands/Database/CreateDatabase.php | 2 +- system/Commands/Database/MigrateRefresh.php | 2 +- system/Commands/Database/MigrateRollback.php | 2 +- system/Commands/Database/MigrateStatus.php | 2 +- .../Commands/Translation/LocalizationFinder.php | 2 +- .../Commands/Translation/LocalizationSync.php | 2 +- system/Config/BaseService.php | 2 ++ system/Config/Services.php | 8 +++++--- system/Database/Config.php | 2 +- system/Database/MigrationRunner.php | 4 ++-- system/Debug/ExceptionHandler.php | 4 ++-- system/Debug/Toolbar.php | 2 +- system/Debug/Toolbar/Collectors/Config.php | 2 +- system/EnvironmentDetector.php | 13 +++++++++++-- system/Format/JSONFormatter.php | 2 +- system/HTTP/DownloadResponse.php | 2 +- system/HTTP/SSEResponse.php | 4 ++-- system/Helpers/form_helper.php | 2 +- system/Log/Logger.php | 2 +- system/Router/Attributes/Restrict.php | 2 +- system/Router/RouteCollection.php | 2 +- system/Session/Session.php | 8 ++++---- system/Validation/StrictRules/FileRules.php | 2 +- tests/system/EnvironmentDetectorTest.php | 4 ++-- tests/system/Format/JSONFormatterTest.php | 4 ++-- .../Validation/StrictRules/FileRulesTest.php | 5 +++-- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- user_guide_src/source/general/environments.rst | 17 ++++++++++++----- .../source/general/environments/001.php | 2 +- 31 files changed, 67 insertions(+), 46 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 427cea12e9c0..6a9dd2a925c5 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -656,7 +656,7 @@ public static function strlen(?string $string): int */ public static function streamSupports(string $function, $resource): bool { - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { // In the current setup of the tests we cannot fully check // if the stream supports the function since we are using // filtered streams. diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index 9fd701682085..bde252abd066 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -42,7 +42,7 @@ public function __construct() public function input(?string $prefix = null): string { // readline() can't be tested. - if ($this->readlineSupport && ! service('environmentdetector')->isTesting()) { + if ($this->readlineSupport && ! service('envdetector')->isTesting()) { return readline($prefix); // @codeCoverageIgnore } diff --git a/system/Commands/Database/CreateDatabase.php b/system/Commands/Database/CreateDatabase.php index 6a219d270b33..f41a1f904fd7 100644 --- a/system/Commands/Database/CreateDatabase.php +++ b/system/Commands/Database/CreateDatabase.php @@ -87,7 +87,7 @@ public function run(array $params) $config = config(Database::class); // Set to an empty database to prevent connection errors. - $group = service('environmentdetector')->isTesting() ? 'tests' : $config->defaultGroup; + $group = service('envdetector')->isTesting() ? 'tests' : $config->defaultGroup; $config->{$group}['database'] = ''; diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index 60ae788290e3..6947a97dd7e8 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -74,7 +74,7 @@ public function run(array $params) { $params['b'] = 0; - if (service('environmentdetector')->isProduction()) { + if (service('envdetector')->isProduction()) { // @codeCoverageIgnoreStart $force = array_key_exists('f', $params) || CLI::getOption('f'); diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index 8b478180b028..bbadd9e0c574 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -72,7 +72,7 @@ class MigrateRollback extends BaseCommand */ public function run(array $params) { - if (service('environmentdetector')->isProduction()) { + if (service('envdetector')->isProduction()) { // @codeCoverageIgnoreStart $force = array_key_exists('f', $params) || CLI::getOption('f'); diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 9e1b9580f24c..511fe4ea4d2e 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -92,7 +92,7 @@ public function run(array $params) $status = []; foreach (array_keys($namespaces) as $namespace) { - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { // Make Tests\\Support discoverable for testing $this->ignoredNamespaces[] = 'Tests\Support'; // @codeCoverageIgnore } diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index 676d47094f70..afba01d07baa 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -61,7 +61,7 @@ public function run(array $params) $currentDir = APPPATH; $this->languagePath = $currentDir . 'Language'; - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { $currentDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR; $this->languagePath = SUPPORTPATH . 'Language'; } diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index 1b46b1798c12..c891ea56975d 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -85,7 +85,7 @@ public function run(array $params) return EXIT_USER_INPUT; } - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { $this->languagePath = SUPPORTPATH . 'Language'; } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 3c3bddb26146..1cfe5125a41c 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -29,6 +29,7 @@ use CodeIgniter\Debug\Toolbar; use CodeIgniter\Email\Email; use CodeIgniter\Encryption\EncrypterInterface; +use CodeIgniter\EnvironmentDetector; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Filters\Filters; use CodeIgniter\Format\Format; @@ -111,6 +112,7 @@ * @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true) * @method static Email email($config = null, $getShared = true) * @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false) + * @method static EnvironmentDetector envdetector(?string $environment = null, bool $getShared = true) * @method static Exceptions exceptions(ConfigExceptions $config = null, $getShared = true) * @method static Filters filters(ConfigFilters $config = null, $getShared = true) * @method static Format format(ConfigFormat $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 76ba47f81350..b651d9ce0765 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -264,12 +264,14 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared = * Provides a simple way to determine the current environment * of the application. * - * @return EnvironmentDetector + * Primarily intended for testing environment-specific branches by + * mocking this service. Mocking it does not modify the `ENVIRONMENT` + * constant. It only affects code paths that resolve and use this service. */ - public static function environmentdetector(?string $environment = null, bool $getShared = true) + public static function envdetector(?string $environment = null, bool $getShared = true): EnvironmentDetector { if ($getShared) { - return static::getSharedInstance('environmentdetector', $environment); + return static::getSharedInstance('envdetector', $environment); } return new EnvironmentDetector($environment); diff --git a/system/Database/Config.php b/system/Database/Config.php index fd7163cbd12b..29e5416ff6b2 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -61,7 +61,7 @@ public static function connect($group = null, bool $getShared = true) $dbConfig = config(DbConfig::class); if ($group === null) { - $group = service('environmentdetector')->isTesting() ? 'tests' : $dbConfig->defaultGroup; + $group = service('envdetector')->isTesting() ? 'tests' : $dbConfig->defaultGroup; } assert(is_string($group)); diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index e385972f7e58..7dd001d209ad 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -448,7 +448,7 @@ public function findMigrations(): array $migrations = []; foreach ($namespaces as $namespace) { - if (! service('environmentdetector')->isTesting() && $namespace === 'Tests\Support') { + if (! service('envdetector')->isTesting() && $namespace === 'Tests\Support') { continue; } @@ -985,7 +985,7 @@ protected function migrate($direction, $migration): bool $instance = new $class(Database::forge($this->db)); $group = $instance->getDBGroup() ?? $this->group; - if (! service('environmentdetector')->isTesting() && $group === 'tests' && $this->groupFilter !== 'tests') { + if (! service('envdetector')->isTesting() && $group === 'tests' && $this->groupFilter !== 'tests') { // @codeCoverageIgnoreStart $this->groupSkip = true; diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index a4fdb5658d03..ddcef681b4e3 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -94,7 +94,7 @@ public function handle( $this->respond($data, $statusCode)->send(); - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { exit($exitCode); // @codeCoverageIgnore } @@ -123,7 +123,7 @@ public function handle( // Displays the HTML or CLI error code. $this->render($exception, $statusCode, $viewFile); - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { exit($exitCode); // @codeCoverageIgnore } } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 1dfa18ebcf7e..ba579aedc456 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -457,7 +457,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r */ public function respond(): void { - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { return; } diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php index e3687750c1c4..cc4323928b00 100644 --- a/system/Debug/Toolbar/Collectors/Config.php +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -32,7 +32,7 @@ public static function display(): array 'ciVersion' => CodeIgniter::CI_VERSION, 'phpVersion' => PHP_VERSION, 'phpSAPI' => PHP_SAPI, - 'environment' => service('environmentdetector')->get(), + 'environment' => service('envdetector')->get(), 'baseURL' => $config->baseURL, 'timezone' => app_timezone(), 'locale' => service('request')->getLocale(), diff --git a/system/EnvironmentDetector.php b/system/EnvironmentDetector.php index f45c352deb74..ecdc48d4432a 100644 --- a/system/EnvironmentDetector.php +++ b/system/EnvironmentDetector.php @@ -18,6 +18,13 @@ /** * Provides a simple way to determine the current environment of the application. * + * Primarily intended as a mockable seam for testing environment-specific code + * paths that resolve this class via the `envdetector` service. + * + * It does not redefine the `ENVIRONMENT` constant. It affects only code paths + * that resolve and use this class, while code that still reads `ENVIRONMENT` + * directly keeps its current behavior. + * * For custom environment names beyond the built-in production/development/testing, * use {@see self::is()}. * @@ -33,11 +40,13 @@ */ public function __construct(?string $environment = null) { - if ($environment !== null && trim($environment) === '') { + $environment = $environment !== null ? trim($environment) : ENVIRONMENT; + + if ($environment === '') { throw new InvalidArgumentException('Environment cannot be an empty string.'); } - $this->environment = $environment !== null ? trim($environment) : ENVIRONMENT; + $this->environment = $environment; } public function get(): string diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index dfa939799338..656711f03f90 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -37,7 +37,7 @@ public function format($data) $options = $config->formatterOptions['application/json'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; - if (! service('environmentdetector')->isProduction()) { + if (! service('envdetector')->isProduction()) { $options |= JSON_PRETTY_PRINT; } diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index bd16d40a2449..98444bc5e7c0 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -247,7 +247,7 @@ public function noCache(): self public function send() { // Turn off output buffering completely, even if php.ini output_buffering is not off - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { while (ob_get_level() > 0) { ob_end_clean(); } diff --git a/system/HTTP/SSEResponse.php b/system/HTTP/SSEResponse.php index c3be970965d7..5cb19bbf6d33 100644 --- a/system/HTTP/SSEResponse.php +++ b/system/HTTP/SSEResponse.php @@ -137,7 +137,7 @@ private function write(string $output): bool { echo $output; - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { if (ob_get_level() > 0) { ob_flush(); } @@ -156,7 +156,7 @@ private function write(string $output): bool public function send() { // Turn off output buffering completely, even if php.ini output_buffering is not off - if (! service('environmentdetector')->isTesting()) { + if (! service('envdetector')->isTesting()) { set_time_limit(0); ini_set('zlib.output_compression', 'Off'); diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index fc4aa4977829..5cc98d3adeca 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -703,7 +703,7 @@ function validation_errors() // Check the session to see if any were // passed along from a redirect withErrors() request. - if ($errors !== null && (service('environmentdetector')->isTesting() || ! is_cli())) { + if ($errors !== null && (service('envdetector')->isTesting() || ! is_cli())) { return $errors; } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 4cbde05622d0..ae83b03b23a5 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -401,7 +401,7 @@ protected function interpolate($message, array $context = []) $replace['{post_vars}'] = '$_POST: ' . print_r(service('superglobals')->getPostArray(), true); $replace['{get_vars}'] = '$_GET: ' . print_r(service('superglobals')->getGetArray(), true); - $replace['{env}'] = service('environmentdetector')->get(); + $replace['{env}'] = service('envdetector')->get(); // Allow us to log the file/line that we are logging from if (str_contains($message, '{file}') || str_contains($message, '{line}')) { diff --git a/system/Router/Attributes/Restrict.php b/system/Router/Attributes/Restrict.php index ea9f40f27a4c..a89ca226d0c1 100644 --- a/system/Router/Attributes/Restrict.php +++ b/system/Router/Attributes/Restrict.php @@ -69,7 +69,7 @@ protected function checkEnvironment(): void return; } - $currentEnv = service('environmentdetector')->get(); + $currentEnv = service('envdetector')->get(); $allowed = []; $denied = []; diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 160b90d8c0cd..c6705dd6aa95 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1163,7 +1163,7 @@ public function view(string $from, string $view, ?array $options = null): RouteC */ public function environment(string $env, Closure $callback): RouteCollectionInterface { - if (service('environmentdetector')->is($env)) { + if (service('envdetector')->is($env)) { $callback($this); } diff --git a/system/Session/Session.php b/system/Session/Session.php index 8f7285996181..4e4549356743 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -86,7 +86,7 @@ public function __construct(SessionHandlerInterface $driver, SessionConfig $conf */ public function start() { - if (is_cli() && ! service('environmentdetector')->isTesting()) { + if (is_cli() && ! service('envdetector')->isTesting()) { // @codeCoverageIgnoreStart $this->logger->debug('Session: Initialization under CLI aborted.'); @@ -273,7 +273,7 @@ private function removeOldSessionCookie(): void public function destroy() { - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { return; } @@ -287,7 +287,7 @@ public function destroy() */ public function close() { - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { return; } @@ -616,7 +616,7 @@ protected function setSaveHandler() */ protected function startSession() { - if (service('environmentdetector')->isTesting()) { + if (service('envdetector')->isTesting()) { $_SESSION = []; return; diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index d238ba92fe4b..c174ad003f47 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -64,7 +64,7 @@ public function uploaded(?string $blank, string $name): bool // In the testing env is_uploaded_file() always returns false // (fixtures aren't real HTTP uploads), so check the error code directly. - $isValid = service('environmentdetector')->isTesting() + $isValid = service('envdetector')->isTesting() ? $file->getError() === UPLOAD_ERR_OK : $file->isValid(); diff --git a/tests/system/EnvironmentDetectorTest.php b/tests/system/EnvironmentDetectorTest.php index cd334c34f7b8..8a379888ace1 100644 --- a/tests/system/EnvironmentDetectorTest.php +++ b/tests/system/EnvironmentDetectorTest.php @@ -121,8 +121,8 @@ public static function provideBuiltInEnvironmentHelpers(): iterable public function testResolvesAsSharedService(): void { - $first = service('environmentdetector'); - $second = service('environmentdetector'); + $first = service('envdetector'); + $second = service('envdetector'); $this->assertInstanceOf(EnvironmentDetector::class, $first); $this->assertSame($first, $second); diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index 5fedad36b8f0..acbfdb562170 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -37,7 +37,7 @@ protected function setUp(): void protected function tearDown(): void { parent::tearDown(); - Services::resetSingle('environmentdetector'); + Services::resetSingle('envdetector'); } /** @@ -73,7 +73,7 @@ public function testJSONFormatterThrowsError(): void public function testFormattingToJsonIsCompactInProduction(): void { - Services::injectMock('environmentdetector', new EnvironmentDetector('production')); + Services::injectMock('envdetector', new EnvironmentDetector('production')); $this->assertSame('{"foo":"bar"}', $this->jsonFormatter->format(['foo' => 'bar'])); } diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index 50fd91575aec..1d3a6dab3b53 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -156,8 +156,9 @@ protected function setUp(): void protected function tearDown(): void { parent::tearDown(); + service('superglobals')->setFilesArray([]); - Services::resetSingle('environmentdetector'); + Services::resetSingle('envdetector'); } public function testUploadedPassesForSingleValidFile(): void @@ -175,7 +176,7 @@ public function testUploadedFailsInProductionWhenFileWasNotHttpUpload(): void // enforces is_uploaded_file(). Runs in a separate process because the // namespace-level is_uploaded_file() override in FileMovingTest.php would // otherwise leak in and make the fixture appear to be a valid upload. - Services::injectMock('environmentdetector', new EnvironmentDetector('production')); + Services::injectMock('envdetector', new EnvironmentDetector('production')); $this->validation->setRules(['avatar' => 'uploaded[avatar]']); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index a835dfa51072..a738e9cae02e 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -263,7 +263,7 @@ Others - **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities. - Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided. -- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environmentdetector`` service as a mockable wrapper around the ``ENVIRONMENT`` constant. +- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``envdetector`` service as a mockable wrapper around the ``ENVIRONMENT`` constant. Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`. *************** diff --git a/user_guide_src/source/general/environments.rst b/user_guide_src/source/general/environments.rst index 7897518b0e45..f5b5a35c3f1a 100644 --- a/user_guide_src/source/general/environments.rst +++ b/user_guide_src/source/general/environments.rst @@ -147,20 +147,27 @@ You can also check the current environment by ``spark env`` command: .. _environment-detector-service: -The ``environmentdetector`` service -=================================== +The ``envdetector`` service +=========================== .. versionadded:: 4.8.0 As an alternative to reading the ``ENVIRONMENT`` constant directly, CodeIgniter -provides the ``environmentdetector`` service, backed by the -:php:class:`CodeIgniter\\EnvironmentDetector` class. Because it is a shared -service, it can be mocked in tests (via ``Services::injectMock()``) to exercise +provides the ``envdetector`` service, backed by the +``CodeIgniter\EnvironmentDetector`` class. Because it is a shared service, it +can be mocked in tests (via ``Services::injectMock()``) to exercise environment-specific branches without having to redefine the ``ENVIRONMENT`` constant. .. literalinclude:: environments/001.php +.. note:: + + The ``envdetector`` service is primarily intended for testing + environment-specific code paths. Mocking it only affects code that resolves + and uses the service itself. It does not modify the ``ENVIRONMENT`` constant. + Code that still reads ``ENVIRONMENT`` directly keeps its current behavior. + Passing a value to the constructor overrides the detected environment; passing ``null`` (the default) falls back to the ``ENVIRONMENT`` constant. An empty or whitespace-only string throws ``CodeIgniter\Exceptions\InvalidArgumentException``. diff --git a/user_guide_src/source/general/environments/001.php b/user_guide_src/source/general/environments/001.php index 916395f89469..95d277275021 100644 --- a/user_guide_src/source/general/environments/001.php +++ b/user_guide_src/source/general/environments/001.php @@ -1,6 +1,6 @@ get();