From b9af1c48dc7008cddf3b2d3479635283a090e6b4 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sun, 15 Mar 2026 03:52:42 -0300 Subject: [PATCH] Raise PHPStan from level 5 to level 7 Add generic type annotations for SplObjectStorage parameters and return types across Mapper internals. Add iterable value types to array parameters in Sql and Db. Fix PDOStatement null-safety in MapperTest with a query() helper, add getColumnMeta() false check, and use get_object_vars() for object iteration. --- phpstan.neon.dist | 2 +- src/Db.php | 2 + src/Mapper.php | 24 ++++++++-- src/Sql.php | 3 ++ tests/MapperTest.php | 107 +++++++++++++++++++++++-------------------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 334cef8..666d9c0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 7 paths: - src/ - tests/ diff --git a/src/Db.php b/src/Db.php index e91781c..5937c87 100644 --- a/src/Db.php +++ b/src/Db.php @@ -55,6 +55,7 @@ public function getSql(): Sql return $this->currentSql; } + /** @param array|null $extra */ public function prepare(string $queryString, mixed $object = '\stdClass', array|null $extra = null): PDOStatement { $statement = $this->connection->prepare($queryString); @@ -72,6 +73,7 @@ public function prepare(string $queryString, mixed $object = '\stdClass', array| return $statement; } + /** @param array|null $params */ public function query(string $rawSql, array|null $params = null): static { $this->currentSql->setQuery($rawSql, $params); diff --git a/src/Mapper.php b/src/Mapper.php index e64cb05..22d7f79 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -525,7 +525,7 @@ protected function transformSingleRow(object $row, string $entityName): object { $newRow = $this->getNewEntityByName($entityName); - foreach ($row as $prop => $value) { + foreach (get_object_vars($row) as $prop => $value) { $this->inferSet($newRow, $prop, $value); } @@ -557,7 +557,11 @@ protected function inferGet(object &$object, string $prop): mixed } } - /** @param array $row */ + /** + * @param array $row + * + * @return SplObjectStorage + */ protected function createEntities( array $row, PDOStatement $statement, @@ -573,6 +577,10 @@ protected function createEntities( //Reversely traverses the columns to avoid conflicting foreign key names foreach (array_reverse($row, true) as $col => $value) { $columnMeta = $statement->getColumnMeta($col); + if ($columnMeta === false) { + continue; + } + $columnName = $columnMeta['name']; $primaryName = $this->getStyle()->identifier( $entities[$entityInstance]->getName(), @@ -590,7 +598,11 @@ protected function createEntities( return $entities; } - /** @return array */ + /** + * @param SplObjectStorage $entities + * + * @return array + */ protected function buildEntitiesInstances( Collection $collection, SplObjectStorage $entities, @@ -620,6 +632,7 @@ protected function buildEntitiesInstances( return $entitiesInstances; } + /** @param SplObjectStorage $entities */ protected function postHydrate(SplObjectStorage $entities): void { $entitiesClone = clone $entities; @@ -639,6 +652,7 @@ protected function postHydrate(SplObjectStorage $entities): void } } + /** @param SplObjectStorage $entities */ protected function tryHydration(SplObjectStorage $entities, object $sub, string $field, mixed &$v): void { $tableName = $entities[$sub]->getName(); @@ -678,6 +692,7 @@ protected function getAllProperties(object $object): array return $cols; } + /** @param SplObjectStorage $hydrated */ private function parseHydrated(SplObjectStorage $hydrated): mixed { $this->tracked->addAll($hydrated); @@ -686,6 +701,7 @@ private function parseHydrated(SplObjectStorage $hydrated): mixed return $hydrated->current(); } + /** @return SplObjectStorage|false */ private function fetchHydrated(Collection $collection, PDOStatement $statement): SplObjectStorage|false { if (!$collection->hasMore()) { @@ -711,6 +727,7 @@ private function createStatement( return $statement; } + /** @return SplObjectStorage|false */ private function fetchSingle( Collection $collection, PDOStatement $statement, @@ -733,6 +750,7 @@ private function fetchSingle( return $entities; } + /** @return SplObjectStorage|false */ private function fetchMulti( Collection $collection, PDOStatement $statement, diff --git a/src/Sql.php b/src/Sql.php index b211b79..fb05737 100644 --- a/src/Sql.php +++ b/src/Sql.php @@ -30,6 +30,7 @@ class Sql /** @var array */ protected array $params = []; + /** @param array|null $params */ public function __construct(string $rawSql = '', array|null $params = null) { $this->setQuery($rawSql, $params); @@ -52,6 +53,7 @@ public function getParams(): array return $this->params; } + /** @param array|null $params */ public function setQuery(string $rawSql, array|null $params = null): static { $this->query = $rawSql; @@ -62,6 +64,7 @@ public function setQuery(string $rawSql, array|null $params = null): static return $this; } + /** @param array|null $params */ public function appendQuery(mixed $sql, array|null $params = null): static { $this->query = trim($this->query) . ' ' . $sql; diff --git a/tests/MapperTest.php b/tests/MapperTest.php index d0f29f0..0f83603 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -341,7 +341,7 @@ public function testSimplePersist(): void $entity = (object) ['id' => 4, 'name' => 'inserted', 'category_id' => null]; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query('select * from category where id=4') + $result = $this->query('select * from category where id=4') ->fetch(PDO::FETCH_OBJ); $this->assertEquals($entity, $result); } @@ -352,7 +352,7 @@ public function testSimplePersistCollection(): void $entity = (object) ['id' => 4, 'name' => 'inserted', 'category_id' => null]; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query('select * from category where id=4') + $result = $this->query('select * from category where id=4') ->fetch(PDO::FETCH_OBJ); $this->assertEquals($entity, $result); } @@ -370,10 +370,10 @@ public function testNestedPersistCollection(): void ]; $this->mapper->post->author->persist($postWithAuthor); $this->mapper->flush(); - $author = $this->conn->query( + $author = $this->query( 'select * from author order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); - $post = $this->conn->query( + $post = $this->query( 'select * from post order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('New', $author->name); @@ -394,10 +394,10 @@ public function testNestedPersistCollectionShortcut(): void $this->mapper->postAuthor = $this->mapper->post->author; $this->mapper->postAuthor->persist($postWithAuthor); $this->mapper->flush(); - $author = $this->conn->query( + $author = $this->query( 'select * from author order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); - $post = $this->conn->query( + $post = $this->query( 'select * from post order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('New', $author->name); @@ -418,10 +418,10 @@ public function testNestedPersistCollectionWithChildrenShortcut(): void $this->mapper->postAuthor = $this->mapper->post($this->mapper->author); $this->mapper->postAuthor->persist($postWithAuthor); $this->mapper->flush(); - $author = $this->conn->query( + $author = $this->query( 'select * from author order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); - $post = $this->conn->query( + $post = $this->query( 'select * from post order by id desc limit 1', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('New', $author->name); @@ -434,7 +434,7 @@ public function testSubCategory(): void $entity = (object) ['id' => 8, 'name' => 'inserted', 'category_id' => 2]; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query('select * from category where id=8') + $result = $this->query('select * from category where id=8') ->fetch(PDO::FETCH_OBJ); $result2 = $mapper->category[8]->category->fetch(); $this->assertEquals($result->id, $result2->id); @@ -448,7 +448,7 @@ public function testSubCategoryCondition(): void $entity = (object) ['id' => 8, 'name' => 'inserted', 'category_id' => 2]; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query('select * from category where id=8') + $result = $this->query('select * from category where id=8') ->fetch(PDO::FETCH_OBJ); $result2 = $mapper->category(['id' => 8])->category->fetch(); $this->assertEquals($result->id, $result2->id); @@ -462,7 +462,7 @@ public function testAutoIncrementPersist(): void $entity = (object) ['id' => null, 'name' => 'inserted', 'category_id' => null]; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query( + $result = $this->query( 'select * from category where name="inserted"', )->fetch(PDO::FETCH_OBJ); $this->assertEquals($entity, $result); @@ -487,14 +487,13 @@ public function testPassedIdentity(): void $mapper->comment->persist($comment); $mapper->flush(); - $postId = $this->conn - ->query('select id from post where title = 12345') + $postId = $this->query('select id from post where title = 12345') ->fetchColumn(0); - $comment = $this->conn - ->query('select * from comment where post_id = ' . $postId) + $comment = $this->query('select * from comment where post_id = ' . $postId) ->fetchObject(); + self::assertInstanceOf(stdClass::class, $comment); $this->assertEquals('abc', $comment->text); } @@ -505,7 +504,7 @@ public function testJoinedPersist(): void $entity->text = 'HeyHey'; $mapper->comment->persist($entity); $mapper->flush(); - $result = $this->conn->query('select text from comment where id=8') + $result = $this->query('select text from comment where id=8') ->fetchColumn(0); $this->assertEquals('HeyHey', $result); } @@ -514,10 +513,10 @@ public function testRemove(): void { $mapper = $this->mapper; $c8 = $mapper->comment[8]->fetch(); - $pre = $this->conn->query('select count(*) from comment')->fetchColumn(0); + $pre = (int) $this->query('select count(*) from comment')->fetchColumn(0); $mapper->comment->remove($c8); $mapper->flush(); - $total = $this->conn->query('select count(*) from comment')->fetchColumn(0); + $total = (int) $this->query('select count(*) from comment')->fetchColumn(0); $this->assertEquals($total, $pre - 1); } @@ -554,7 +553,7 @@ public function testPersistingEntityTyped(): void $comment->text = 'HeyHey'; $mapper->comment->persist($comment); $mapper->flush(); - $result = $this->conn->query('select text from comment where id=8') + $result = $this->query('select text from comment where id=8') ->fetchColumn(0); $this->assertEquals('HeyHey', $result); } @@ -567,7 +566,7 @@ public function testPersistingNewEntityTyped(): void $comment->text = 'HeyHey'; $mapper->comment->persist($comment); $mapper->flush(); - $result = $this->conn->query('select text from comment where id=9') + $result = $this->query('select text from comment where id=9') ->fetchColumn(0); $this->assertEquals('HeyHey', $result); } @@ -626,7 +625,7 @@ public function testPersistingaPreviouslyFetchedFilteredEntityBackIntoItsCollect $author->name = 'Author Changed'; $mapper->authorsWithPosts->persist($author); $mapper->flush(); - $result = $this->conn->query('select name from author where id=1') + $result = $this->query('select name from author where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Author Changed', $result->name); } @@ -639,7 +638,7 @@ public function testPersistingaPreviouslyFetchedFilteredEntityBackIntoaForeignCo $author->name = 'Author Changed'; $mapper->author->persist($author); $mapper->flush(); - $result = $this->conn->query('select name from author where id=1') + $result = $this->query('select name from author where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Author Changed', $result->name); } @@ -653,7 +652,7 @@ public function testPersistingaNewlyCreatedFilteredEntityIntoItsCollection(): vo $author->name = 'Author Changed'; $mapper->authorsWithPosts->persist($author); $mapper->flush(); - $result = $this->conn->query( + $result = $this->query( 'select name from author order by id desc', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('Author Changed', $result->name); @@ -668,7 +667,7 @@ public function testPersistingaNewlyCreatedFilteredEntityIntoaForeignCompatibleC $author->name = 'Author Changed'; $mapper->author->persist($author); $mapper->flush(); - $result = $this->conn->query( + $result = $this->query( 'select name from author order by id desc', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('Author Changed', $result->name); @@ -708,10 +707,10 @@ public function testFilteredCollectionsShouldPersistHydratedNonFilteredPartsAsUs $post->author_id->name = 'John'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query('select name from author where id=1') + $result = $this->query('select name from author where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('John', $result->name); } @@ -730,10 +729,10 @@ public function testMultipleFilteredCollectionsDontPersist(): void $post->author_id->name = 'A'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query('select name from author where id=1') + $result = $this->query('select name from author where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertNotEquals('A', $result->name); } @@ -753,10 +752,10 @@ public function testMultipleFilteredCollectionsDontPersistNewlyCreateObjects(): $post->author_id->name = 'A'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query( + $result = $this->query( 'select name from author order by id desc', )->fetch(PDO::FETCH_OBJ); $this->assertNotEquals('A', $result->name); @@ -777,10 +776,10 @@ public function testMultipleFilteredCollectionsFetchAtOnceDontPersist(): void $post->author_id->name = 'A'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query('select name from author where id=1') + $result = $this->query('select name from author where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertNotEquals('A', $result->name); } @@ -798,7 +797,7 @@ public function testReusingRegisteredFilteredCollectionsKeepsTheirFiltering(): v $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -817,7 +816,7 @@ public function testReusingRegisteredFilteredCollectionsKeepsTheirFilteringOnFet $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -834,7 +833,7 @@ public function testRegisteredFilteredCollectionsByColumnKeepsTheirFiltering(): $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -852,7 +851,7 @@ public function testRegisteredFilteredCollectionsByColumnKeepsTheirFilteringOnFe $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -866,7 +865,7 @@ public function testRegisteredFilteredWildcardCollectionsKeepsTheirFiltering(): $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -881,7 +880,7 @@ public function testRegisteredFilteredWildcardCollectionsKeepsTheirFilteringOnFe $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -903,7 +902,7 @@ public function testFetchingRegisteredFilteredCollectionsAlongsideNormal(): void $post->title = 'Title Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -952,10 +951,10 @@ public function testMixinsPersistsResultsOnTwoTables(): void $post->text = 'Comment Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query('select text from comment where id=7') + $result = $this->query('select text from comment where id=7') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Comment Changed', $result->text); } @@ -968,12 +967,12 @@ public function testMixinsPersistsNewlyCreatedEntitiesOnTwoTables(): void $post->author_id = (object) ['name' => 'Author X', 'id' => null]; $mapper->postComment->persist($post); $mapper->flush(); - $result = $this->conn->query( + $result = $this->query( 'select title, text from post order by id desc', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('Post X', $result->title); $this->assertEquals('', $result->text); - $result = $this->conn->query( + $result = $this->query( 'select text from comment order by id desc', )->fetch(PDO::FETCH_OBJ); $this->assertEquals('Comment X', $result->text); @@ -1003,10 +1002,10 @@ public function testMixinsAll(): void $post->text = 'Comment Changed'; $mapper->postsFromAuthorsWithComments->persist($post); $mapper->flush(); - $result = $this->conn->query('select title from post where id=5') + $result = $this->query('select title from post where id=5') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); - $result = $this->conn->query('select text from comment where id=7') + $result = $this->query('select text from comment where id=7') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Comment Changed', $result->text); } @@ -1024,7 +1023,7 @@ public function testTyped(): void $issues[0]->title = 'Title Changed'; $mapper->typedIssues->persist($issues[0]); $mapper->flush(); - $result = $this->conn->query('select title from issues where id=1') + $result = $this->query('select title from issues where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -1040,7 +1039,7 @@ public function testTypedSingle(): void $issue->title = 'Title Changed'; $mapper->typedIssues->persist($issue); $mapper->flush(); - $result = $this->conn->query('select title from issues where id=1') + $result = $this->query('select title from issues where id=1') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('Title Changed', $result->title); } @@ -1052,7 +1051,7 @@ public function testPersistNewWithArrayobject(): void $entity = (object) $arrayEntity; $mapper->category->persist($entity); $mapper->flush(); - $result = $this->conn->query('select * from category where id=10') + $result = $this->query('select * from category where id=10') ->fetch(PDO::FETCH_OBJ); $this->assertEquals('array_object_category', $result->name); } @@ -1095,7 +1094,7 @@ public function testPersistingEntityWithoutPublicPropertiesTyped(): void $mapper->post->persist($post); $mapper->flush(); - $result = $this->conn->query('select text from post where id=5') + $result = $this->query('select text from post where id=5') ->fetchColumn(0); $this->assertEquals('HeyHey', $result); } @@ -1115,7 +1114,7 @@ public function testPersistingNewEntityWithoutPublicPropertiesTyped(): void $post->setText('My new Post Text'); $mapper->post->persist($post); $mapper->flush(); - $result = $this->conn->query('select text from post where id=6') + $result = $this->query('select text from post where id=6') ->fetchColumn(0); $this->assertEquals('My new Post Text', $result); } @@ -1144,4 +1143,12 @@ public function testShouldNotExecuteEntityConstructorWhenDisabled(): void $mapper->comment->fetch(), ); } + + private function query(string $sql): PDOStatement + { + $stmt = $this->conn->query($sql); + self::assertInstanceOf(PDOStatement::class, $stmt); + + return $stmt; + } }