diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bd8026f7d..b1ff3bf36 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: operating-system: [ ubuntu-latest ] - php-versions: [ '8.0', '8.1', '8.2' ] + php-versions: [ '8.1', '8.2', '8.3' ] redis-version: [ 6 ] runs-on: ${{ matrix.operating-system }} diff --git a/composer.json b/composer.json index 8cd0c3b51..c6da2a892 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-pdo": "*", "ext-curl": "*", "ext-json": "*", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6cef9766b..59480f89d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - phpVersion: 80000 + phpVersion: 80100 level: 8 scanFiles: diff --git a/rector.php b/rector.php index aff633307..9920973ba 100644 --- a/rector.php +++ b/rector.php @@ -4,6 +4,9 @@ use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; +use Rector\CodingStyle\Rector\FuncCall\FunctionFirstClassCallableRector; +use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector; +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; use Rector\Config\RectorConfig; @@ -13,7 +16,7 @@ __DIR__ . '/src', ]) ->withSets([ - LevelSetList::UP_TO_PHP_80, + LevelSetList::UP_TO_PHP_81, SetList::TYPE_DECLARATION, ]) ->withRules([ @@ -21,4 +24,7 @@ ]) ->withSkip([ ClassPropertyAssignToConstructorPromotionRector::class, + ReadOnlyPropertyRector::class, + FunctionFirstClassCallableRector::class, + ArrayToFirstClassCallableRector::class, ]); diff --git a/src/HttpClient/ResponseHeaders.php b/src/HttpClient/ResponseHeaders.php index 5e0dcc1f5..842911a7a 100644 --- a/src/HttpClient/ResponseHeaders.php +++ b/src/HttpClient/ResponseHeaders.php @@ -82,7 +82,7 @@ public function offsetUnset($offset): void * @param string $offset * @return mixed|null */ - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this->data[strtolower($offset)] ?? null; } @@ -95,7 +95,7 @@ public function count(): int /** * @return mixed */ - public function current() + public function current(): mixed { return current($this->data); } @@ -108,7 +108,7 @@ public function next(): void /** * @return string|null */ - public function key() + public function key(): mixed { $key = key($this->data); diff --git a/src/Session/Adapters/Database/DatabaseHandler.php b/src/Session/Adapters/Database/DatabaseHandler.php index 32549aaf1..94f852678 100644 --- a/src/Session/Adapters/Database/DatabaseHandler.php +++ b/src/Session/Adapters/Database/DatabaseHandler.php @@ -85,18 +85,19 @@ public function destroy($id): bool } /** - * @inheritDoc + * Deletes the expired sessions + * @return int|false The number of deleted sessions, or false when they cannot be deleted */ - public function gc($max_lifetime) + public function gc($max_lifetime): int|false { $old = time() - $max_lifetime; - $result = $this->sessionModel->criteria('ttl', '<', $old)->deleteMany(); + $expired = $this->sessionModel->criteria('ttl', '<', $old)->count(); - if ($result === false) { - return false; + if ($expired === 0) { + return 0; } - return 0; + return $this->sessionModel->criteria('ttl', '<', $old)->deleteMany() ? $expired : false; } } diff --git a/tests/Unit/HttpClient/ResponseHeadersTest.php b/tests/Unit/HttpClient/ResponseHeadersTest.php index 300978521..6a8766862 100644 --- a/tests/Unit/HttpClient/ResponseHeadersTest.php +++ b/tests/Unit/HttpClient/ResponseHeadersTest.php @@ -4,6 +4,7 @@ use Quantum\HttpClient\ResponseHeaders; use Quantum\Tests\Unit\AppTestCase; +use ReflectionMethod; class ResponseHeadersTest extends AppTestCase { @@ -33,4 +34,31 @@ public function testResponseHeadersCanUnsetCaseInsensitively(): void $this->assertCount(0, $headers); $this->assertNull($headers['Content-Type']); } + + /** + * @dataProvider interfaceMethodProvider + */ + public function testResponseHeadersDeclaresTheReturnTypeOfItsInterfaceMethods(string $method): void + { + $this->assertTrue((new ReflectionMethod(ResponseHeaders::class, $method))->hasReturnType()); + } + + /** + * @return array + */ + public function interfaceMethodProvider(): array + { + return [ + 'offsetSet' => ['offsetSet'], + 'offsetExists' => ['offsetExists'], + 'offsetUnset' => ['offsetUnset'], + 'offsetGet' => ['offsetGet'], + 'count' => ['count'], + 'current' => ['current'], + 'next' => ['next'], + 'key' => ['key'], + 'valid' => ['valid'], + 'rewind' => ['rewind'], + ]; + } } diff --git a/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php b/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php new file mode 100644 index 000000000..398d7f99e --- /dev/null +++ b/tests/Unit/Session/Adapters/Database/DatabaseHandlerTest.php @@ -0,0 +1,99 @@ + 'sqlite', 'database' => ':memory:']); + + $this->_createSessionsTable(); + } + + public function tearDown(): void + { + ModelFactory::createDynamicModel('sessions')->deleteMany(); + + parent::tearDown(); + } + + public function testDatabaseHandlerGcDeletesTheExpiredSessionsAndReturnsTheirNumber(): void + { + $this->insertSession('expired-1', time() - 7200); + $this->insertSession('expired-2', time() - 5400); + $this->insertSession('active', time()); + + $this->assertSame(2, $this->createHandler()->gc(3600)); + + $this->assertSame([['session_id' => 'active']], IdiormDbal::query('SELECT session_id FROM sessions')); + } + + public function testDatabaseHandlerGcReturnsZeroWhenNoSessionHasExpired(): void + { + $this->insertSession('active', time()); + + $this->assertSame(0, $this->createHandler()->gc(3600)); + + $this->assertSame([['session_id' => 'active']], IdiormDbal::query('SELECT session_id FROM sessions')); + } + + public function testDatabaseHandlerGcReturnsFalseWhenTheSessionsCannotBeDeleted(): void + { + $model = Mockery::mock(DbalInterface::class); + $model->shouldReceive('criteria')->with('ttl', '<', Mockery::type('int'))->andReturnSelf(); + $model->shouldReceive('count')->andReturn(2); + $model->shouldReceive('deleteMany')->andReturn(false); + + $this->assertFalse((new DatabaseHandler($model))->gc(3600)); + } + + /** + * @dataProvider interfaceMethodProvider + */ + public function testDatabaseHandlerDeclaresTheReturnTypeOfItsInterfaceMethods(string $method): void + { + $this->assertTrue((new ReflectionMethod(DatabaseHandler::class, $method))->hasReturnType()); + } + + /** + * A handler serves one session, so every check gets a handler of its own + */ + private function createHandler(): DatabaseHandler + { + return new DatabaseHandler(ModelFactory::createDynamicModel('sessions')->getOrmInstance()); + } + + private function insertSession(string $id, int $ttl): void + { + IdiormDbal::execute('INSERT INTO sessions (session_id, data, ttl) VALUES (?, ?, ?)', [$id, 'data', $ttl]); + } + + /** + * @return array + */ + public function interfaceMethodProvider(): array + { + return [ + 'open' => ['open'], + 'close' => ['close'], + 'read' => ['read'], + 'write' => ['write'], + 'destroy' => ['destroy'], + 'gc' => ['gc'], + ]; + } +}