Repository: yiisoft/yii-middleware
Pull request #129: PHP 8.4 support
Author: login: DAGpro
Created: 2025-04-14T03:28:23Z | Merged: 2025-04-19T20:16:21Z

| Q             | A
| ------------- | ---
| Is bugfix?    | ❌
| New feature?  | ✔️
| Breaks BC?    | ✔️/❌
| Fixed issues  | <!-- comma-separated list of tickets # fixed by the PR, if any -->



Diff:
diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml
index 1a18d1b..61397fb 100644
--- a/.github/workflows/bc.yml
+++ b/.github/workflows/bc.yml
@@ -31,4 +31,4 @@ jobs:
       os: >-
         ['ubuntu-latest']
       php: >-
-        ['8.1']
+        ['8.3']
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c7b7f05..01430b9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,10 +35,10 @@ jobs:
           - windows-latest
 
         php:
-          - 8.0
           - 8.1
           - 8.2
           - 8.3
+          - 8.4
 
         exclude:
           - os: windows-latest
diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml
index c92b26e..04304be 100644
--- a/.github/workflows/composer-require-checker.yml
+++ b/.github/workflows/composer-require-checker.yml
@@ -31,5 +31,5 @@ jobs:
       os: >-
         ['ubuntu-latest']
       php: >-
-        ['8.0', '8.1', '8.2', '8.3']
+        ['8.1', '8.2', '8.3', '8.4']
       extensions: uopz
diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml
index bfc443a..9f50d9b 100644
--- a/.github/workflows/mutation.yml
+++ b/.github/workflows/mutation.yml
@@ -28,7 +28,7 @@ jobs:
       os: >-
         ['ubuntu-latest']
       php: >-
-        ['8.2']
+        ['8.3']
       extensions: uopz
     secrets:
       STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml
index f5da663..5d8c2e0 100644
--- a/.github/workflows/rector.yml
+++ b/.github/workflows/rector.yml
@@ -21,5 +21,5 @@ jobs:
       os: >-
         ['ubuntu-latest']
       php: >-
-        ['8.3']
+        ['8.4']
       extensions: uopz
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 3e5f87a..d5e46c3 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -30,14 +30,5 @@ jobs:
       os: >-
         ['ubuntu-latest']
       php: >-
-        ['8.1', '8.2', '8.3']
-      extensions: uopz
-  psalm80:
-    uses: yiisoft/actions/.github/workflows/psalm.yml@master
-    with:
-      psalm-config: psalm-8.0.xml
-      os: >-
-        ['ubuntu-latest']
-      php: >-
-        ['8.0']
+        ['8.1', '8.2', '8.3', '8.4']
       extensions: uopz
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d14b1da..e68d4d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 ## 1.0.5 under development
 
+- Chg #129: Bump PHP minimal version to 8.1 and refactor code to use new features (@dagpro)
+- Chg #129: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
+- Chg #129: Bump `yiisoft/router` version to `^4.0` (@dagpro)
+- Chg #129: Bump `yiisoft/session` version to `^3.0` (@dagpro)
+- Bug #129: Explicitly mark nullable parameters (@dagpro)
 - Chg #130: Bump minimal version of `yiisoft/cookie` to `^1.2.3` (@vjik)
 - Enh #130: Allow to use PSR-20 clock interface to get current time into `Locale` middleware (@vjik)
 
diff --git a/README.md b/README.md
index fee1ad0..ee8de8e 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ For more information on how to use middleware in the [Yii Framework](https://www
 
 ## Requirements
 
-- PHP 8.0 or higher.
+- PHP 8.1 or higher.
 
 ## Installation
 
diff --git a/composer.json b/composer.json
index b29eeab..2893f3a 100644
--- a/composer.json
+++ b/composer.json
@@ -29,33 +29,33 @@
     ],
     "prefer-stable": true,
     "require": {
-        "php": "^8.0",
+        "php": "8.1 - 8.4",
         "psr/event-dispatcher": "^1.0",
         "psr/http-factory": "^1.0",
-        "psr/log": "^3.0",
         "psr/http-message": "^1.0|^2.0",
         "psr/http-server-handler": "^1.0",
         "psr/http-server-middleware": "^1.0",
+        "psr/log": "^3.0",
         "yiisoft/aliases": "^3.0",
         "yiisoft/cookies": "^1.2.3",
         "yiisoft/friendly-exception": "^1.0",
         "yiisoft/http": "^1.2",
         "yiisoft/network-utilities": "^1.2",
-        "yiisoft/router": "^3.0",
-        "yiisoft/session": "^2.0",
+        "yiisoft/router": "^4.0",
+        "yiisoft/session": "^3.0",
         "yiisoft/strings": "^2.1",
         "yiisoft/validator": "^1.0|^2.0"
     },
     "require-dev": {
         "httpsoft/http-message": "^1.0",
         "maglnet/composer-require-checker": "^4.2",
-        "phpunit/phpunit": "^9.5",
-        "psr/clock": "^1.0",
-        "rector/rector": "^1.2",
+        "phpunit/phpunit": "^10.5.45",
+        "rector/rector": "^2.0.10",
         "roave/infection-static-analysis-plugin": "^1.16",
+        "psr/clock": "^1.0",
         "spatie/phpunit-watcher": "^1.23",
-        "vimeo/psalm": "^4.30|^5.25",
-        "yiisoft/router-fastroute": "^3.0",
+        "vimeo/psalm": "^5.26.1 || ^6.8.9",
+        "yiisoft/router-fastroute": "^4.0",
         "yiisoft/test-support": "^3.0"
     },
     "suggest": {
@@ -80,7 +80,7 @@
         }
     },
     "scripts": {
-        "test": "phpunit --testdox --no-interaction",
+        "test": "phpunit --testdox",
         "test-watch": "phpunit-watcher watch"
     }
 }
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 39893a9..89f204b 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,31 +1,23 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <phpunit
-    bootstrap="vendor/autoload.php"
-    colors="true"
-    verbose="true"
-    failOnRisky="true"
-    failOnWarning="true"
-    convertErrorsToExceptions="true"
-    convertNoticesToExceptions="true"
-    convertWarningsToExceptions="true"
-    stopOnFailure="false"
-    executionOrder="random"
-    resolveDependencies="true"
->
+        bootstrap="vendor/autoload.php"
+        colors="true"
+        failOnRisky="true"
+        failOnWarning="true"
+        stopOnFailure="false"
+        executionOrder="random"
+        resolveDependencies="true">
     <php>
         <ini name="error_reporting" value="-1"/>
     </php>
-
     <testsuites>
         <testsuite name="Yii Middleware tests">
             <directory>./tests</directory>
         </testsuite>
     </testsuites>
-
-    <coverage>
+    <source>
         <include>
             <directory>./src</directory>
         </include>
-    </coverage>
+    </source>
 </phpunit>
diff --git a/psalm-8.0.xml b/psalm-8.0.xml
deleted file mode 100644
index d091d59..0000000
--- a/psalm-8.0.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<psalm
-    errorLevel="1"
-    findUnusedBaselineEntry="true"
-    findUnusedCode="false"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
->
-    <projectFiles>
-        <directory name="src" />
-        <ignoreFiles>
-            <directory name="vendor" />
-        </ignoreFiles>
-    </projectFiles>
-    <issueHandlers>
-        <MixedAssignment errorLevel="suppress" />
-    </issueHandlers>
-</psalm>
diff --git a/psalm.xml b/psalm.xml
index b48c894..3b72e9f 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -1,20 +1,21 @@
 <?xml version="1.0"?>
 <psalm
-    errorLevel="1"
-    findUnusedBaselineEntry="true"
-    findUnusedCode="false"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        errorLevel="1"
+        findUnusedBaselineEntry="true"
+        findUnusedCode="false"
+        ensureOverrideAttribute="false"
+        xmlns="https://getpsalm.org/schema/config"
+        xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
 >
     <projectFiles>
-        <directory name="src" />
+        <directory name="src"/>
         <ignoreFiles>
-            <directory name="vendor" />
+            <directory name="vendor"/>
         </ignoreFiles>
     </projectFiles>
     <issueHandlers>
-        <MixedAssignment errorLevel="suppress" />
-        <RiskyTruthyFalsyComparison errorLevel="suppress" />
+        <MixedAssignment errorLevel="suppress"/>
+        <RiskyTruthyFalsyComparison errorLevel="suppress"/>
     </issueHandlers>
 </psalm>
diff --git a/rector.php b/rector.php
index f55daae..f42a1b6 100644
--- a/rector.php
+++ b/rector.php
@@ -4,24 +4,17 @@
 
 use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
 use Rector\Config\RectorConfig;
-use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
-use Rector\Set\ValueObject\LevelSetList;
+use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
 
-return static function (RectorConfig $rectorConfig): void {
-    $rectorConfig->paths([
+return RectorConfig::configure()
+    ->withPaths([
         __DIR__ . '/src',
         __DIR__ . '/tests',
+    ])
+    ->withPhpSets(php81: true)
+    ->withRules([
+        InlineConstructorDefaultToPropertyRector::class,
+    ])
+    ->withSkip([
+        NullToStrictStringFuncCallArgRector::class,
     ]);
-
-    // register a single rule
-    $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
-
-    // define sets of rules
-    $rectorConfig->sets([
-        LevelSetList::UP_TO_PHP_80,
-    ]);
-
-    $rectorConfig->skip([
-        ClosureToArrowFunctionRector::class,
-    ]);
-};
diff --git a/src/Event/SetLocaleEvent.php b/src/Event/SetLocaleEvent.php
index 93e96ba..def2de3 100644
--- a/src/Event/SetLocaleEvent.php
+++ b/src/Event/SetLocaleEvent.php
@@ -15,7 +15,7 @@ final class SetLocaleEvent
     /**
      * @param string $locale Locale determined by {@see Locale} middleware.
      */
-    public function __construct(private string $locale)
+    public function __construct(private readonly string $locale)
     {
     }
 
diff --git a/src/ForceSecureConnection.php b/src/ForceSecureConnection.php
index 2f67f52..9f5ae18 100644
--- a/src/ForceSecureConnection.php
+++ b/src/ForceSecureConnection.php
@@ -52,7 +52,7 @@ public function __construct(private ResponseFactoryInterface $responseFactory)
      * @param int $statusCode The response status code of redirection.
      * @param int|null $port The redirection port.
      */
-    public function withRedirection(int $statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
+    public function withRedirection(int $statusCode = Status::MOVED_PERMANENTLY, ?int $port = null): self
     {
         $new = clone $this;
         $new->redirect = true;
diff --git a/src/HttpCache.php b/src/HttpCache.php
index 7af730b..f7e1c2d 100644
--- a/src/HttpCache.php
+++ b/src/HttpCache.php
@@ -14,8 +14,8 @@
 
 use function base64_encode;
 use function in_array;
-use function rtrim;
 use function preg_split;
+use function rtrim;
 use function sha1;
 use function str_replace;
 use function strtotime;
diff --git a/src/IpFilter.php b/src/IpFilter.php
index 48693e2..7008de5 100644
--- a/src/IpFilter.php
+++ b/src/IpFilter.php
@@ -19,7 +19,7 @@
  */
 final class IpFilter implements MiddlewareInterface
 {
-    private IpRanges $ipRanges;
+    private readonly IpRanges $ipRanges;
 
     /**
      * @param ValidatorInterface $validator Client IP validator. The properties of the validator
@@ -37,8 +37,8 @@ public function __construct(
          * @deprecated Will be removed in version 2.0. {@see IpRanges} from `network-utilities` package is used instead.
          */
         ValidatorInterface $validator,
-        private ResponseFactoryInterface $responseFactory,
-        private ?string $clientIpAttribute = null,
+        private readonly ResponseFactoryInterface $responseFactory,
+        private readonly ?string $clientIpAttribute = null,
         array $ipRanges = [],
     ) {
         $this->ipRanges = new IpRanges($ipRanges);
diff --git a/src/Subfolder.php b/src/Subfolder.php
index 04c8b05..a9fb827 100644
--- a/src/Subfolder.php
+++ b/src/Subfolder.php
@@ -31,10 +31,10 @@ final class Subfolder implements MiddlewareInterface
      * @param string|null $baseUrlAlias The base URL alias {@see Aliases::get()}. Defaults to `@baseUrl`.
      */
     public function __construct(
-        private UrlGeneratorInterface $uriGenerator,
-        private Aliases $aliases,
-        private ?string $prefix = null,
-        private ?string $baseUrlAlias = '@baseUrl',
+        private readonly UrlGeneratorInterface $uriGenerator,
+        private readonly Aliases $aliases,
+        private readonly ?string $prefix = null,
+        private readonly ?string $baseUrlAlias = '@baseUrl',
     ) {
     }
 
diff --git a/tests/CorsAllowAllTest.php b/tests/CorsAllowAllTest.php
index b5ed307..b68b5d9 100644
--- a/tests/CorsAllowAllTest.php
+++ b/tests/CorsAllowAllTest.php
@@ -12,7 +12,7 @@
 
 final class CorsAllowAllTest extends TestCase
 {
-    public function testProcess()
+    public function testProcess(): void
     {
         $corsAllowAll = new CorsAllowAll();
         $request = $this->createMock(ServerRequestInterface::class);
@@ -20,7 +20,7 @@ public function testProcess()
         $response
             ->expects($this->any())
             ->method('withHeader')
-            ->withConsecutive(
+            ->willReturnOnConsecutiveCalls(
                 [$this->equalTo('Allow'), $this->equalTo('*')],
                 [$this->equalTo('Vary'), $this->equalTo('Origin')],
                 [$this->equalTo('Access-Control-Allow-Origin'), $this->equalTo('*')],
@@ -31,7 +31,7 @@ public function testProcess()
                 [$this->equalTo('Access-Control-Allow-Headers'), $this->equalTo('*')],
                 [$this->equalTo('Access-Control-Expose-Headers'), $this->equalTo('*')],
                 [$this->equalTo('Access-Control-Allow-Credentials'), $this->equalTo('true')],
-                [$this->equalTo('Access-Control-Max-Age'), $this->equalTo('86400')]
+                [$this->equalTo('Access-Control-Max-Age'), $this->equalTo('86400')],
             )
             ->willReturnSelf();
         $handler = $this->createMock(RequestHandlerInterface::class);
diff --git a/tests/HttpCacheTest.php b/tests/HttpCacheTest.php
index 4743cd8..3dedd1e 100644
--- a/tests/HttpCacheTest.php
+++ b/tests/HttpCacheTest.php
@@ -19,6 +19,60 @@
 
 final class HttpCacheTest extends TestCase
 {
+    public static function dataModifiedResultWithEtag(): array
+    {
+        $etagSeed = 'test-etag';
+        $expectedEtag = '"IMPoQ2/Us52fJk3jpOZtEACPlVA"';
+
+        return [
+            [$etagSeed, $etagSeed, $expectedEtag],
+            [$etagSeed, '', $expectedEtag],
+        ];
+    }
+
+    public static function dataNotModifiedResultWithEtag(): array
+    {
+        $etagSeed = 'test-etag';
+        $etagValue = 'IMPoQ2/Us52fJk3jpOZtEACPlVA';
+
+        return [
+            [$etagSeed, "\"$etagValue\""],
+            [$etagSeed, "\"$etagValue-gzip\""],
+        ];
+    }
+
+    public static function dataNotModifiedResultWithLastModified(): array
+    {
+        $time = time();
+        $ifModifiedSince = self::formatTime($time);
+
+        return [
+            [
+                $time - 1,
+                null,
+                $ifModifiedSince,
+                self::formatTime($time - 1),
+            ],
+            [
+                $time - 1,
+                'test-etag',
+                $ifModifiedSince,
+                '',
+            ],
+            [
+                $time,
+                'test-etag',
+                $ifModifiedSince,
+                '',
+            ],
+        ];
+    }
+
+    private static function formatTime(int $time): string
+    {
+        return gmdate('D, d M Y H:i:s', $time) . ' GMT';
+    }
+
     public function testNotCacheableMethods(): void
     {
         $time = time();
@@ -46,17 +100,6 @@ public function testModifiedResultWithLastModified(): void
         $this->assertSame(Status::OK, $response->getStatusCode());
     }
 
-    public function dataModifiedResultWithEtag(): array
-    {
-        $etagSeed = 'test-etag';
-        $expectedEtag = '"IMPoQ2/Us52fJk3jpOZtEACPlVA"';
-
-        return [
-            [$etagSeed, $etagSeed, $expectedEtag],
-            [$etagSeed, '', $expectedEtag],
-        ];
-    }
-
     /**
      * @dataProvider dataModifiedResultWithEtag
      */
@@ -75,17 +118,6 @@ public function testModifiedResultWithEtag(string $etagSeed, string $etagHeaderV
         $this->assertSame($response->getHeaderLine('Etag'), $expectedEtag);
     }
 
-    public function dataNotModifiedResultWithEtag(): array
-    {
-        $etagSeed = 'test-etag';
-        $etagValue = 'IMPoQ2/Us52fJk3jpOZtEACPlVA';
-
-        return [
-            [$etagSeed, "\"$etagValue\""],
-            [$etagSeed, "\"$etagValue-gzip\""],
-        ];
-    }
-
     /**
      * @dataProvider dataNotModifiedResultWithEtag
      */
@@ -101,7 +133,7 @@ public function testNotModifiedResultWIthEtag(string $etagSeed, string $etagHead
         );
 
         $this->assertSame(Status::NOT_MODIFIED, $response->getStatusCode());
-        $this->assertEmpty((string) $response->getBody());
+        $this->assertEmpty((string)$response->getBody());
     }
 
     public function testModifiedResultWithWeakEtag(): void
@@ -120,33 +152,6 @@ public function testModifiedResultWithWeakEtag(): void
         $this->assertSame($response->getHeaderLine('Etag'), 'W/"IMPoQ2/Us52fJk3jpOZtEACPlVA"');
     }
 
-    public function dataNotModifiedResultWithLastModified(): array
-    {
-        $time = time();
-        $ifModifiedSince = $this->formatTime($time);
-
-        return [
-            [
-                $time - 1,
-                null,
-                $ifModifiedSince,
-                $this->formatTime($time - 1),
-            ],
-            [
-                $time - 1,
-                'test-etag',
-                $ifModifiedSince,
-                '',
-            ],
-            [
-                $time,
-                'test-etag',
-                $ifModifiedSince,
-                '',
-            ],
-        ];
-    }
-
     /**
      * @dataProvider dataNotModifiedResultWithLastModified
      */
@@ -158,7 +163,7 @@ public function testNotModifiedResultWithLastModified(
     ): void {
         $middleware = $this->createMiddlewareWithLastModified($lastModified);
         if ($etagSeed !== null) {
-            $middleware = $middleware->withEtagSeed(fn () => $etagSeed);
+            $middleware = $middleware->withEtagSeed(fn() => $etagSeed);
         }
 
         $headers = [
@@ -170,7 +175,7 @@ public function testNotModifiedResultWithLastModified(
         );
 
         $this->assertSame(Status::NOT_MODIFIED, $response->getStatusCode());
-        $this->assertEmpty((string) $response->getBody());
+        $this->assertEmpty((string)$response->getBody());
         $this->assertSame($expectedLastModified, $response->getHeaderLine('Last-Modified'));
     }
 
@@ -188,29 +193,28 @@ public function testModifiedResultWithLastModifiedAndEmptyIfNoneMatchHeader(): v
         );
 
         $this->assertSame(Status::OK, $response->getStatusCode());
-        $this->assertEmpty((string) $response->getBody());
+        $this->assertEmpty((string)$response->getBody());
         $this->assertSame($this->formatTime($time - 1), $response->getHeaderLine('Last-Modified'));
     }
 
     public function testEmptyIfNoneMatchAndIfModifiedSinceHeaders(): void
     {
         $middleware = (new HttpCache())
-            ->withEtagSeed(static fn () => 'test-etag')
-            ->withLastModified(static fn () => time() + 3600)
-        ;
+            ->withEtagSeed(static fn() => 'test-etag')
+            ->withLastModified(static fn() => time() + 3600);
 
         $response = $middleware->process($this->createServerRequest(), $this->createRequestHandler());
 
         $this->assertSame(Status::OK, $response->getStatusCode());
-        $this->assertEmpty((string) $response->getBody());
+        $this->assertEmpty((string)$response->getBody());
     }
 
     public function testImmutability(): void
     {
         $middleware = new HttpCache();
 
-        $this->assertNotSame($middleware, $middleware->withLastModified(static fn () => 3600));
-        $this->assertNotSame($middleware, $middleware->withEtagSeed(static fn () => 'test-etag'));
+        $this->assertNotSame($middleware, $middleware->withLastModified(static fn() => 3600));
+        $this->assertNotSame($middleware, $middleware->withEtagSeed(static fn() => 'test-etag'));
         $this->assertNotSame($middleware, $middleware->withWeakEtag());
         $this->assertNotSame($middleware, $middleware->withParams(['key' => 'value']));
         $this->assertNotSame($middleware, $middleware->withCacheControlHeader('public, max-age=3600'));
@@ -219,13 +223,13 @@ public function testImmutability(): void
     private function createMiddlewareWithLastModified(int $lastModified): HttpCache
     {
         $middleware = new HttpCache();
-        return $middleware->withLastModified(static fn () => $lastModified);
+        return $middleware->withLastModified(static fn() => $lastModified);
     }
 
     private function createMiddlewareWithETag(string $etag): HttpCache
     {
         $middleware = new HttpCache();
-        return $middleware->withEtagSeed(fn () => $etag);
+        return $middleware->withEtagSeed(fn() => $etag);
     }
 
     private function createRequestHandler(): RequestHandlerInterface
@@ -247,9 +251,4 @@ private function createServerRequest(string $method = Method::GET, array $header
 
         return $request;
     }
-
-    private function formatTime(int $time): string
-    {
-        return gmdate('D, d M Y H:i:s', $time) . ' GMT';
-    }
 }
diff --git a/tests/IpFilterTest.php b/tests/IpFilterTest.php
index 988a8c8..b563273 100644
--- a/tests/IpFilterTest.php
+++ b/tests/IpFilterTest.php
@@ -19,14 +19,7 @@ final class IpFilterTest extends TestCase
     private MockObject|ResponseFactoryInterface $responseFactoryMock;
     private MockObject|RequestHandlerInterface $requestHandlerMock;
 
-    protected function setUp(): void
-    {
-        parent::setUp();
-        $this->responseFactoryMock = $this->createMock(ResponseFactoryInterface::class);
-        $this->requestHandlerMock = $this->createMock(RequestHandlerInterface::class);
-    }
-
-    public function ipNotAllowedDataProvider(): array
+    public static function ipNotAllowedDataProvider(): array
     {
         return [
             'basic' => [['REMOTE_ADDR' => '8.8.8.8']],
@@ -38,6 +31,14 @@ public function ipNotAllowedDataProvider(): array
         ];
     }
 
+    public static function dataProcessCallsRequestHandlerWhenRemoteAddressIsAllowed(): array
+    {
+        return [
+            'basic' => ['1.1.1.1'],
+            'with ranges' => ['10.0.0.1', ['10.0.0.1', '!10.0.0.0/8', '!babe::/8', 'any']],
+        ];
+    }
+
     /**
      * @dataProvider ipNotAllowedDataProvider
      *
@@ -69,15 +70,7 @@ public function testProcessReturnsAccessDeniedResponseWhenIpIsNotAllowed(
         $response = $ipFilter->process($requestMock, $this->requestHandlerMock);
 
         $this->assertSame(Status::FORBIDDEN, $response->getStatusCode());
-        $this->assertSame(Status::TEXTS[Status::FORBIDDEN], (string) $response->getBody());
-    }
-
-    public function dataProcessCallsRequestHandlerWhenRemoteAddressIsAllowed(): array
-    {
-        return [
-            'basic' => ['1.1.1.1'],
-            'with ranges' => ['10.0.0.1', ['10.0.0.1', '!10.0.0.0/8', '!babe::/8', 'any']],
-        ];
+        $this->assertSame(Status::TEXTS[Status::FORBIDDEN], (string)$response->getBody());
     }
 
     /**
@@ -129,4 +122,11 @@ public function testProcessCallsRequestHandlerWithSetClientIpAttribute(): void
 
         $this->assertSame(Status::OK, $response->getStatusCode());
     }
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->responseFactoryMock = $this->createMock(ResponseFactoryInterface::class);
+        $this->requestHandlerMock = $this->createMock(RequestHandlerInterface::class);
+    }
 }
diff --git a/tests/LocaleTest.php b/tests/LocaleTest.php
index 4a5e0e8..727d836 100644
--- a/tests/LocaleTest.php
+++ b/tests/LocaleTest.php
@@ -40,6 +40,83 @@ final class LocaleTest extends TestCase
     private ?ServerRequestInterface $lastRequest;
     private LoggerInterface $logger;
 
+    public static function dataInvalidLocalesFormat(): array
+    {
+        return [
+            [['en', 'ru', 'uz']],
+            [['en' => 'en-US', 'ru' => ['ru-RU'], 'uz' => 'uz-UZ']],
+            [['en' => 'en-US', 'ru-RU', 'uz' => 'uz-UZ']],
+            [['en' => 'en-US', ['ru-RU'], 'uz' => 'uz-UZ']],
+        ];
+    }
+
+    public static function dataWithDefaultLocaleAndNotSupportedLocale(): array
+    {
+        return [
+            'full name is specified instead of short one' => ['uz-UZ'],
+            'irrelevant / non-existing locale' => ['test'],
+        ];
+    }
+
+    public static function dataDefaultLocaleAndMultipleSupportedLocales(): array
+    {
+        return [
+            'basic' => ['', '/home'],
+            'with URI prefix' => ['/api', '/api/home'],
+            'with URI prefix, trailing slash' => ['/api/', '/api/home'],
+        ];
+    }
+
+    public static function dataSaveLocaleWithCustomArguments(): array
+    {
+        return [
+            'cookie name: default, secureCookie: default' => [null, null],
+            'cookie name: default, secureCookie: false' => [null, false],
+            'cookie name: default, secureCookie: true' => [null, true],
+            'cookie name: custom, secureCookie: default' => ['_cookie_language', null],
+        ];
+    }
+
+    public static function dataLocaleFromCookies(): array
+    {
+        return [
+            'default parameter name' => [null],
+            'custom parameter name' => ['_cookies_language'],
+        ];
+    }
+
+    public static function dataLocaleFromQueryParam(): array
+    {
+        return [
+            'basic, default parameter name' => ['uz', 'uz', null],
+            'basic, custom parameter name' => ['uz', 'uz', '_query_language'],
+            'extended, default parameter name' => ['uz-UZ', 'uz', null],
+        ];
+    }
+
+    public static function dataDetectLocale(): array
+    {
+        return [
+            'dash separator' => ['uz', 'uz-UZ', '', '/uz'],
+            'underscore separator' => ['uz', 'uz_UZ', '', '/uz'],
+            'locale with more than separator' => ['zh', 'zh-Hant-TW', '', '/zh'],
+            'locale without separator' => ['uz', 'uz', '', '/uz'],
+            'with URI prefix' => ['uz', 'uz-UZ', '/api', '/api/uz'],
+            'with URI prefix, trailing slash' => ['uz', 'uz-UZ', '/api/', '/api/uz'],
+        ];
+    }
+
+    public static function dataLocale(): array
+    {
+        return [
+            'basic' => ['/uz', ['uz' => 'uz-UZ']],
+            'with dash' => ['/uz-UZ', ['uz' => 'uz-UZ']],
+            'with underscore' => ['/uz_UZ', ['uz' => 'uz_UZ']],
+            'without country' => ['/uz', ['uz' => 'uz']],
+            'with subtags' => ['/en_us', ['en_us' => 'en-US', 'en_gb' => 'en-GB']],
+        ];
+    }
+
     public function setUp(): void
     {
         $this->translatorLocale = null;
@@ -58,22 +135,13 @@ public function testImmutability(): void
         $this->assertNotSame($localeMiddleware->withCookieDuration(new DateInterval('P31D')), $localeMiddleware);
         $this->assertNotSame($localeMiddleware->withDefaultLocale('uz'), $localeMiddleware);
         $this->assertNotSame($localeMiddleware->withDetectLocale(true), $localeMiddleware);
-        $this->assertNotSame($localeMiddleware->withSupportedLocales(['ru' => 'ru-RU', 'uz' => 'uz-UZ']), $localeMiddleware);
+        $this->assertNotSame($localeMiddleware->withSupportedLocales(['ru' => 'ru-RU', 'uz' => 'uz-UZ']),
+            $localeMiddleware);
         $this->assertNotSame($localeMiddleware->withQueryParameterName('lang'), $localeMiddleware);
         $this->assertNotSame($localeMiddleware->withCookieName('lang'), $localeMiddleware);
         $this->assertNotSame($localeMiddleware->withIgnoredRequestUrlPatterns(['/auth**']), $localeMiddleware);
     }
 
-    public function dataInvalidLocalesFormat(): array
-    {
-        return [
-            [['en', 'ru', 'uz']],
-            [['en' => 'en-US', 'ru' => ['ru-RU'], 'uz' => 'uz-UZ']],
-            [['en' => 'en-US', 'ru-RU', 'uz' => 'uz-UZ']],
-            [['en' => 'en-US', ['ru-RU'], 'uz' => 'uz-UZ']],
-        ];
-    }
-
     /**
      * @dataProvider dataInvalidLocalesFormat
      */
@@ -146,14 +214,6 @@ public function testWithDefaultLocale(): void
         $this->assertSame(Status::OK, $response->getStatusCode());
     }
 
-    public function dataWithDefaultLocaleAndNotSupportedLocale(): array
-    {
-        return [
-            'full name is specified instead of short one' => ['uz-UZ'],
-            'irrelevant / non-existing locale' => ['test'],
-        ];
-    }
-
     /**
      * @dataProvider dataWithDefaultLocaleAndNotSupportedLocale
      */
@@ -166,15 +226,6 @@ public function testWithDefaultLocaleAndNotSupportedLocale(string $defaultLocale
         $middleware->withDefaultLocale($defaultLocale);
     }
 
-    public function dataDefaultLocaleAndMultipleSupportedLocales(): array
-    {
-        return [
-            'basic' => ['', '/home'],
-            'with URI prefix' => ['/api', '/api/home'],
-            'with URI prefix, trailing slash' => ['/api/', '/api/home'],
-        ];
-    }
-
     /**
      * @dataProvider dataDefaultLocaleAndMultipleSupportedLocales
      */
@@ -217,17 +268,6 @@ public function testWithoutSupportedLocales(): void
         $this->assertSame($uri, $this->getRequestPath());
     }
 
-    public function dataLocale(): array
-    {
-        return [
-            'basic' => ['/uz', ['uz' => 'uz-UZ']],
-            'with dash' => ['/uz-UZ', ['uz' => 'uz-UZ']],
-            'with underscore' => ['/uz_UZ', ['uz' => 'uz_UZ']],
-            'without country' => ['/uz', ['uz' => 'uz']],
-            'with subtags' => ['/en_us', ['en_us' => 'en-US', 'en_gb' => 'en-GB']],
-        ];
-    }
-
     /**
      * @dataProvider dataLocale
      */
@@ -265,16 +305,6 @@ public function testSaveLocaleWithDefaultArguments(): void
         $this->assertSame($expectedLoggerMessages, $this->logger->getMessages());
     }
 
-    public function dataSaveLocaleWithCustomArguments(): array
-    {
-        return [
-            'cookie name: default, secureCookie: default' => [null, null],
-            'cookie name: default, secureCookie: false' => [null, false],
-            'cookie name: default, secureCookie: true' => [null, true],
-            'cookie name: custom, secureCookie: default' => ['_cookie_language', null],
-        ];
-    }
-
     /**
      * @dataProvider dataSaveLocaleWithCustomArguments
      */
@@ -342,14 +372,6 @@ public function testSaveLocaleWithCustomArguments(?string $cookieName, ?bool $se
         $this->assertSame($expectedLoggerMessages, $this->logger->getMessages());
     }
 
-    public function dataLocaleFromCookies(): array
-    {
-        return [
-            'default parameter name' => [null],
-            'custom parameter name' => ['_cookies_language'],
-        ];
-    }
-
     /**
      * @dataProvider dataLocaleFromCookies
      */
@@ -380,15 +402,6 @@ public function testLocaleFromCookies(?string $parameterName): void
         $this->assertSame($expectedLoggerMessages, $this->logger->getMessages());
     }
 
-    public function dataLocaleFromQueryParam(): array
-    {
-        return [
-            'basic, default parameter name' => ['uz', 'uz', null],
-            'basic, custom parameter name' => ['uz', 'uz', '_query_language'],
-            'extended, default parameter name' => ['uz-UZ', 'uz', null],
-        ];
-    }
-
     /**
      * @dataProvider dataLocaleFromQueryParam
      */
@@ -426,7 +439,7 @@ public function testLocaleFromQueryParamPriorityOverCookie(): void
         $request = $this->createRequest(
             '/',
             queryParams: ['_language' => 'uz'],
-            cookieParams: ['_language' => 'ru']
+            cookieParams: ['_language' => 'ru'],
         );
         $response = $this->process($middleware, $request);
 
@@ -434,18 +447,6 @@ public function testLocaleFromQueryParamPriorityOverCookie(): void
         $this->assertSame(Status::FOUND, $response->getStatusCode());
     }
 
-    public function dataDetectLocale(): array
-    {
-        return [
-            'dash separator' => ['uz', 'uz-UZ', '', '/uz'],
-            'underscore separator' => ['uz', 'uz_UZ', '', '/uz'],
-            'locale with more than separator' => ['zh', 'zh-Hant-TW', '', '/zh'],
-            'locale without separator' => ['uz', 'uz', '', '/uz'],
-            'with URI prefix' => ['uz', 'uz-UZ', '/api', '/api/uz'],
-            'with URI prefix, trailing slash' => ['uz', 'uz-UZ', '/api/', '/api/uz'],
-        ];
-    }
-
     /**
      * @dataProvider dataDetectLocale
      */
@@ -538,7 +539,7 @@ function (SetLocaleEvent $event) use ($stack) {
                     $stack->add('event');
                     $stack->add($event->getLocale());
                     return $event;
-                }
+                },
             );
 
         $urlGenerator = $this->createMock(UrlGeneratorInterface::class);
@@ -548,7 +549,7 @@ function (SetLocaleEvent $event) use ($stack) {
                 function ($name, $value) use ($stack) {
                     $stack->add('urlGenerator');
                     $stack->add($value);
-                }
+                },
             );
 
         $middleware = new Locale(
@@ -573,12 +574,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface
 
         $middleware->process(
             $this->createRequest('/ru/test'),
-            $handler
+            $handler,
         );
 
         $this->assertSame(
             ['event', 'ru-RU', 'urlGenerator', 'ru', 'handler'],
-            $stack->data
+            $stack->data,
         );
     }
 
@@ -657,7 +658,7 @@ private function createRequest(
         string $method = Method::GET,
         array $queryParams = [],
         array $headers = [],
-        $cookieParams = []
+        $cookieParams = [],
     ): ServerRequestInterface {
         return new ServerRequest(
             cookieParams: $cookieParams,
@@ -679,7 +680,7 @@ private function createSession(): SessionInterface
 
         $session
             ->method('get')
-            ->willReturnCallback(fn ($name) => $this->session[$name]);
+            ->willReturnCallback(fn($name) => $this->session[$name]);
 
         return $session;
     }
diff --git a/tests/SubfolderTest.php b/tests/SubfolderTest.php
index 973119d..6d0f990 100644
--- a/tests/SubfolderTest.php
+++ b/tests/SubfolderTest.php
@@ -22,6 +22,87 @@ final class SubfolderTest extends TestCase
     private Aliases $aliases;
     private ?ServerRequestInterface $lastRequest;
 
+    public static function autoPrefixDataProvider(): array
+    {
+        return [
+            'auto prefix' => [
+                '/public/',
+                '/public/index.php',
+                '/public',
+                '/public',
+                '/',
+            ],
+            'auto prefix logn' => [
+                '/root/php/dev-server/project-42/index_html/public/web/',
+                '/root/php/dev-server/project-42/index_html/public/web/index.php',
+                '/root/php/dev-server/project-42/index_html/public/web',
+                '/root/php/dev-server/project-42/index_html/public/web',
+                '/',
+            ],
+            'auto prefix and uri without trailing slash' => [
+                '/public',
+                '/public/index.php',
+                '/public',
+                '/public',
+                '/',
+            ],
+            'auto prefix full url' => [
+                '/public/index.php?test',
+                '/public/index.php',
+                '/public',
+                '/public',
+                '/index.php',
+            ],
+            'failed auto prefix' => [
+                '/web/index.php',
+                '/public/index.php',
+                '/default/web',
+                '',
+                '/web/index.php',
+            ],
+            'auto prefix doesn\'t match completely' => [
+                '/public/web/',
+                '/pub/index.php',
+                '/default/web',
+                '',
+                '/public/web/',
+            ],
+        ];
+    }
+
+    public static function scriptParamsProvider(): iterable
+    {
+        yield [
+            [
+                'PHP_SELF' => '/public/index.php',
+                'SCRIPT_FILENAME' => '/public/index.php',
+            ],
+        ];
+        yield [
+            [
+                'PHP_SELF' => '/public/index.php/test',
+                'SCRIPT_FILENAME' => '/public/index.php',
+            ],
+        ];
+        yield [
+            [
+                'ORIG_SCRIPT_NAME' => '/public/index.php',
+                'SCRIPT_FILENAME' => '/public/index.php',
+            ],
+        ];
+        yield [
+            [
+                'ORIG_SCRIPT_NAME' => '/public/index.php',
+            ],
+        ];
+        yield [
+            [
+                'DOCUMENT_ROOT' => '/www',
+                'SCRIPT_FILENAME' => '/www/public/index.php',
+            ],
+        ];
+    }
+
     public function setUp(): void
     {
         $this->urlGeneratorUriPrefix = '';
@@ -116,7 +197,7 @@ public function testAutoPrefix(
         string $script,
         string $expectedBaseUrl,
         string $expectedPrefix,
-        $expectedPath
+        $expectedPath,
     ): void {
         $request = $this->createRequest($uri, $script);
         $mw = $this->createMiddleware(alias: '@baseUrl');
@@ -143,87 +224,6 @@ public function testAutoPrefixWithScriptParams(array $scriptParams): void
         $this->assertSame('/', $this->getRequestPath());
     }
 
-    public function autoPrefixDataProvider(): array
-    {
-        return [
-            'auto prefix' => [
-                '/public/',
-                '/public/index.php',
-                '/public',
-                '/public',
-                '/',
-            ],
-            'auto prefix logn' => [
-                '/root/php/dev-server/project-42/index_html/public/web/',
-                '/root/php/dev-server/project-42/index_html/public/web/index.php',
-                '/root/php/dev-server/project-42/index_html/public/web',
-                '/root/php/dev-server/project-42/index_html/public/web',
-                '/',
-            ],
-            'auto prefix and uri without trailing slash' => [
-                '/public',
-                '/public/index.php',
-                '/public',
-                '/public',
-                '/',
-            ],
-            'auto prefix full url' => [
-                '/public/index.php?test',
-                '/public/index.php',
-                '/public',
-                '/public',
-                '/index.php',
-            ],
-            'failed auto prefix' => [
-                '/web/index.php',
-                '/public/index.php',
-                '/default/web',
-                '',
-                '/web/index.php',
-            ],
-            'auto prefix doesn\'t match completely' => [
-                '/public/web/',
-                '/pub/index.php',
-                '/default/web',
-                '',
-                '/public/web/',
-            ],
-        ];
-    }
-
-    public function scriptParamsProvider(): iterable
-    {
-        yield [
-            [
-                'PHP_SELF' => '/public/index.php',
-                'SCRIPT_FILENAME' => '/public/index.php',
-            ],
-        ];
-        yield [
-            [
-                'PHP_SELF' => '/public/index.php/test',
-                'SCRIPT_FILENAME' => '/public/index.php',
-            ],
-        ];
-        yield [
-            [
-                'ORIG_SCRIPT_NAME' => '/public/index.php',
-                'SCRIPT_FILENAME' => '/public/index.php',
-            ],
-        ];
-        yield [
-            [
-                'ORIG_SCRIPT_NAME' => '/public/index.php',
-            ],
-        ];
-        yield [
-            [
-                'DOCUMENT_ROOT' => '/www',
-                'SCRIPT_FILENAME' => '/www/public/index.php',
-            ],
-        ];
-    }
-
     private function process(Subfolder $middleware, ServerRequestInterface $request): ResponseInterface
     {
         $handler = new class () implements RequestHandlerInterface {
diff --git a/tests/Support/StaticClock.php b/tests/Support/StaticClock.php
index 0a6fe12..5c00b6c 100644
--- a/tests/Support/StaticClock.php
+++ b/tests/Support/StaticClock.php
@@ -10,7 +10,7 @@
 final class StaticClock implements ClockInterface
 {
     public function __construct(
-        private DateTimeImmutable $now,
+        private readonly DateTimeImmutable $now,
     ) {
     }
 
diff --git a/tests/TagRequestTest.php b/tests/TagRequestTest.php
index fd19f12..1bcf694 100644
--- a/tests/TagRequestTest.php
+++ b/tests/TagRequestTest.php
@@ -20,7 +20,7 @@ public function testProcess(): void
             ->method('withAttribute')
             ->with(
                 $this->equalTo('requestTag'),
-                $this->isType('string')
+                $this->isType('string'),
             )
             ->willReturnSelf();
 


Reviews:
user: login: samdark
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T11:48:28Z
user: login: DAGpro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:59:19Z
user: login: samdark
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T21:44:48Z
user: login: vjik
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T05:54:20Z

Review comments:
diff_hunk: @@ -1,34 +1,34 @@
 on:
-  pull_request:
-    paths-ignore:
-      - 'docs/**'
-      - 'README.md'
-      - 'CHANGELOG.md'
-      - '.gitignore'
-      - '.gitattributes'
-      - 'infection.json.dist'
-      - 'phpunit.xml.dist'
-      - 'psalm.xml'
-  push:
-    branches: ['master']
-    paths-ignore:
-      - 'docs/**'
-      - 'README.md'
-      - 'CHANGELOG.md'
-      - '.gitignore'
-      - '.gitattributes'
-      - 'infection.json.dist'
-      - 'phpunit.xml.dist'
-      - 'psalm.xml'
+    pull_request:
path: .github/workflows/bc.yml
user: login: samdark
body: Usually yaml uses 2 spaces. I'd keep it like that.
created_at: 2025-04-14T11:46:47Z
author_association: MEMBER
diff_hunk: @@ -2,7 +2,8 @@
 
 ## 1.0.5 under development
 
-- no changes in this release.
+- Chg: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
+- Bug: Explicitly mark nullable parameters (@dagpro)
path: CHANGELOG.md
user: login: samdark
body: ```suggestion
- Chg #129: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
- Bug #129: Explicitly mark nullable parameters (@dagpro)
```
created_at: 2025-04-14T11:47:35Z
author_association: MEMBER
diff_hunk: @@ -1,34 +1,34 @@
 on:
-  pull_request:
-    paths-ignore:
-      - 'docs/**'
-      - 'README.md'
-      - 'CHANGELOG.md'
-      - '.gitignore'
-      - '.gitattributes'
-      - 'infection.json.dist'
-      - 'phpunit.xml.dist'
-      - 'psalm.xml'
-  push:
-    branches: ['master']
-    paths-ignore:
-      - 'docs/**'
-      - 'README.md'
-      - 'CHANGELOG.md'
-      - '.gitignore'
-      - '.gitattributes'
-      - 'infection.json.dist'
-      - 'phpunit.xml.dist'
-      - 'psalm.xml'
+    pull_request:
path: .github/workflows/bc.yml
user: login: DAGpro
body: I use the standard ide settings, for php I used php-fig code style, but style ci always puzzles me with edits, although I adjusted the ide for style ci 😅, I'll have to look at the settings for yaml
created_at: 2025-04-14T12:59:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,7 +10,7 @@ on:
       - 'phpunit.xml.dist'
       - 'psalm.xml'
   push:
-    branches: ['master']
+    branches: [ 'master' ]
path: .github/workflows/bc.yml
user: login: samdark
body: ```suggestion
    branches: ['master']
```
created_at: 2025-04-14T21:43:50Z
author_association: MEMBER
diff_hunk: @@ -10,7 +10,7 @@ on:
       - 'psalm.xml'
 
   push:
-    branches: ['master']
+    branches: [ 'master' ]
path: .github/workflows/build.yml
user: login: samdark
body: ```suggestion
    branches: ['master']
```
created_at: 2025-04-14T21:44:00Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@ on:
       - 'psalm.xml'
 
   push:
-    branches: ['master']
+    branches: [ 'master' ]
path: .github/workflows/composer-require-checker.yml
user: login: samdark
body: ```suggestion
    branches: ['master']
```
created_at: 2025-04-14T21:44:12Z
author_association: MEMBER
diff_hunk: @@ -9,7 +9,7 @@ on:
       - 'psalm.xml'
 
   push:
-    branches: ['master']
+    branches: [ 'master' ]
path: .github/workflows/mutation.yml
user: login: samdark
body: ```suggestion
    branches: ['master']
```
created_at: 2025-04-14T21:44:24Z
author_association: MEMBER
diff_hunk: @@ -2,6 +2,8 @@
 
 ## 1.0.5 under development
 
+- Chg #129: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
+- Bug #129: Explicitly mark nullable parameters (@dagpro)
path: CHANGELOG.md
user: login: vjik
body: ```suggestion
- Chg #129: Bump PHP minimal version to 8.1 and refactor code to use new features (@dagpro)
- Chg #129: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
- Chg #129: Bump `yiisoft/router` version to `^4.0` (@dagpro)
- Chg #129: Bump `yiisoft/session` version to `^3.0` (@dagpro)
- Bug #129: Explicitly mark nullable parameters (@dagpro)
```
created_at: 2025-04-18T05:54:16Z
author_association: MEMBER

PR comments:
user: login: DAGpro
created_at: 2025-04-14T04:06:15Z
body: IDE played with formatting Yaml and Composer Files I hope this is not critical 😅
author_association: CONTRIBUTOR
user: login: DAGpro
created_at: 2025-04-14T04:09:35Z
body: Inspired by the ideas of corrections from this pr https://github.com/yiisoft/view/pull/280
author_association: CONTRIBUTOR
user: login: samdark
created_at: 2025-04-15T09:27:33Z
body: https://github.com/yiisoft/yii-middleware/actions/runs/14459280124/job/40548734999?pr=129

author_association: MEMBER
user: login: DAGpro
created_at: 2025-04-15T17:45:39Z
body: https://pecl.php.net/package/uopz/7.1.1/windows

Pecl doesn't have a version for PHP 8.4 yet.
Lower PHP requirements?
author_association: CONTRIBUTOR
user: login: vjik
created_at: 2025-04-16T05:33:12Z
body: > https://pecl.php.net/package/uopz/7.1.1/windows
> 
> Pecl doesn't have a version for PHP 8.4 yet. Lower PHP requirements?

It needs to try remove uopz extension usage. Seems, after https://github.com/yiisoft/cookies/pull/72 it can be done.
author_association: MEMBER
user: login: DAGpro
created_at: 2025-04-17T00:54:39Z
body: > > https://pecl.php.net/package/uopz/7.1.1/windows
> > У Pecl пока нет версии для PHP 8.4. Снижение требований к PHP?
> 
> Нужно попробовать удалить использование расширения uopz. Кажется, после [yiisoft/cookies#72](https://github.com/yiisoft/cookies/pull/72) это можно сделать.


    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
    {
        if ($this->cookieDuration === null) {
            return $response;
        }

        $this->logger->debug('Saving found locale to cookies.');
        $cookie = new Cookie(name: $this->cookieName, value: $locale, secure: $this->secureCookie);
        $cookie = $cookie->withMaxAge($this->cookieDuration);

        return $cookie->addToResponse($response);
    }

Add psr/clock and clock-implementation to dependencies or implement the class in the package and pass it to the cookie class?
author_association: CONTRIBUTOR
user: login: vjik
created_at: 2025-04-17T06:19:25Z
body: I did it: https://github.com/yiisoft/yii-middleware/pull/130
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-17T17:43:23Z
body: ## [Codecov](https://app.codecov.io/gh/yiisoft/yii-middleware/pull/129?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yiisoft) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`6a99980`)](https://app.codecov.io/gh/yiisoft/yii-middleware/commit/6a99980371cfc49db7196761fccbdc707068546e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yiisoft) to head [(`1167976`)](https://app.codecov.io/gh/yiisoft/yii-middleware/commit/1167976aef41aab1870e34953f532ca512f79baf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yiisoft).
> Report is 19 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              master      #129   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity       136       138    +2     
===========================================
  Files             11        11           
  Lines            360       366    +6     
===========================================
+ Hits             360       366    +6     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yiisoft/yii-middleware/pull/129?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yiisoft).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yiisoft).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: samdark
created_at: 2025-04-19T20:16:29Z
body: Thank you!
author_association: MEMBER