diff --git a/CHANGELOG.md b/CHANGELOG.md
index 069a23a1c..2009671dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,12 @@ The format is based on Keep a Changelog.
### Changed
- The `DemoWeb` and `Toolkit` module templates redirect back with `back()` instead of `redirect(get_referrer() ?? base_url())` and `redirectWith(get_referrer() ?? base_url(), $data)`, so generated code gets the same-origin check. Modules generated before keep their code (#541)
- Twig is a required dependency again: `twig/twig` `^3.27` is in `require` and no longer only suggested, which reverses the 3.0.3 change that made it optional while the framework supported PHP 8.0 (#532). Twig `^3.27` needs PHP 8.1, the minimum since 3.1.0. An application that pins an older Twig must allow `^3.27`. Removed `RendererException::twigNotInstalled()` and the check in `TwigAdapter` that threw it when Twig was missing, and the Twig tests no longer skip (#626)
+- The test suite runs on PHPUnit `^10.5` (a development dependency) and `phpunit.xml` uses its schema. It now sets `error_reporting` to `-1`: it was the text `E_ALL & ~E_DEPRECATED`, which PHP reads as `0`, so every PHP warning was hidden in the tests (#622)
+
+### Fixed
+- `php qt core:env` no longer prints `.env is successfully generated` and no longer tries to copy the file when `.env.example` is missing: it stops after the error message (#622)
+- `flush()` of the session adapters no longer raises a warning when no session is active, so calling it twice, or before a session was started, no longer throws (#622)
+- `listDirectory()` of the Dropbox and Google Drive adapters returns `false`, as its contract says, when the API response has no `entries` or `files`. It could return `null` when PHP warnings were not reported (#622)
## [3.1.0] - 2026-10-24
diff --git a/composer.json b/composer.json
index 9858c62f4..a1350314f 100644
--- a/composer.json
+++ b/composer.json
@@ -56,7 +56,7 @@
"twig/twig": "^3.27"
},
"require-dev": {
- "phpunit/phpunit": "^9.0",
+ "phpunit/phpunit": "^10.5",
"mockery/mockery": "^1.4",
"mikey179/vfsstream": "^1.6",
"rector/rector": "^2.3",
diff --git a/phpunit.xml b/phpunit.xml
index 3b2122c21..2c5efdfa3 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -4,13 +4,12 @@
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
- verbose="true"
stderr="true"
stopOnFailure="false"
- convertDeprecationsToExceptions="false"
+ cacheDirectory=".phpunit.cache"
>
-
+
@@ -19,9 +18,9 @@
-
+
./src
-
-
\ No newline at end of file
+
+
diff --git a/src/Console/Commands/EnvCommand.php b/src/Console/Commands/EnvCommand.php
index 171bf7ea7..5c7194ba3 100644
--- a/src/Console/Commands/EnvCommand.php
+++ b/src/Console/Commands/EnvCommand.php
@@ -64,6 +64,7 @@ public function exec(): void
if (!$fs->exists(App::getBaseDir() . DS . '.env.example')) {
$this->error('.env.example file not found');
+ return;
}
if ($fs->exists(App::getBaseDir() . DS . '.env') && !$this->getOption('yes') && !$this->confirm('The operation will overwrite values of the existing .env and will create new one from .env.example. Continue?')) {
diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php
index a4765b25d..583f2f6e3 100644
--- a/src/Debugger/Debugger.php
+++ b/src/Debugger/Debugger.php
@@ -249,7 +249,7 @@ protected function routeInfo(): array
$info['View'] = request()->getCurrentModule() . '/Views/' . $viewFile;
}
- return array_filter($info, fn ($value): bool => $value !== null && $value !== []);
+ return array_filter($info, fn (string|array|null $value): bool => $value !== null && $value !== []);
}
/**
diff --git a/src/Session/Traits/SessionTrait.php b/src/Session/Traits/SessionTrait.php
index 31f584297..480d8e5ad 100644
--- a/src/Session/Traits/SessionTrait.php
+++ b/src/Session/Traits/SessionTrait.php
@@ -103,7 +103,10 @@ public function delete(string $key): void
public function flush(): void
{
self::$storage = [];
- session_destroy();
+
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ session_destroy();
+ }
}
/**
diff --git a/src/Storage/Adapters/Dropbox/DropboxFileSystemAdapter.php b/src/Storage/Adapters/Dropbox/DropboxFileSystemAdapter.php
index a70ebbd87..480701bea 100644
--- a/src/Storage/Adapters/Dropbox/DropboxFileSystemAdapter.php
+++ b/src/Storage/Adapters/Dropbox/DropboxFileSystemAdapter.php
@@ -210,7 +210,7 @@ public function listDirectory(string $dirname)
{
try {
$response = (array) $this->dropboxApp->rpcRequest(DropboxApp::ENDPOINT_LIST_FOLDER, $this->dropboxApp->path($dirname));
- return $response['entries'];
+ return $response['entries'] ?? false;
} catch (Exception) {
return false;
}
diff --git a/src/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapter.php b/src/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapter.php
index d49298298..1f18e061e 100644
--- a/src/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapter.php
+++ b/src/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapter.php
@@ -235,7 +235,7 @@ public function listDirectory(string $dirname)
'fields' => '*',
];
$response = (array) $this->googleDriveApp->rpcRequest(GoogleDriveApp::FILE_METADATA_URL . '?' . http_build_query($params), [], 'GET');
- return $response['files'];
+ return $response['files'] ?? false;
} catch (Exception) {
return false;
}
diff --git a/tests/Unit/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStageTest.php b/tests/Unit/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStageTest.php
index 8c7216e0e..63c04809a 100644
--- a/tests/Unit/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStageTest.php
+++ b/tests/Unit/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStageTest.php
@@ -89,7 +89,7 @@ public function testResolveEnvironmentStageThrowsTheParseExceptionWhenTheEnvFile
/**
* @return array
*/
- public function commandsThatRunWithoutTheEnvironment(): array
+ public static function commandsThatRunWithoutTheEnvironment(): array
{
return [
'a CliCommand that does not require the environment' => ['no:env'],
@@ -101,7 +101,7 @@ public function commandsThatRunWithoutTheEnvironment(): array
/**
* @return array}>
*/
- public function inputsThatNeedTheEnvironment(): array
+ public static function inputsThatNeedTheEnvironment(): array
{
return [
'a CliCommand that requires the environment' => [['command' => 'needs:env']],
diff --git a/tests/Unit/AppTestCase.php b/tests/Unit/AppTestCase.php
index 7ac1fed5e..2c92b6e6a 100644
--- a/tests/Unit/AppTestCase.php
+++ b/tests/Unit/AppTestCase.php
@@ -55,6 +55,9 @@ public function tearDown(): void
}
$this->clearAppContext();
+
+ restore_error_handler();
+ restore_exception_handler();
}
protected function createContext(string $type = AppType::WEB): AppContext
diff --git a/tests/Unit/Console/Commands/EnvCommandTest.php b/tests/Unit/Console/Commands/EnvCommandTest.php
index 717b7b00b..c71e34a65 100644
--- a/tests/Unit/Console/Commands/EnvCommandTest.php
+++ b/tests/Unit/Console/Commands/EnvCommandTest.php
@@ -109,6 +109,7 @@ public function testExecShowsErrorWhenEnvExampleMissing(): void
$output = $this->tester->getDisplay();
$this->assertStringContainsString('.env.example file not found', $output);
+ $this->assertStringNotContainsString('.env is successfully generated', $output);
} finally {
if ($existed && file_exists($backup)) {
rename($backup, $envExample);
diff --git a/tests/Unit/Console/Commands/OpenApiCommandTest.php b/tests/Unit/Console/Commands/OpenApiCommandTest.php
index 6d337dd04..60f025853 100644
--- a/tests/Unit/Console/Commands/OpenApiCommandTest.php
+++ b/tests/Unit/Console/Commands/OpenApiCommandTest.php
@@ -216,7 +216,7 @@ public function testExecCreatesTheResourcesFoldersOfAModuleWithoutThem(): void
mkdir($controllersDir, 0777, true);
file_put_contents($routesDir . DS . 'routes.php', "
*/
- public function interfaceMethodProvider(): array
+ public static function interfaceMethodProvider(): array
{
return [
'offsetSet' => ['offsetSet'],
diff --git a/tests/Unit/Logger/LoggerTest.php b/tests/Unit/Logger/LoggerTest.php
index aa40beab8..281f89325 100644
--- a/tests/Unit/Logger/LoggerTest.php
+++ b/tests/Unit/Logger/LoggerTest.php
@@ -70,7 +70,7 @@ public function __toString(): string
/**
* @return array
*/
- public function levelMethodProvider(): array
+ public static function levelMethodProvider(): array
{
return [
'emergency' => ['emergency', LogLevel::EMERGENCY],
diff --git a/tests/Unit/Mailer/MailerTest.php b/tests/Unit/Mailer/MailerTest.php
index bc9737abf..5d2319e60 100644
--- a/tests/Unit/Mailer/MailerTest.php
+++ b/tests/Unit/Mailer/MailerTest.php
@@ -77,7 +77,7 @@ public function testHttpAdapterGetTransportErrorsReturnsArray(string $adapterCla
/**
* @return array}>
*/
- public function httpAdapterProvider(): array
+ public static function httpAdapterProvider(): array
{
return [
'mailgun' => [MailgunAdapter::class, ['api_key' => 'test', 'domain' => 'test.com']],
diff --git a/tests/Unit/Module/Templates/OpenApiTemplatesTest.php b/tests/Unit/Module/Templates/OpenApiTemplatesTest.php
index 6cd2737c5..a7cc00bb7 100644
--- a/tests/Unit/Module/Templates/OpenApiTemplatesTest.php
+++ b/tests/Unit/Module/Templates/OpenApiTemplatesTest.php
@@ -81,7 +81,7 @@ public function testApiTemplatesDescribeTheApiWithAttributesOnly(string $templat
/**
* @return array
*/
- public function apiTemplateProvider(): array
+ public static function apiTemplateProvider(): array
{
return [
'default-api' => ['DefaultApi'],
diff --git a/tests/Unit/Module/Templates/RedirectTemplatesTest.php b/tests/Unit/Module/Templates/RedirectTemplatesTest.php
index 62053abd3..de61a72ef 100644
--- a/tests/Unit/Module/Templates/RedirectTemplatesTest.php
+++ b/tests/Unit/Module/Templates/RedirectTemplatesTest.php
@@ -43,7 +43,7 @@ public function testTemplatesGoBackWithTheBackRedirect(string $template): void
/**
* @return array
*/
- public function redirectTemplateProvider(): array
+ public static function redirectTemplateProvider(): array
{
return [
'demo-web' => ['DemoWeb'],
diff --git a/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php b/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php
index 398d7f99e..e06c56c56 100644
--- a/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php
+++ b/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php
@@ -85,7 +85,7 @@ private function insertSession(string $id, int $ttl): void
/**
* @return array
*/
- public function interfaceMethodProvider(): array
+ public static function interfaceMethodProvider(): array
{
return [
'open' => ['open'],
diff --git a/tests/Unit/Session/Adapters/Native/NativeSessionAdapterTest.php b/tests/Unit/Session/Adapters/Native/NativeSessionAdapterTest.php
index b48f51c9e..c33249aec 100644
--- a/tests/Unit/Session/Adapters/Native/NativeSessionAdapterTest.php
+++ b/tests/Unit/Session/Adapters/Native/NativeSessionAdapterTest.php
@@ -81,6 +81,23 @@ public function testNativeSessionFlush(): void
session_start();
}
+ public function testNativeSessionFlushWithoutAnActiveSessionDoesNotWarn(): void
+ {
+ $oldLevel = error_reporting(E_ALL);
+
+ try {
+ $this->session->flush();
+
+ $this->session->flush();
+
+ $this->assertEmpty($this->session->all());
+ } finally {
+ error_reporting($oldLevel);
+
+ @session_start();
+ }
+ }
+
public function testNativeSessionGetSessionId(): void
{
$this->assertEquals(session_id(), $this->session->getId());
diff --git a/tests/Unit/Storage/Adapters/Dropbox/DropboxFileSystemAdapterTest.php b/tests/Unit/Storage/Adapters/Dropbox/DropboxFileSystemAdapterTest.php
index 07a80a9bd..dea5e6859 100644
--- a/tests/Unit/Storage/Adapters/Dropbox/DropboxFileSystemAdapterTest.php
+++ b/tests/Unit/Storage/Adapters/Dropbox/DropboxFileSystemAdapterTest.php
@@ -218,7 +218,7 @@ public function testDropboxListDirectory(): void
self::$response = [];
- $this->assertNull($this->fs->listDirectory('test'));
+ $this->assertFalse($this->fs->listDirectory('test'));
}
}
diff --git a/tests/Unit/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapterTest.php b/tests/Unit/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapterTest.php
index 90b53c116..5b685fb78 100644
--- a/tests/Unit/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapterTest.php
+++ b/tests/Unit/Storage/Adapters/GoogleDrive/GoogleDriveFileSystemAdapterTest.php
@@ -199,7 +199,7 @@ public function testGoogleDriveListDirectory(): void
self::$response = [];
- $this->assertNull($this->fs->listDirectory('test'));
+ $this->assertFalse($this->fs->listDirectory('test'));
}
}
diff --git a/tests/Unit/Tracer/ErrorHandlerTest.php b/tests/Unit/Tracer/ErrorHandlerTest.php
index 5f57f34f1..89487c2cf 100644
--- a/tests/Unit/Tracer/ErrorHandlerTest.php
+++ b/tests/Unit/Tracer/ErrorHandlerTest.php
@@ -278,7 +278,7 @@ public function testLogErrorFallsBackToErrorMethodForUnknownLevel(): void
/**
* @return array
*/
- public function deprecationSeverityProvider(): array
+ public static function deprecationSeverityProvider(): array
{
return [
'deprecated' => [E_DEPRECATED],
@@ -299,7 +299,7 @@ public function testGetErrorTypeMappings(\Throwable $throwable, string $expected
/**
* @return array
*/
- public function errorTypeProvider(): array
+ public static function errorTypeProvider(): array
{
return [
'mapped-error-exception' => [new ErrorException('x', 0, E_WARNING), 'warning'],
diff --git a/tests/Unit/Tracer/ExceptionSeverityResolverTest.php b/tests/Unit/Tracer/ExceptionSeverityResolverTest.php
index c60981885..ae35eff71 100644
--- a/tests/Unit/Tracer/ExceptionSeverityResolverTest.php
+++ b/tests/Unit/Tracer/ExceptionSeverityResolverTest.php
@@ -29,7 +29,7 @@ public function testResolveMappings(\Throwable $throwable, string $expected): vo
/**
* @return array
*/
- public function mappingProvider(): array
+ public static function mappingProvider(): array
{
return [
'mapped-error-exception' => [new ErrorException('x', 0, E_WARNING), 'warning'],
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index ccada9cec..5c41896ec 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,7 +1,5 @@