From 9d12c693eb22f891543159d32bd902ffb53ccc41 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sun, 15 Mar 2026 04:26:18 -0300 Subject: [PATCH] Add readonly properties, fix deprecations, improve cov and CI Mark Mapper::$db and Db::$connection as readonly. Fix 3 deprecation warnings by adding explicit typed properties to TestFetchingClassArgs for PDO FETCH_CLASS compatibility. Add 10 tests covering Db, Sql, and Mapper edge cases to reach 98.66% line coverage. Add composer coverage script. Add code-coverage CI job with Codecov upload and PHPCS to the static-analysis CI job. --- .github/workflows/ci.yml | 18 +++++++++++++ composer.json | 1 + src/Db.php | 2 +- src/Mapper.php | 2 +- tests/DbTest.php | 27 +++++++++++++++++++ tests/MapperTest.php | 39 +++++++++++++++++++++++++++ tests/SqlTest.php | 30 +++++++++++++++++++++ tests/Stubs/TestFetchingClassArgs.php | 6 +++++ 8 files changed, 123 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02b873..d99ab21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,23 @@ jobs: - uses: ramsey/composer-install@v3 - run: composer phpunit + code-coverage: + name: Code Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: pcov + - uses: ramsey/composer-install@v3 + - name: Generate coverage report + run: vendor/bin/phpunit --coverage-clover=coverage.xml + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + static-analysis: name: Static Analysis runs-on: ubuntu-latest @@ -26,4 +43,5 @@ jobs: with: php-version: '8.5' - uses: ramsey/composer-install@v3 + - run: composer phpcs - run: composer phpstan diff --git a/composer.json b/composer.json index 8620c9d..efe4406 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "phpcs": "vendor/bin/phpcs", "phpstan": "vendor/bin/phpstan analyze", "phpunit": "vendor/bin/phpunit", + "coverage": "vendor/bin/phpunit --coverage-text", "qa": [ "@phpcs", "@phpstan", diff --git a/src/Db.php b/src/Db.php index 5937c87..a21baa9 100644 --- a/src/Db.php +++ b/src/Db.php @@ -19,7 +19,7 @@ final class Db protected Sql $protoSql; - public function __construct(protected PDO $connection, Sql|null $sqlPrototype = null) + public function __construct(protected readonly PDO $connection, Sql|null $sqlPrototype = null) { $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->protoSql = $sqlPrototype ?: new Sql(); diff --git a/src/Mapper.php b/src/Mapper.php index 22d7f79..e2b2967 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -43,7 +43,7 @@ final class Mapper extends AbstractMapper implements c\Mixable, c\Typable { - protected Db $db; + protected readonly Db $db; public string $entityNamespace = '\\'; diff --git a/tests/DbTest.php b/tests/DbTest.php index 4e3133f..d68ad17 100644 --- a/tests/DbTest.php +++ b/tests/DbTest.php @@ -118,6 +118,33 @@ public function testRawSqlWithParams(): void $this->assertEquals(10, $line->testa); } + public function testExecReturnsTrueOnSuccess(): void + { + $result = $this->object->insertInto('unit', ['testa' => 40, 'testb' => 'jkl']) + ->values(['testa' => 40, 'testb' => 'jkl']) + ->exec(); + $this->assertTrue($result); + } + + public function testGetConnectionReturnsPdoInstance(): void + { + $connection = $this->object->getConnection(); + $this->assertInstanceOf(PDO::class, $connection); + } + + public function testFetchAllWithCallback(): void + { + $all = $this->object->select('*')->from('unit')->fetchAll( + static function ($row) { + $row->extra = 'callback'; + + return $row; + }, + ); + $this->assertCount(3, $all); + $this->assertEquals('callback', $all[0]->extra); + } + protected function tearDown(): void { unset($this->object); diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 0f83603..b2e313a 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -1144,6 +1144,45 @@ public function testShouldNotExecuteEntityConstructorWhenDisabled(): void ); } + public function testFetchWithStringConditionUsingColumnExpression(): void + { + $mapper = $this->mapper; + $comments = $mapper->comment(['comment.id > 0'])->fetchAll(); + $this->assertCount(2, $comments); + } + + public function testPersistNewEntityWithNoAutoIncrementId(): void + { + $conn = $this->createStub(PDO::class); + $conn->method('getAttribute') + ->willReturn('sqlite'); + $stmt = $this->createStub(PDOStatement::class); + $stmt->method('execute') + ->willReturn(true); + $conn->method('prepare') + ->willReturn($stmt); + $conn->method('lastInsertId') + ->willReturn('0'); + $conn->method('beginTransaction') + ->willReturn(true); + $conn->method('commit') + ->willReturn(true); + $mapper = new Mapper($conn); + $obj = new stdClass(); + $obj->id = null; + $obj->name = 'test'; + $mapper->foo->persist($obj); + $mapper->flush(); + $this->assertNull($obj->id); + } + + public function testFetchReturnsDbInstance(): void + { + $db = new Db($this->conn); + $mapper = new Mapper($db); + $this->assertInstanceOf(Db::class, $mapper->getDb()); + } + private function query(string $sql): PDOStatement { $stmt = $this->conn->query($sql); diff --git a/tests/SqlTest.php b/tests/SqlTest.php index 8dfed0d..f92cc8d 100644 --- a/tests/SqlTest.php +++ b/tests/SqlTest.php @@ -507,4 +507,34 @@ public function testInsertWithSelectSubquery(): void ); $this->assertEquals(array_values($data), $this->object->getParams()); } + + public function testEncloseWithStringWrapsInParentheses(): void + { + $result = Sql::enclose('SELECT 1'); + $this->assertEquals('(SELECT 1) ', $result); + } + + public function testEncloseWithEmptyStringReturnsEmpty(): void + { + $result = Sql::enclose(''); + $this->assertEquals('', $result); + } + + public function testEncloseWithSqlObjectWrapsQuery(): void + { + $sql = new Sql('SELECT 1'); + $result = Sql::enclose($sql); + $this->assertInstanceOf(Sql::class, $result); + $this->assertEquals('(SELECT 1)', (string) $result); + } + + public function testBuildOperationWithLeadingUnderscore(): void + { + $sql = (string) $this->object->select('*')->from('table') + ->where('column > 1')->_innerSelect('f1')->from('t2')->_(); + $this->assertEquals( + 'SELECT * FROM table WHERE column > 1 (INNER SELECT f1 FROM t2)', + $sql, + ); + } } diff --git a/tests/Stubs/TestFetchingClassArgs.php b/tests/Stubs/TestFetchingClassArgs.php index b8717f6..a824991 100644 --- a/tests/Stubs/TestFetchingClassArgs.php +++ b/tests/Stubs/TestFetchingClassArgs.php @@ -6,6 +6,12 @@ class TestFetchingClassArgs { + public int|null $testa = null; + + public string|null $testb = null; + + public int|null $testez = null; + public function __construct(public string|null $testd = null) { }