Răsfoiți Sursa

PHPStan Level 7 complete DAOs (#5354)

* PHPStan Level 7 complete DAOs

* Finalise PHPStan Level 7 for CategoryDAO

* PHPStan Level 7 for Context and Search

* Apply suggestions from code review

Co-authored-by: Luc SANCHEZ <4697568+ColonelMoutarde@users.noreply.github.com>
Alexandre Alapetite 2 ani în urmă
părinte
comite
bd9fa803f1

+ 2 - 4
app/Controllers/feedController.php

@@ -823,8 +823,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feed = $feedDAO->searchById($id);
-
-		if (!$feed) {
+		if ($feed === null) {
 			Minz_Request::bad(_t('feedback.sub.feed.not_found'), array());
 			return;
 		}
@@ -854,8 +853,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 		$feed = $feedDAO->searchById($feed_id);
-
-		if (!$feed) {
+		if ($feed === null) {
 			Minz_Request::bad(_t('feedback.sub.feed.not_found'), array());
 			return;
 		}

+ 4 - 3
app/Controllers/indexController.php

@@ -194,7 +194,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		$get = FreshRSS_Context::currentGet(true);
 		if (is_array($get)) {
 			$type = $get[0];
-			$id = $get[1];
+			$id = (int)$get[1];
 		} else {
 			$type = $get;
 			$id = 0;
@@ -219,7 +219,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 			case 'f':
 				// We most likely already have the feed object in cache
 				$feed = FreshRSS_CategoryDAO::findFeed($categories, $id);
-				if ($feed == null) {
+				if ($feed === null) {
 					$feedDAO = FreshRSS_Factory::createFeedDao();
 					$feed = $feedDAO->searchById($id);
 					if ($feed == null) {
@@ -290,8 +290,9 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 	 */
 	public function tosAction(): void {
 		$terms_of_service = file_get_contents(TOS_FILENAME);
-		if (!$terms_of_service) {
+		if ($terms_of_service === false) {
 			Minz_Error::error(404);
+			return;
 		}
 
 		$this->view->terms_of_service = $terms_of_service;

+ 4 - 1
app/Controllers/statsController.php

@@ -155,7 +155,10 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
 
 		foreach ($feeds as $feed) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
-			$feed['favicon'] = $feedDAO->searchById($feed['id'])->favicon();
+			$feedObject = $feedDAO->searchById($feed['id']);
+			if ($feedObject !== null) {
+				$feed['favicon'] = $feedObject->favicon();
+			}
 
 			$feedDate->setTimestamp($feed['last_date']);
 			if ($feedDate >= $lastWeek) {

+ 59 - 73
app/Models/CategoryDAO.php

@@ -29,10 +29,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 			} elseif ('attributes' === $name) {	//v1.15.0
 				$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
 
-				$stm = $this->pdo->query('SELECT * FROM `_feed`');
-				$feeds = $stm->fetchAll(PDO::FETCH_ASSOC);
+				/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
+				 * 	'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}> $feeds */
+				$feeds = $this->fetchAssoc('SELECT * FROM `_feed`') ?? [];
 
 				$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
+				if ($stm === false) {
+					Minz_Log::error('SQL error ' . __METHOD__ . json_encode($this->pdo->errorInfo()));
+					return false;
+				}
 				foreach ($feeds as $feed) {
 					if (empty($feed['keep_history']) || empty($feed['id'])) {
 						continue;
@@ -54,9 +59,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 					} else {
 						continue;
 					}
-					$stm->bindValue(':id', $feed['id'], PDO::PARAM_INT);
-					$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES));
-					$stm->execute();
+					if (!($stm->bindValue(':id', $feed['id'], PDO::PARAM_INT) &&
+						$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES)) &&
+						$stm->execute())) {
+						Minz_Log::error('SQL error ' . __METHOD__ . json_encode($stm->errorInfo()));
+					}
 				}
 
 				if ($this->pdo->dbType() !== 'sqlite') {	//SQLite does not support DROP COLUMN
@@ -91,7 +98,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 	}
 
 	/**
-	 * @param array<string,mixed> $valuesTmp
+	 * @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp
 	 * @return int|false
 	 */
 	public function addCategory(array $valuesTmp) {
@@ -116,7 +123,8 @@ SQL;
 		);
 
 		if ($stm !== false && $stm->execute($values) && $stm->rowCount() > 0) {
-			return $this->pdo->lastInsertId('`_category_id_seq`');
+			$catId = $this->pdo->lastInsertId('`_category_id_seq`');
+			return $catId === false ? false : (int)$catId;
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
@@ -143,7 +151,7 @@ SQL;
 	}
 
 	/**
-	 * @param array{'name':string,'kind':int,'attributes':array<string,mixed>} $valuesTmp
+	 * @param array{'name':string,'kind':int,'attributes'?:string|array<string,mixed>} $valuesTmp
 	 * @return int|false
 	 */
 	public function updateCategory(int $id, array $valuesTmp) {
@@ -204,8 +212,7 @@ SQL;
 		}
 		$sql = 'DELETE FROM `_category` WHERE id=:id';
 		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id', $id, PDO::PARAM_INT);
-		if ($stm !== false && $stm->execute()) {
+		if ($stm !== false && $stm->bindParam(':id', $id, PDO::PARAM_INT) && $stm->execute()) {
 			return $stm->rowCount();
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
@@ -234,36 +241,23 @@ SQL;
 
 	public function searchById(int $id): ?FreshRSS_Category {
 		$sql = 'SELECT * FROM `_category` WHERE id=:id';
-		$stm = $this->pdo->prepare($sql);
-		if ($stm !== false &&
-			$stm->bindParam(':id', $id, PDO::PARAM_INT) &&
-			$stm->execute()) {
-			$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-			$cat = self::daoToCategory($res);
-			if (isset($cat[0])) {
-				return $cat[0];
-			}
-		}
-		return null;
+		$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
+		/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
+		$cat = self::daoToCategory($res);
+		return $cat[0] ?? null;
 	}
 
-	/** @return FreshRSS_Category|null|false */
-	public function searchByName(string $name) {
+	public function searchByName(string $name): ?FreshRSS_Category {
 		$sql = 'SELECT * FROM `_category` WHERE name=:name';
-		$res = $this->fetchAssoc($sql, ['name' => $name]);
-		if ($res == null) {
-			return false;
-		}
+		$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
+		/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
 		$cat = self::daoToCategory($res);
 		return $cat[0] ?? null;
 	}
 
-	/** @return array<FreshRSS_Category>|false */
-	public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false) {
+	/** @return array<FreshRSS_Category> */
+	public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
 		$categories = $this->listCategories($prePopulateFeeds, $details);
-		if ($categories === false) {
-			return false;
-		}
 
 		uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
 			$aPosition = $a->attributes('position');
@@ -281,11 +275,11 @@ SQL;
 		return $categories;
 	}
 
-	/** @return array<FreshRSS_Category>|false */
-	public function listCategories(bool $prePopulateFeeds = true, bool $details = false) {
+	/** @return array<FreshRSS_Category> */
+	public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
 		if ($prePopulateFeeds) {
 			$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
-				. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
+				. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.kind, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
 				. 'FROM `_category` c '
 				. 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
 				. 'WHERE f.priority >= :priority_normal '
@@ -294,18 +288,22 @@ SQL;
 			$stm = $this->pdo->prepare($sql);
 			$values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ];
 			if ($stm !== false && $stm->execute($values)) {
-				return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
+				$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
+				/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
+				 * 	'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
+				return self::daoToCategoryPrepopulated($res);
 			} else {
 				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				if ($this->autoUpdateDb($info)) {
 					return $this->listCategories($prePopulateFeeds, $details);
 				}
 				Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
-				return false;
+				return [];
 			}
 		} else {
 			$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
-			return $res == null ? false : self::daoToCategory($res);
+			/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
+			return $res == null ? [] : self::daoToCategory($res);
 		}
 	}
 
@@ -331,12 +329,9 @@ SQL;
 
 	public function getDefault(): ?FreshRSS_Category {
 		$sql = 'SELECT * FROM `_category` WHERE id=:id';
-		$stm = $this->pdo->prepare($sql);
-		$stm->bindValue(':id', self::DEFAULTCATEGORYID, PDO::PARAM_INT);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		$cat = self::daoToCategory($res);
-
+		$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]);
+		/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
+		$cat = self::daoToCategory($res ?? []);
 		if (isset($cat[0])) {
 			return $cat[0];
 		} else {
@@ -369,7 +364,8 @@ SQL;
 			);
 
 			if ($stm !== false && $stm->execute($values)) {
-				return $this->pdo->lastInsertId('`_category_id_seq`');
+				$catId = $this->pdo->lastInsertId('`_category_id_seq`');
+				return $catId === false ? false : (int)$catId;
 			} else {
 				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@@ -381,27 +377,20 @@ SQL;
 
 	public function count(): int {
 		$sql = 'SELECT COUNT(*) AS count FROM `_category`';
-		$stm = $this->pdo->query($sql);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		return $res[0]['count'];
+		$res = $this->fetchColumn($sql, 0);
+		return isset($res[0]) ? (int)$res[0] : -1;
 	}
 
 	public function countFeed(int $id): int {
 		$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
-		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id', $id, PDO::PARAM_INT);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		return $res[0]['count'];
+		$res = $this->fetchColumn($sql, 0, [':id' => $id]);
+		return isset($res[0]) ? (int)$res[0] : -1;
 	}
 
 	public function countNotRead(int $id): int {
 		$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
-		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id', $id, PDO::PARAM_INT);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		return $res[0]['count'];
+		$res = $this->fetchColumn($sql, 0, [':id' => $id]);
+		return isset($res[0]) ? (int)$res[0] : -1;
 	}
 
 	/** @param array<FreshRSS_Category> $categories */
@@ -432,14 +421,15 @@ SQL;
 	}
 
 	/**
-	 * @param array<string,array<string,string|int>> $listDAO
+	 * @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
+	 * 	'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
+	 * 	'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
 	 * @return array<int,FreshRSS_Category>
 	 */
 	private static function daoToCategoryPrepopulated(array $listDAO) {
-		$list = array();
-		$previousLine = null;
-		/** @var array<string,string|int> */
-		$feedsDao = array();
+		$list = [];
+		$previousLine = [];
+		$feedsDao = [];
 		$feedDao = FreshRSS_Factory::createFeedDAO();
 		foreach ($listDAO as $line) {
 			if (!empty($previousLine['c_id']) && $line['c_id'] !== $previousLine['c_id']) {
@@ -450,10 +440,10 @@ SQL;
 				);
 				$cat->_id($previousLine['c_id']);
 				$cat->_kind($previousLine['c_kind']);
-				$cat->_attributes('', $previousLine['c_attributes']);
+				$cat->_attributes('', $previousLine['c_attributes'] ?? '[]');
 				$list[$previousLine['c_id']] = $cat;
 
-				$feedsDao = array();	//Prepare for next category
+				$feedsDao = [];	//Prepare for next category
 			}
 
 			$previousLine = $line;
@@ -470,7 +460,7 @@ SQL;
 			$cat->_kind($previousLine['c_kind']);
 			$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
 			$cat->_error($previousLine['c_error'] ?? 0);
-			$cat->_attributes('', $previousLine['c_attributes']);
+			$cat->_attributes('', $previousLine['c_attributes'] ?? []);
 			$list[$previousLine['c_id']] = $cat;
 		}
 
@@ -478,15 +468,11 @@ SQL;
 	}
 
 	/**
-	 * @param array<array<string,string|int>>|array<string,string|int> $listDAO
+	 * @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
 	 * @return array<FreshRSS_Category>
 	 */
-	private static function daoToCategory($listDAO): array {
-		$list = array();
-
-		if (!is_array($listDAO)) {
-			$listDAO = array($listDAO);
-		}
+	private static function daoToCategory(array $listDAO): array {
+		$list = [];
 
 		foreach ($listDAO as $dao) {
 			$cat = new FreshRSS_Category(

+ 11 - 7
app/Models/Context.php

@@ -16,11 +16,11 @@ final class FreshRSS_Context {
 	 */
 	public static $system_conf;
 	/**
-	 * @var array<FreshRSS_Category>
+	 * @var array<int,FreshRSS_Category>
 	 */
 	public static $categories = array();
 	/**
-	 * @var array<string>
+	 * @var array<int,FreshRSS_Tag>
 	 */
 	public static $tags = array();
 	/**
@@ -67,6 +67,7 @@ final class FreshRSS_Context {
 	 */
 	public static $state = 0;
 	/**
+	 * @phpstan-var 'ASC'|'DESC'
 	 * @var string
 	 */
 	public static $order = 'DESC';
@@ -217,7 +218,8 @@ final class FreshRSS_Context {
 		}
 
 		self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
-		self::$order = Minz_Request::paramString('order') ?: self::$user_conf->sort_order;
+		$order = Minz_Request::paramString('order') ?: self::$user_conf->sort_order;
+		self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
 		self::$number = Minz_Request::paramInt('nb') ?: self::$user_conf->posts_per_page;
 		if (self::$number > self::$user_conf->max_posts_per_rss) {
 			self::$number = max(
@@ -381,7 +383,7 @@ final class FreshRSS_Context {
 			if ($feed === null) {
 				$feedDAO = FreshRSS_Factory::createFeedDao();
 				$feed = $feedDAO->searchById($id);
-				if (!$feed) {
+				if ($feed === null) {
 					throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
 				}
 			}
@@ -397,9 +399,10 @@ final class FreshRSS_Context {
 			if (!isset(self::$categories[$id])) {
 				$catDAO = FreshRSS_Factory::createCategoryDao();
 				$cat = $catDAO->searchById($id);
-				if (!$cat) {
+				if ($cat === null) {
 					throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
 				}
+				//self::$categories[$id] = $cat;
 			} else {
 				$cat = self::$categories[$id];
 			}
@@ -412,9 +415,10 @@ final class FreshRSS_Context {
 			if (!isset(self::$tags[$id])) {
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				$tag = $tagDAO->searchById($id);
-				if (!$tag) {
+				if ($tag === null) {
 					throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
 				}
+				//self::$tags[$id] = $tag;
 			} else {
 				$tag = self::$tags[$id];
 			}
@@ -541,6 +545,6 @@ final class FreshRSS_Context {
 
 	public static function defaultTimeZone(): string {
 		$timezone = ini_get('date.timezone');
-		return $timezone != '' ? $timezone : 'UTC';
+		return $timezone != false ? $timezone : 'UTC';
 	}
 }

+ 5 - 5
app/Models/Entry.php

@@ -350,10 +350,7 @@ HTML;
 		return $this->is_favorite;
 	}
 
-	/**
-	 * @return FreshRSS_Feed|null|false
-	 */
-	public function feed() {
+	public function feed(): ?FreshRSS_Feed {
 		if ($this->feed === null) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$this->feed = $feedDAO->searchById($this->feedId);
@@ -778,7 +775,10 @@ HTML;
 		return false;
 	}
 
-	/** @return array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>} */
+	/**
+	 * @return array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
+	 * 	'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>}
+	 */
 	public function toArray(): array {
 		return array(
 			'id' => $this->id(),

+ 52 - 34
app/Models/EntryDAO.php

@@ -120,7 +120,8 @@ SQL;
 	 */
 	private $addEntryPrepared = false;
 
-	/** @param array<string,string|int> $valuesTmp */
+	/** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'hash':string,
+	 *		'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
 	public function addEntry(array $valuesTmp, bool $useTmpTable = true): bool {
 		if ($this->addEntryPrepared == null) {
 			$sql = static::sqlIgnoreConflict(
@@ -220,7 +221,8 @@ SQL;
 	/** @var PDOStatement|null */
 	private $updateEntryPrepared = null;
 
-	/** @param array<string,string|int> $valuesTmp */
+	/** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'hash':string,
+	 *		'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
 	public function updateEntry(array $valuesTmp): bool {
 		if (!isset($valuesTmp['is_read'])) {
 			$valuesTmp['is_read'] = null;
@@ -239,7 +241,7 @@ SQL;
 				. ', is_favorite=COALESCE(:is_favorite, is_favorite)'
 				. ', tags=:tags, attributes=:attributes '
 				. 'WHERE id_feed=:id_feed AND guid=:guid';
-			$this->updateEntryPrepared = $this->pdo->prepare($sql);
+			$this->updateEntryPrepared = $this->pdo->prepare($sql) ?: null;
 		}
 		if ($this->updateEntryPrepared) {
 			$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
@@ -578,8 +580,9 @@ SQL;
 				 . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
 				 . ' WHERE id=:id';
 			$stm = $this->pdo->prepare($sql);
-			$stm->bindParam(':id', $id_feed, PDO::PARAM_INT);
-			if (!($stm && $stm->execute())) {
+			if (!($stm !== false &&
+				$stm->bindParam(':id', $id_feed, PDO::PARAM_INT) &&
+				$stm->execute())) {
 				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
 				$this->pdo->rollBack();
@@ -667,7 +670,7 @@ SQL;
 
 		//==Inclusions==
 		$sql .= ' AND (1=0';
-		if (!empty($options['keep_period'])) {
+		if (!empty($options['keep_period']) && is_string($options['keep_period'])) {
 			$sql .= ' OR `lastSeen` < :max_last_seen';
 			$now = new DateTime('now');
 			$now->sub(new DateInterval($options['keep_period']));
@@ -696,12 +699,15 @@ SQL;
 		}
 	}
 
-	/** @return Traversable<array<string,string|int>> */
+	/** @return Traversable<array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
+	 *		'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>}> */
 	public function selectAll(): Traversable {
-		$sql = 'SELECT id, guid, title, author, '
-			. (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
-			. ', link, date, `lastSeen`, ' . static::sqlHexEncode('hash') . ' AS hash, is_read, is_favorite, id_feed, tags, attributes '
-			. 'FROM `_entry`';
+		$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
+		$hash = static::sqlHexEncode('hash');
+		$sql = <<<SQL
+SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
+FROM `_entry`
+SQL;
 		$stm = $this->pdo->query($sql);
 		if ($stm != false) {
 			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
@@ -998,7 +1004,10 @@ SQL;
 		return [ $values, $search ];
 	}
 
-	/** @return array{0:array<int|string>,1:string} */
+	/**
+	 * @param 'ASC'|'DESC' $order
+	 * @return array{0:array<int|string>,1:string}
+	 */
 	protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null,
 			int $state = FreshRSS_Entry::STATE_ALL,
 			string $order = 'DESC', string $firstId = '', int $date_min = 0) {
@@ -1048,6 +1057,7 @@ SQL;
 	/**
 	 * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
 	 * @param int $id category/feed/tag ID
+	 * @param 'ASC'|'DESC' $order
 	 * @return array{0:array<int|string>,1:string}
 	 */
 	private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
@@ -1111,23 +1121,25 @@ SQL;
 
 	/**
 	 * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
+	 * @param 'ASC'|'DESC' $order
 	 * @param int $id category/feed/tag ID
 	 * @return PDOStatement|false
 	 */
 	private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
 			string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
 			int $date_min = 0) {
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
-
-		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
-			. (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
-			. ', e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes '
-			. 'FROM `_entry` e0 '
-			. 'INNER JOIN ('
-			. $sql
-			. ') e2 ON e2.id=e0.id '
-			. 'ORDER BY e0.id ' . $order;
+		[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
+		if ($order !== 'DESC' && $order !== 'ASC') {
+			$order = 'DESC';
+		}
+		$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
+		$sql = <<<SQL
+SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes
+FROM `_entry` e0
+INNER JOIN ({$sql}) e2 ON e2.id=e0.id
+ORDER BY e0.id {$order}
+SQL;
 		$stm = $this->pdo->prepare($sql);
 		if ($stm !== false && $stm->execute($values)) {
 			return $stm;
@@ -1142,7 +1154,9 @@ SQL;
 	}
 
 	/**
+	 * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
 	 * @param int $id category/feed/tag ID
+	 * @param 'ASC'|'DESC' $order
 	 * @return Traversable<FreshRSS_Entry>
 	 */
 	public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
@@ -1160,6 +1174,7 @@ SQL;
 
 	/**
 	 * @param array<string> $ids
+	 * @param 'ASC'|'DESC' $order
 	 * @return Traversable<FreshRSS_Entry>
 	 */
 	public function listByIds(array $ids, string $order = 'DESC'): Traversable {
@@ -1179,7 +1194,6 @@ SQL;
 		if ($order !== 'DESC' && $order !== 'ASC') {
 			$order = 'DESC';
 		}
-
 		$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
 		$repeats = str_repeat('?,', count($ids) - 1) . '?';
 		$sql = <<<SQL
@@ -1188,9 +1202,10 @@ FROM `_entry`
 WHERE id IN ({$repeats})
 ORDER BY id {$order}
 SQL;
-
 		$stm = $this->pdo->prepare($sql);
-		$stm->execute($ids);
+		if ($stm === false || !$stm->execute($ids)) {
+			return;
+		}
 		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 			/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
 			 *		'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */
@@ -1201,6 +1216,7 @@ SQL;
 	/**
 	 * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
 	 * @param int $id category/feed/tag ID
+	 * @param 'ASC'|'DESC' $order
 	 * @return array<numeric-string>|null
 	 */
 	public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
@@ -1292,8 +1308,8 @@ SQL;
 		}
 	}
 
-	/** @return array<string,int>|false */
-	public function countUnreadRead() {
+	/** @return array<string,int> */
+	public function countUnreadRead(): array {
 		$sql = <<<'SQL'
 SELECT COUNT(e.id) AS count FROM `_entry` e
 	INNER JOIN `_feed` f ON e.id_feed=f.id
@@ -1304,8 +1320,8 @@ SELECT COUNT(e.id) AS count FROM `_entry` e
 	WHERE f.priority > 0 AND e.is_read=0
 SQL;
 		$res = $this->fetchColumn($sql, 0);
-		if ($res == null) {
-			return false;
+		if ($res === null) {
+			return ['all' => -1, 'unread' => -1, 'read' => -1];
 		}
 		rsort($res);
 		$all = (int)($res[0] ?? 0);
@@ -1329,15 +1345,17 @@ SQL;
 			$sql .= ' INNER JOIN `_feed` f ON e.id_feed=f.id';
 		}
 		$sql .= ' WHERE e.is_read=0';
+		$values = [];
 		if ($minPriority !== null) {
 			$sql .= ' AND f.priority > :priority';
+			$values[':priority'] = $minPriority;
 		}
-		$res = $this->fetchColumn($sql, 0, [':priority' => $minPriority]);
+		$res = $this->fetchColumn($sql, 0, $values);
 		return isset($res[0]) ? (int)($res[0]) : -1;
 	}
 
-	/** @return array<string,int>|false */
-	public function countUnreadReadFavorites() {
+	/** @return array{'all':int,'read':int,'unread':int} */
+	public function countUnreadReadFavorites(): array {
 		$sql = <<<'SQL'
 SELECT c FROM (
 	SELECT COUNT(e1.id) AS c, 1 AS o
@@ -1359,8 +1377,8 @@ SQL;
 			':priority_normal1' => FreshRSS_Feed::PRIORITY_NORMAL,
 			':priority_normal2' => FreshRSS_Feed::PRIORITY_NORMAL,
 		]);
-		if ($res == null) {
-			return false;
+		if ($res === null) {
+			return ['all' => -1, 'unread' => -1, 'read' => -1];
 		}
 
 		rsort($res);

+ 1 - 4
app/Models/Feed.php

@@ -125,10 +125,7 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->hubUrl;
 	}
 
-	/**
-	 * @return FreshRSS_Category|null|false
-	 */
-	public function category() {
+	public function category(): ?FreshRSS_Category {
 		if ($this->category === null) {
 			$catDAO = FreshRSS_Factory::createCategoryDao();
 			$this->category = $catDAO->searchById($this->categoryId);

+ 67 - 62
app/Models/FeedDAO.php

@@ -35,7 +35,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 	}
 
 	/**
-	 * @param array<string,mixed> $valuesTmp
+	 * @param array{'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
+	 * 	'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string|array<string|mixed>} $valuesTmp
 	 * @return int|false
 	 */
 	public function addFeed(array $valuesTmp) {
@@ -69,7 +70,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		);
 
 		if ($stm !== false && $stm->execute($values)) {
-			return (int)($this->pdo->lastInsertId('`_feed_id_seq`'));
+			$feedId = $this->pdo->lastInsertId('`_feed_id_seq`');
+			return $feedId === false ? false : (int)$feedId;
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
@@ -94,6 +96,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 				'website' => $feed->website(),
 				'description' => $feed->description(),
 				'lastUpdate' => 0,
+				'error' => false,
 				'pathEntries' => $feed->pathEntries(),
 				'httpAuth' => $feed->httpAuth(),
 				'ttl' => $feed->ttl(true),
@@ -137,10 +140,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 	}
 
 	/**
-	 * @param array<string,mixed> $valuesTmp
+	 * @param array{'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int,
+	 * 	'pathEntries'?:string,'httpAuth'?:string,'error'?:int,'ttl'?:int,'attributes'?:string|array<string,mixed>} $valuesTmp $valuesTmp
 	 * @return int|false
 	 */
 	public function updateFeed(int $id, array $valuesTmp) {
+		$originalValues = $valuesTmp;
 		if (isset($valuesTmp['name'])) {
 			$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
 		}
@@ -176,7 +181,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
-				return $this->updateFeed($id, $valuesTmp);
+				return $this->updateFeed($id, $originalValues);
 			}
 			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info) . ' for feed ' . $id);
 			return false;
@@ -227,7 +232,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 	public function changeCategory(int $idOldCat, int $idNewCat) {
 		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$newCat = $catDAO->searchById($idNewCat);
-		if (!$newCat) {
+		if ($newCat === null) {
 			$newCat = $catDAO->getDefault();
 		}
 
@@ -286,7 +291,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		}
 	}
 
-	/** @return Traversable<array<string,string|int>> */
+	/** @return Traversable<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
+	 * 	'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string}> */
 	public function selectAll(): Traversable {
 		$sql = <<<'SQL'
 SELECT id, url, kind, category, name, website, description, `lastUpdate`,
@@ -294,6 +300,9 @@ SELECT id, url, kind, category, name, website, description, `lastUpdate`,
 FROM `_feed`
 SQL;
 		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return;
+		}
 		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 			yield $row;
 		}
@@ -305,27 +314,26 @@ SQL;
 		if ($res == null) {
 			return null;
 		}
-		$feed = self::daoToFeed($res);
-		return $feed[$id] ?? null;
+		/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
+		 *	'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
+		$feeds = self::daoToFeed($res);
+		return $feeds[$id] ?? null;
 	}
 
 	public function searchByUrl(string $url): ?FreshRSS_Feed {
-		$sql = 'SELECT * FROM `_feed` WHERE url=?';
-		$stm = $this->pdo->prepare($sql);
-
-		$values = array($url);
-
-		$stm->execute($values);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		$feed = current(self::daoToFeed($res));
-		return $feed == false ? null : $feed;
+		$sql = 'SELECT * FROM `_feed` WHERE url=:url';
+		$res = $this->fetchAssoc($sql, [':url' => $url]);
+		/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
+		 *	'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
+		return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null);
 	}
 
-	/** @return array<int>|false */
-	public function listFeedsIds() {
+	/** @return array<int> */
+	public function listFeedsIds(): array {
 		$sql = 'SELECT id FROM `_feed`';
-		$stm = $this->pdo->query($sql);
-		return $stm ? $stm->fetchAll(PDO::FETCH_COLUMN, 0) : false;
+		/** @var array<int> $res */
+		$res = $this->fetchColumn($sql, 0) ?? [];
+		return $res;
 	}
 
 	/**
@@ -333,8 +341,10 @@ SQL;
 	 */
 	public function listFeeds(): array {
 		$sql = 'SELECT * FROM `_feed` ORDER BY name';
-		$stm = $this->pdo->query($sql);
-		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+		$res = $this->fetchAssoc($sql);
+		/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
+		 *	'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
+		return $res == null ? [] : self::daoToFeed($res);
 	}
 
 	/** @return array<string,string> */
@@ -345,8 +355,11 @@ SQL;
 		} else {
 			$sql .= 'WHERE id_feed=' . intval($id_feed);
 		}
-		$stm = $this->pdo->query($sql);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$res = $this->fetchAssoc($sql);
+		/** @var array<array{'id_feed':int,'newest_item_us':string}>|null $res */
+		if ($res == null) {
+			return [];
+		}
 		$newestItemUsec = [];
 		foreach ($res as $line) {
 			$newestItemUsec['f_' . $line['id_feed']] = $line['newest_item_us'];
@@ -380,18 +393,13 @@ SQL;
 		}
 	}
 
-	/** @return array<string>|false */
-	public function listTitles(int $id, int $limit = 0) {
+	/** @return array<string> */
+	public function listTitles(int $id, int $limit = 0): array {
 		$sql = 'SELECT title FROM `_entry` WHERE id_feed=:id_feed ORDER BY id DESC'
 			. ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
-
-		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
-
-		if ($stm !== false && $stm->execute()) {
-			return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
-		}
-		return false;
+		$res = $this->fetchColumn($sql, 0, [':id_feed' => $id]) ?? [];
+		/** @var array<string> $res */
+		return $res;
 	}
 
 	/**
@@ -399,15 +407,18 @@ SQL;
 	 * @return array<FreshRSS_Feed>
 	 */
 	public function listByCategory(int $cat, ?bool $muted = null): array {
-		$sql = 'SELECT * FROM `_feed` WHERE category=?';
+		$sql = 'SELECT * FROM `_feed` WHERE category=:category';
 		if ($muted) {
 			$sql .= ' AND ttl < 0';
 		}
-		$stm = $this->pdo->prepare($sql);
-
-		$stm->execute(array($cat));
+		$res = $this->fetchAssoc($sql, [':category' => $cat]);
+		if ($res == null) {
+			return [];
+		}
 
-		$feeds = self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+		/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
+		 *	'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
+		$feeds = self::daoToFeed($res);
 
 		usort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
 			return strnatcasecmp($a->name(), $b->name());
@@ -417,23 +428,15 @@ SQL;
 	}
 
 	public function countEntries(int $id): int {
-		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=?';
-		$stm = $this->pdo->prepare($sql);
-		$values = array($id);
-		$stm->execute($values);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
+		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=:id_feed';
+		$res = $this->fetchColumn($sql, 0, ['id_feed' => $id]);
+		return isset($res[0]) ? (int)($res[0]) : -1;
 	}
 
 	public function countNotRead(int $id): int {
-		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=? AND is_read=0';
-		$stm = $this->pdo->prepare($sql);
-		$values = array($id);
-		$stm->execute($values);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
+		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=:id_feed AND is_read=0';
+		$res = $this->fetchColumn($sql, 0, ['id_feed' => $id]);
+		return isset($res[0]) ? (int)($res[0]) : -1;
 	}
 
 	/**
@@ -525,9 +528,10 @@ SQL;
 	public function truncate(int $id) {
 		$sql = 'DELETE FROM `_entry` WHERE id_feed=:id';
 		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id', $id, PDO::PARAM_INT);
 		$this->pdo->beginTransaction();
-		if (!($stm && $stm->execute())) {
+		if (!($stm !== false &&
+			$stm->bindParam(':id', $id, PDO::PARAM_INT) &&
+			$stm->execute())) {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
 			$this->pdo->rollBack();
@@ -535,11 +539,11 @@ SQL;
 		}
 		$affected = $stm->rowCount();
 
-		$sql = 'UPDATE `_feed` '
-			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0, `lastUpdate`=0 WHERE id=:id';
+		$sql = 'UPDATE `_feed` SET `cache_nbEntries`=0, `cache_nbUnreads`=0, `lastUpdate`=0 WHERE id=:id';
 		$stm = $this->pdo->prepare($sql);
-		$stm->bindParam(':id', $id, PDO::PARAM_INT);
-		if (!($stm && $stm->execute())) {
+		if (!($stm !== false &&
+			$stm->bindParam(':id', $id, PDO::PARAM_INT) &&
+			$stm->execute())) {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
 			$this->pdo->rollBack();
@@ -574,7 +578,8 @@ SQL;
 	}
 
 	/**
-	 * @param array<int,array<string,string|int>>|array<string,string|int> $listDAO
+	 * @param array<int,array{'id'?:int,'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int,
+	 * 	'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
 	 * @return array<int,FreshRSS_Feed>
 	 */
 	public static function daoToFeed(array $listDAO, ?int $catID = null): array {
@@ -589,7 +594,7 @@ SQL;
 				continue;
 			}
 			if (isset($dao['id'])) {
-				$key = $dao['id'];
+				$key = (int)$dao['id'];
 			}
 			if ($catID === null) {
 				$category = $dao['category'] ?? 0;

+ 12 - 12
app/Models/Search.php

@@ -164,37 +164,37 @@ class FreshRSS_Search {
 	}
 
 	public function getMinDate(): ?int {
-		return $this->min_date;
+		return $this->min_date ?: null;
 	}
 	public function getNotMinDate(): ?int {
-		return $this->not_min_date;
+		return $this->not_min_date ?: null;
 	}
 	public function setMinDate(int $value): void {
 		$this->min_date = $value;
 	}
 
 	public function getMaxDate(): ?int {
-		return $this->max_date;
+		return $this->max_date ?: null;
 	}
 	public function getNotMaxDate(): ?int {
-		return $this->not_max_date;
+		return $this->not_max_date ?: null;
 	}
 	public function setMaxDate(int $value): void {
 		$this->max_date = $value;
 	}
 
 	public function getMinPubdate(): ?int {
-		return $this->min_pubdate;
+		return $this->min_pubdate ?: null;
 	}
 	public function getNotMinPubdate(): ?int {
-		return $this->not_min_pubdate;
+		return $this->not_min_pubdate ?: null;
 	}
 
 	public function getMaxPubdate(): ?int {
-		return $this->max_pubdate;
+		return $this->max_pubdate ?: null;
 	}
 	public function getNotMaxPubdate(): ?int {
-		return $this->not_max_pubdate;
+		return $this->not_max_pubdate ?: null;
 	}
 
 	/** @return array<string>|null */
@@ -518,7 +518,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
-				list($this->min_date, $this->max_date) = parseDateInterval($dates[0]);
+				[$this->min_date, $this->max_date] = parseDateInterval($dates[0]);
 			}
 		}
 		return $input;
@@ -529,7 +529,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
-				list($this->not_min_date, $this->not_max_date) = parseDateInterval($dates[0]);
+				[$this->not_min_date, $this->not_max_date] = parseDateInterval($dates[0]);
 			}
 		}
 		return $input;
@@ -545,7 +545,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
-				list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($dates[0]);
+				[$this->min_pubdate, $this->max_pubdate] = parseDateInterval($dates[0]);
 			}
 		}
 		return $input;
@@ -556,7 +556,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
-				list($this->not_min_pubdate, $this->not_max_pubdate) = parseDateInterval($dates[0]);
+				[$this->not_min_pubdate, $this->not_max_pubdate] = parseDateInterval($dates[0]);
 			}
 		}
 		return $input;

+ 8 - 14
app/Models/TagDAO.php

@@ -67,7 +67,8 @@ SQL;
 		);
 
 		if ($stm !== false && $stm->execute($values) && $stm->rowCount() > 0) {
-			return (int)($this->pdo->lastInsertId('`_tag_id_seq`'));
+			$tagId = $this->pdo->lastInsertId('`_tag_id_seq`');
+			return $tagId === false ? false : (int)$tagId;
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@@ -275,8 +276,7 @@ SQL;
 		return $newestItemUsec;
 	}
 
-	/** @return int|false */
-	public function count() {
+	public function count(): int {
 		$sql = 'SELECT COUNT(*) AS count FROM `_tag`';
 		$stm = $this->pdo->query($sql);
 		if ($stm !== false) {
@@ -288,25 +288,19 @@ SQL;
 			return $this->count();
 		}
 		Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
-		return false;
+		return -1;
 	}
 
-	/**
-	 * @return int|false
-	 */
-	public function countEntries(int $id) {
+	public function countEntries(int $id): int {
 		$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=:id_tag';
 		$res = $this->fetchAssoc($sql, [':id_tag' => $id]);
 		if ($res == null || !isset($res[0]['count'])) {
-			return false;
+			return -1;
 		}
 		return (int)$res[0]['count'];
 	}
 
-	/**
-	 * @return int|false
-	 */
-	public function countNotRead(?int $id = null) {
+	public function countNotRead(?int $id = null): int {
 		$sql = <<<'SQL'
 SELECT COUNT(*) AS count FROM `_entrytag` et
 INNER JOIN `_entry` e ON et.id_entry=e.id
@@ -320,7 +314,7 @@ SQL;
 
 		$res = $this->fetchAssoc($sql, $values);
 		if ($res == null || !isset($res[0]['count'])) {
-			return false;
+			return -1;
 		}
 		return (int)$res[0]['count'];
 	}

+ 3 - 3
app/Models/UserQuery.php

@@ -128,7 +128,7 @@ class FreshRSS_UserQuery {
 			throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
 		}
 		$category = $this->category_dao->searchById($id);
-		if ($category) {
+		if ($category !== null) {
 			$this->get_name = $category->name();
 		} else {
 			$this->deprecated = true;
@@ -146,7 +146,7 @@ class FreshRSS_UserQuery {
 			throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
 		}
 		$feed = $this->feed_dao->searchById($id);
-		if ($feed) {
+		if ($feed !== null) {
 			$this->get_name = $feed->name();
 		} else {
 			$this->deprecated = true;
@@ -164,7 +164,7 @@ class FreshRSS_UserQuery {
 			throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
 		}
 		$tag = $this->tag_dao->searchById($id);
-		if ($tag) {
+		if ($tag !== null) {
 			$this->get_name = $tag->name();
 		} else {
 			$this->deprecated = true;

+ 2 - 2
app/Services/ExportService.php

@@ -94,7 +94,7 @@ class FreshRSS_Export_Service {
 	 */
 	public function generateFeedEntries(int $feed_id, int $max_number_entries): ?array {
 		$feed = $this->feed_dao->searchById($feed_id);
-		if (!$feed) {
+		if ($feed === null) {
 			return null;
 		}
 
@@ -127,7 +127,7 @@ class FreshRSS_Export_Service {
 	 * @return array<string,string> Keys are filenames and values are contents.
 	 */
 	public function generateAllFeedEntries(int $max_number_entries): array {
-		$feed_ids = $this->feed_dao->listFeedsIds() ?: [];
+		$feed_ids = $this->feed_dao->listFeedsIds();
 
 		$exported_files = [];
 		foreach ($feed_ids as $feed_id) {

+ 1 - 15
cli/user-info.php

@@ -63,22 +63,8 @@ foreach ($users as $username) {
 
 	$nbEntries = $entryDAO->countUnreadRead();
 	$nbFavorites = $entryDAO->countUnreadReadFavorites();
-
-	if ($nbFavorites === false) {
-		$nbFavorites = [
-			'all' => -1,
-		];
-	}
-
 	$feedList = $feedDAO->listFeedsIds();
 
-	if ($nbEntries === false) {
-		$nbEntries = [
-			'read' => -1,
-			'unread' => -1,
-		];
-	}
-
 	$data = array(
 		'default' => $username === FreshRSS_Context::$system_conf->default_user ? '*' : '',
 		'user' => $username,
@@ -87,7 +73,7 @@ foreach ($users as $username) {
 		'last_user_activity' => FreshRSS_UserDAO::mtime($username),
 		'database_size' => $databaseDAO->size(),
 		'categories' => $catDAO->count(),
-		'feeds' => count($feedList === false ? [] : $feedList),
+		'feeds' => count($feedList),
 		'reads' => (int)$nbEntries['read'],
 		'unreads' => (int)$nbEntries['unread'],
 		'favourites' => (int)$nbFavorites['all'],

+ 0 - 5
tests/phpstan-next.txt

@@ -8,12 +8,7 @@
 ./app/Controllers/indexController.php
 ./app/Controllers/updateController.php
 ./app/Controllers/userController.php
-./app/Models/CategoryDAO.php
-./app/Models/Context.php
-./app/Models/EntryDAO.php
 ./app/Models/Feed.php
-./app/Models/FeedDAO.php
-./app/Models/Search.php
 ./app/Models/Share.php
 ./app/views/helpers/logs_pagination.phtml
 ./lib/Minz/Error.php