Skip to content
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 5 additions & 6 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<php>
<ini name="error_reporting" value="E_ALL &amp; ~E_DEPRECATED"/>
<ini name="error_reporting" value="-1"/>
</php>

<testsuites>
Expand All @@ -19,9 +18,9 @@
</testsuite>
</testsuites>

<coverage processUncoveredFiles="true">
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>
</source>
</phpunit>
1 change: 1 addition & 0 deletions src/Console/Commands/EnvCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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?')) {
Expand Down
2 changes: 1 addition & 1 deletion src/Debugger/Debugger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== []);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/Session/Traits/SessionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Storage/Adapters/Dropbox/DropboxFileSystemAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function testResolveEnvironmentStageThrowsTheParseExceptionWhenTheEnvFile
/**
* @return array<string, array{string}>
*/
public function commandsThatRunWithoutTheEnvironment(): array
public static function commandsThatRunWithoutTheEnvironment(): array
{
return [
'a CliCommand that does not require the environment' => ['no:env'],
Expand All @@ -101,7 +101,7 @@ public function commandsThatRunWithoutTheEnvironment(): array
/**
* @return array<string, array{array<string, string>}>
*/
public function inputsThatNeedTheEnvironment(): array
public static function inputsThatNeedTheEnvironment(): array
{
return [
'a CliCommand that requires the environment' => [['command' => 'needs:env']],
Expand Down
3 changes: 3 additions & 0 deletions tests/Unit/AppTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public function tearDown(): void
}

$this->clearAppContext();

restore_error_handler();
restore_exception_handler();
}

protected function createContext(string $type = AppType::WEB): AppContext
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/Console/Commands/EnvCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Console/Commands/OpenApiCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public function testExecCreatesTheResourcesFoldersOfAModuleWithoutThem(): void
mkdir($controllersDir, 0777, true);

file_put_contents($routesDir . DS . 'routes.php', "<?php\n\nreturn function (\$route) {\n};\n");
file_put_contents($controllersDir . DS . 'SpecController.php', "<?php\n\nuse OpenApi\\Attributes as OA;\n\n#[OA\\Info(title: 'Spec', version: '1.0.0')]\nclass " . $className . "\n{\n}\n");
file_put_contents($controllersDir . DS . 'SpecController.php', "<?php\n\nuse OpenApi\\Attributes as OA;\n\n#[OA\\Info(title: 'Spec', version: '1.0.0')]\nclass " . $className . "\n{\n #[OA\\Get(path: '/spec', responses: [new OA\\Response(response: 200, description: 'OK')])]\n public function index(): void\n {\n }\n}\n");

require_once $controllersDir . DS . 'SpecController.php';

Expand Down Expand Up @@ -271,7 +271,7 @@ public function error(string $message): void

mkdir($annotationDir, 0777, true);
mkdir($specDir, 0777, true);
file_put_contents($annotationDir . DS . 'SpecController.php', "<?php\n\nuse OpenApi\\Attributes as OA;\n\n#[OA\\Info(title: 'Spec', version: '1.0.0')]\nclass " . $className . "\n{\n}\n");
file_put_contents($annotationDir . DS . 'SpecController.php', "<?php\n\nuse OpenApi\\Attributes as OA;\n\n#[OA\\Info(title: 'Spec', version: '1.0.0')]\nclass " . $className . "\n{\n #[OA\\Get(path: '/spec', responses: [new OA\\Response(response: 200, description: 'OK')])]\n public function index(): void\n {\n }\n}\n");

require_once $annotationDir . DS . 'SpecController.php';

Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/HttpClient/ResponseHeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function testResponseHeadersDeclaresTheReturnTypeOfItsInterfaceMethods(st
/**
* @return array<string, array{0: string}>
*/
public function interfaceMethodProvider(): array
public static function interfaceMethodProvider(): array
{
return [
'offsetSet' => ['offsetSet'],
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Logger/LoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function __toString(): string
/**
* @return array<string, array{0: string, 1: string}>
*/
public function levelMethodProvider(): array
public static function levelMethodProvider(): array
{
return [
'emergency' => ['emergency', LogLevel::EMERGENCY],
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Mailer/MailerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public function testHttpAdapterGetTransportErrorsReturnsArray(string $adapterCla
/**
* @return array<string, array{0: class-string, 1: array<string, mixed>}>
*/
public function httpAdapterProvider(): array
public static function httpAdapterProvider(): array
{
return [
'mailgun' => [MailgunAdapter::class, ['api_key' => 'test', 'domain' => 'test.com']],
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Module/Templates/OpenApiTemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function testApiTemplatesDescribeTheApiWithAttributesOnly(string $templat
/**
* @return array<string, array{0: string}>
*/
public function apiTemplateProvider(): array
public static function apiTemplateProvider(): array
{
return [
'default-api' => ['DefaultApi'],
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Module/Templates/RedirectTemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function testTemplatesGoBackWithTheBackRedirect(string $template): void
/**
* @return array<string, array{0: string}>
*/
public function redirectTemplateProvider(): array
public static function redirectTemplateProvider(): array
{
return [
'demo-web' => ['DemoWeb'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private function insertSession(string $id, int $ttl): void
/**
* @return array<string, array{0: string}>
*/
public function interfaceMethodProvider(): array
public static function interfaceMethodProvider(): array
{
return [
'open' => ['open'],
Expand Down
17 changes: 17 additions & 0 deletions tests/Unit/Session/Adapters/Native/NativeSessionAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ public function testDropboxListDirectory(): void

self::$response = [];

$this->assertNull($this->fs->listDirectory('test'));
$this->assertFalse($this->fs->listDirectory('test'));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public function testGoogleDriveListDirectory(): void

self::$response = [];

$this->assertNull($this->fs->listDirectory('test'));
$this->assertFalse($this->fs->listDirectory('test'));

}
}
4 changes: 2 additions & 2 deletions tests/Unit/Tracer/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public function testLogErrorFallsBackToErrorMethodForUnknownLevel(): void
/**
* @return array<string, array{0: int}>
*/
public function deprecationSeverityProvider(): array
public static function deprecationSeverityProvider(): array
{
return [
'deprecated' => [E_DEPRECATED],
Expand All @@ -299,7 +299,7 @@ public function testGetErrorTypeMappings(\Throwable $throwable, string $expected
/**
* @return array<string, array{0: \Throwable, 1: string}>
*/
public function errorTypeProvider(): array
public static function errorTypeProvider(): array
{
return [
'mapped-error-exception' => [new ErrorException('x', 0, E_WARNING), 'warning'],
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Tracer/ExceptionSeverityResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function testResolveMappings(\Throwable $throwable, string $expected): vo
/**
* @return array<string, array{0: \Throwable, 1: string}>
*/
public function mappingProvider(): array
public static function mappingProvider(): array
{
return [
'mapped-error-exception' => [new ErrorException('x', 0, E_WARNING), 'warning'],
Expand Down
2 changes: 0 additions & 2 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

error_reporting(E_ALL & ~E_DEPRECATED);

if (!defined('DS')) {
define('DS', DIRECTORY_SEPARATOR);
}
Expand Down
Loading