From 148c23acd389c230fb6ddb9d169fe6800d977bf4 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 31 Jul 2026 21:37:12 +0200 Subject: [PATCH 1/3] fix: treat a vanished .env file as absent instead of throwing The .env file may be removed or replaced by a concurrent process between the is_file() check and the read attempt, e.g. another test process renaming ROOTPATH/.env. The previous is_readable() pre-check performed a fresh access() syscall that could fail on a just-renamed file, crashing CI with 'The .env file is not readable'. Read the file first and only throw when it still exists after a re-check with a fresh stat cache. --- system/Config/DotEnv.php | 19 ++++++--- tests/system/Config/DotEnvTest.php | 62 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 6778c44e0d8d..3667d9c96430 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -59,15 +59,24 @@ public function parse(): ?array return null; } - // Ensure the file is readable - if (! is_readable($this->path)) { - throw new InvalidArgumentException("The .env file is not readable: {$this->path}"); + $lines = @file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + // The .env file may have been removed or replaced by a concurrent + // process between the is_file() check above and this read attempt + // (e.g. another test process renaming `.env`). A vanished file is + // treated as absent, so re-check with a fresh stat cache. + if ($lines === false) { + clearstatcache(); + + if (is_file($this->path)) { + throw new InvalidArgumentException("The .env file is not readable: {$this->path}"); + } + + return null; } $vars = []; - $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { // Is it a comment? if (str_starts_with(trim($line), '#')) { diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index c789169b1b87..58e66d412e9e 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -143,6 +143,68 @@ public function testLoadsUnreadableFile(): void $dotenv->load(); } + /** + * Regression test: a concurrent process may remove or replace the .env + * file between the is_file() check and the read attempt. In that case the + * file must be treated as absent (parse() returns null) instead of + * throwing an exception. + */ + public function testParseReturnsNullIfFileRemovedBetweenCheckAndRead(): void + { + $scheme = 'vanishenv'; + + $wrapper = new class () { + public static bool $vanished = false; + + /** + * @var resource|null + */ + public $context; + + /** + * @return array|false + */ + public function url_stat(string $path, int $flags): array|false + { + if (self::$vanished) { + return false; + } + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0100644, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 1, + 'atime' => 1, + 'mtime' => 1, + 'ctime' => 1, + 'blksize' => 4096, + 'blocks' => 8, + ]; + } + + public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool + { + self::$vanished = true; + + return false; + } + }; + + stream_wrapper_register($scheme, $wrapper::class, STREAM_IS_URL); + + try { + $dotenv = new DotEnv("{$scheme}://dir", '.env'); + $this->assertNull($dotenv->parse()); + } finally { + stream_wrapper_unregister($scheme); + } + } + public function testQuotedDotenvLoadsEnvironmentVars(): void { $dotenv = new DotEnv($this->fixturesFolder, 'quoted.env'); From 64a729b172f4e451855e4eaf1bbca0db21ee64f6 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Tue, 11 Aug 2026 00:04:45 +0200 Subject: [PATCH 2/3] Update system/Config/DotEnv.php Co-authored-by: Michal Sniatala --- system/Config/DotEnv.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 3667d9c96430..bf1d866f6d93 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -66,7 +66,7 @@ public function parse(): ?array // (e.g. another test process renaming `.env`). A vanished file is // treated as absent, so re-check with a fresh stat cache. if ($lines === false) { - clearstatcache(); + clearstatcache(true, $this->path); if (is_file($this->path)) { throw new InvalidArgumentException("The .env file is not readable: {$this->path}"); From 18d262b7ea32317257e64167ba2e35bd42dc6d4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:30:55 +0700 Subject: [PATCH 3/3] chore(deps-dev): update rector/rector requirement (#10437) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.5.9 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.8...2.5.9) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.5.9 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- rector.php | 18 ------------------ system/Router/Router.php | 6 +++--- utils/phpstan-baseline/empty.notAllowed.neon | 4 ++-- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +------ 6 files changed, 8 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 9cabb129ff8c..27b87b9e709c 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.8", + "rector/rector": "2.5.9", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index cd5f3c21dd0f..856952b19206 100644 --- a/rector.php +++ b/rector.php @@ -33,10 +33,7 @@ use Rector\Php70\Rector\FuncCall\RandomFunctionRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; -use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; -use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; @@ -107,13 +104,6 @@ // Keep property defaults for backward compatibility. RemoveDefaultValueFromAssignedPropertyRector::class, - ReadOnlyPropertyRector::class => [ - __DIR__ . '/system/Cache/ResponseCache.php', - __DIR__ . '/system/HotReloader/IteratorFilter.php', - __DIR__ . '/system/Router/RouteCollection.php', - __DIR__ . '/system/Security/Security.php', - ], - // Exclude test file because `is_cli()` is mocked and Rector might remove needed parameters. RemoveExtraParametersRector::class => [ __DIR__ . '/tests/system/Debug/ToolbarTest.php', @@ -160,19 +150,11 @@ __DIR__ . '/system/HTTP/SiteURI.php', ], - // Unnecessary (string) is inserted - NullToStrictStringFuncCallArgRector::class, - CompactToVariablesRector::class, // possibly isset() on purpose, on updated Config classes property across versions IssetOnPropertyObjectToPropertyExistsRector::class, - AssertFuncCallToPHPUnitAssertRector::class => [ - // use $this inside static closure - __DIR__ . '/tests/system/AutoReview/FrameworkCodeTest.php', - ], - // some tests extended by other tests FinalizeTestCaseClassRector::class, diff --git a/system/Router/Router.php b/system/Router/Router.php index 063bb5074cbb..1eda4c616d62 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -694,7 +694,7 @@ private function isValidSegment(string $segment): bool * Takes an array of URI segments as input and sets the class/method * to be called. * - * @param array $segments URI segments + * @param list $segments URI segments * * @return void */ @@ -705,13 +705,13 @@ protected function setRequest(array $segments = []) return; } - [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null); + [$controller, $method] = explode('::', $segments[0], 2) + [null, null]; $this->controller = $controller; // $this->method already contains the default method name, // so don't overwrite it with emptiness. - if (! empty($method)) { + if ($method !== null && $method !== '') { $this->method = $method; } diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 796292761d9c..9060c95a91ec 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 205 errors +# total 204 errors parameters: ignoreErrors: @@ -279,7 +279,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 + count: 1 path: ../../system/Router/Router.php - diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 03e4965a51a4..32ad4dacc2f5 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1810 errors +# total 1808 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 677e7101b2c3..28aeaabd8f58 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1140 errors +# total 1139 errors parameters: ignoreErrors: @@ -3637,11 +3637,6 @@ parameters: count: 1 path: ../../system/Router/Router.php - - - message: '#^Method CodeIgniter\\Router\\Router\:\:setRequest\(\) has parameter \$segments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - message: '#^Method CodeIgniter\\Router\\Router\:\:validateRequest\(\) has parameter \$segments with no value type specified in iterable type array\.$#' count: 1