Prechádzať zdrojové kódy

Complete PHPStan Level 6 (#5305)

* Complete PHPStan Level 6
Fix https://github.com/FreshRSS/FreshRSS/issues/4112
And initiate PHPStan Level 7

* PHPStan Level 6 for tests
* Use phpstan/phpstan-phpunit
* Update to PHPStan version 1.10

* Fix mixed bug

* Fix mixed return bug

* Fix paginator bug

* Fix FreshRSS_UserConfiguration

* A couple more Minz_Configuration bug fixes

* A few trivial PHPStan Level 7 fixes

* A few more simple PHPStan Level 7

* More files passing PHPStan Level 7
Add interface to replace removed class from https://github.com/FreshRSS/FreshRSS/pull/5251

* A few more PHPStan Level 7 preparations

* A few last details
Alexandre Alapetite 3 rokov pred
rodič
commit
f3760f138d
100 zmenil súbory, kde vykonal 836 pridanie a 719 odobranie
  1. 2 2
      app/Controllers/categoryController.php
  2. 2 2
      app/Controllers/configureController.php
  3. 2 2
      app/Controllers/entryController.php
  4. 3 3
      app/Controllers/extensionController.php
  5. 5 4
      app/Controllers/feedController.php
  6. 4 6
      app/Controllers/importExportController.php
  7. 6 6
      app/Controllers/indexController.php
  8. 3 3
      app/Controllers/javascriptController.php
  9. 2 2
      app/Controllers/statsController.php
  10. 6 6
      app/Controllers/subscriptionController.php
  11. 7 8
      app/Controllers/tagController.php
  12. 3 3
      app/Models/Auth.php
  13. 1 1
      app/Models/BooleanSearch.php
  14. 1 1
      app/Models/Category.php
  15. 1 1
      app/Models/CategoryDAO.php
  16. 4 5
      app/Models/Context.php
  17. 1 1
      app/Models/DatabaseDAO.php
  18. 6 3
      app/Models/DatabaseDAOSQLite.php
  19. 7 4
      app/Models/Entry.php
  20. 18 22
      app/Models/EntryDAO.php
  21. 18 18
      app/Models/EntryDAOSQLite.php
  22. 6 6
      app/Models/Factory.php
  23. 57 52
      app/Models/Feed.php
  24. 4 4
      app/Models/FeedDAO.php
  25. 3 3
      app/Models/Search.php
  26. 2 2
      app/Models/Share.php
  27. 5 5
      app/Models/StatsDAO.php
  28. 4 1
      app/Models/StatsDAOPGSQL.php
  29. 3 0
      app/Models/StatsDAOSQLite.php
  30. 1 1
      app/Models/Tag.php
  31. 2 2
      app/Models/TagDAO.php
  32. 1 1
      app/Models/TagDAOSQLite.php
  33. 1 1
      app/Models/UserQuery.php
  34. 2 2
      app/Models/View.php
  35. 2 2
      app/Services/ExportService.php
  36. 16 23
      app/Services/ImportService.php
  37. 1 1
      app/Utils/feverUtil.php
  38. 3 3
      app/actualize_script.php
  39. 48 40
      app/install.php
  40. 1 1
      app/views/helpers/export/articles.phtml
  41. 4 2
      app/views/helpers/export/opml.phtml
  42. 1 1
      app/views/helpers/javascript_vars.phtml
  43. 2 2
      app/views/index/logs.phtml
  44. 1 1
      app/views/index/normal.phtml
  45. 1 1
      app/views/stats/idle.phtml
  46. 2 2
      cli/_cli.php
  47. 1 1
      cli/_update-or-create-user.php
  48. 2 2
      cli/actualize-user.php
  49. 2 2
      cli/db-optimize.php
  50. 2 2
      cli/delete-user.php
  51. 2 2
      cli/export-opml-for-user.php
  52. 2 2
      cli/export-sqlite-for-user.php
  53. 2 2
      cli/export-zip-for-user.php
  54. 2 2
      cli/i18n/I18nData.php
  55. 2 2
      cli/import-for-user.php
  56. 2 2
      cli/import-sqlite-for-user.php
  57. 6 6
      cli/user-info.php
  58. 8 2
      composer.json
  59. 136 77
      composer.lock
  60. 15 13
      lib/Minz/Configuration.php
  61. 19 0
      lib/Minz/ConfigurationSetterInterface.php
  62. 11 14
      lib/Minz/Dispatcher.php
  63. 1 1
      lib/Minz/Error.php
  64. 4 4
      lib/Minz/Extension.php
  65. 1 1
      lib/Minz/ExtensionManager.php
  66. 3 3
      lib/Minz/FrontController.php
  67. 2 3
      lib/Minz/Mailer.php
  68. 4 5
      lib/Minz/Migrator.php
  69. 1 1
      lib/Minz/ModelArray.php
  70. 1 1
      lib/Minz/ModelPdo.php
  71. 28 30
      lib/Minz/Paginator.php
  72. 2 2
      lib/Minz/Request.php
  73. 3 4
      lib/Minz/Translate.php
  74. 13 18
      lib/Minz/Url.php
  75. 5 6
      lib/Minz/View.php
  76. 2 2
      lib/lib_date.php
  77. 8 4
      lib/lib_install.php
  78. 14 27
      lib/lib_rss.php
  79. 7 8
      p/api/fever.php
  80. 3 3
      p/f.php
  81. 2 2
      p/i/index.php
  82. 7 4
      phpstan.neon
  83. 5 6
      tests/app/Models/CategoryTest.php
  84. 1 1
      tests/app/Models/LogDAOTest.php
  85. 52 65
      tests/app/Models/SearchTest.php
  86. 37 27
      tests/app/Models/UserQueryTest.php
  87. 3 3
      tests/app/Utils/passwordUtilTest.php
  88. 7 6
      tests/cli/i18n/I18nCompletionValidatorTest.php
  89. 35 33
      tests/cli/i18n/I18nDataTest.php
  90. 3 2
      tests/cli/i18n/I18nFileTest.php
  91. 8 7
      tests/cli/i18n/I18nUsageValidatorTest.php
  92. 9 9
      tests/cli/i18n/I18nValueTest.php
  93. 2 2
      tests/fixtures/migrations/2019_12_22_FooBar.php
  94. 2 2
      tests/fixtures/migrations/2019_12_23_Baz.php
  95. 2 2
      tests/fixtures/migrations_with_failing/2020_01_11_FooBar.php
  96. 2 2
      tests/fixtures/migrations_with_failing/2020_01_12_Baz.php
  97. 1 1
      tests/lib/CssXPath/CssXPathTest.php
  98. 23 23
      tests/lib/Minz/MigratorTest.php
  99. 1 1
      tests/lib/PHPMailer/PHPMailerTest.php
  100. 43 5
      tests/phpstan-next.txt

+ 2 - 2
app/Controllers/categoryController.php

@@ -33,7 +33,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 		$url_redirect = array('c' => 'subscription', 'a' => 'add');
 
 		$limits = FreshRSS_Context::$system_conf->limits;
-		$this->view->categories = $catDAO->listCategories(false);
+		$this->view->categories = $catDAO->listCategories(false) ?: [];
 
 		if (count($this->view->categories) >= $limits['max_categories']) {
 			Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), $url_redirect);
@@ -231,7 +231,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 
 			if (Minz_Request::paramBoolean('ajax')) {
 				Minz_Request::setGoodNotification(_t('feedback.sub.category.updated'));
-				$this->view->_layout(false);
+				$this->view->_layout(null);
 			} else {
 				if ($ok) {
 					Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);

+ 2 - 2
app/Controllers/configureController.php

@@ -264,7 +264,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 		FreshRSS_Context::$user_conf->volatile = $volatile;
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
-		$this->view->nb_total = $entryDAO->count();
+		$this->view->nb_total = $entryDAO->count() ?: 0;
 
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$this->view->size_user = $databaseDAO->size();
@@ -338,7 +338,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 	 * applied to the selected query.
 	 */
 	public function queryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 		$id = Minz_Request::paramInt('id');
 		if ($id !== 0 || !isset(FreshRSS_Context::$user_conf->queries[$id])) {

+ 2 - 2
app/Controllers/entryController.php

@@ -24,7 +24,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		if ($this->ajax) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 			Minz_Request::_param('ajax');
 		}
 	}
@@ -107,7 +107,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 			$ids = is_array($id) ? $id : array($id);
 			$entryDAO->markRead($ids, $is_read);
 			$tagDAO = FreshRSS_Factory::createTagDao();
-			$tagsForEntries = $tagDAO->getTagsForEntries($ids);
+			$tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: [];
 			$tags = array();
 			foreach ($tagsForEntries as $line) {
 				$tags['t_' . $line['id_tag']][] = $line['id_entry'];

+ 3 - 3
app/Controllers/extensionController.php

@@ -79,7 +79,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 	 */
 	public function configureAction(): void {
 		if (Minz_Request::paramBoolean('ajax')) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 			$this->indexAction();
 			$this->view->_path('extension/index.phtml');
@@ -143,7 +143,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 			if ($conf !== null && $res === true) {
 				$ext_list = $conf->extensions_enabled;
-				$ext_list = array_filter($ext_list, function($key) use($type) {
+				$ext_list = array_filter($ext_list, static function(string $key) use($type) {
 					// Remove from list the extensions that have disappeared or changed type
 					$extension = Minz_ExtensionManager::findExtension($key);
 					return $extension !== null && $extension->getType() === $type;
@@ -205,7 +205,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 			if ($conf !== null && $res === true) {
 				$ext_list = $conf->extensions_enabled;
-				$ext_list = array_filter($ext_list, function($key) use($type) {
+				$ext_list = array_filter($ext_list, static function(string $key) use($type) {
 					// Remove from list the extensions that have disappeared or changed type
 					$extension = Minz_ExtensionManager::findExtension($key);
 					return $extension !== null && $extension->getType() === $type;

+ 5 - 4
app/Controllers/feedController.php

@@ -262,7 +262,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			FreshRSS_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 
 			$catDAO = FreshRSS_Factory::createCategoryDao();
-			$this->view->categories = $catDAO->listCategories(false);
+			$this->view->categories = $catDAO->listCategories(false) ?: [];
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
 				// We try to get more information about the feed.
@@ -316,7 +316,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 	/**
 	 * @return array{0:int,1:FreshRSS_Feed|false,2:int}
 	 */
-	public static function actualizeFeed(int $feed_id, string $feed_url, bool $force, ?SimplePie $simplePiePush = null, bool $noCommit = false, int $maxFeeds = 10) {
+	public static function actualizeFeed(int $feed_id, string $feed_url, bool $force, ?SimplePie $simplePiePush = null,
+		bool $noCommit = false, int $maxFeeds = 10): array {
 		@set_time_limit(300);
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -666,7 +667,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			// are several updated feeds.
 			Minz_Request::setGoodNotification(_t('feedback.sub.feed.actualizeds'));
 			// No layout in ajax request.
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 			// Redirect to the main page with correct notification.
 			if ($updated_feeds === 1) {
@@ -903,7 +904,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$this->view->selectorSuccess = false;
 		$this->view->htmlContent = '';
 
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 		$this->_csp([
 			'default-src' => "'self'",

+ 4 - 6
app/Controllers/importExportController.php

@@ -226,7 +226,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		unset($table['article']);
 		for ($i = count($table['items']) - 1; $i >= 0; $i--) {
 			$item = (array)($table['items'][$i]);
-			$item = array_filter($item, function ($v) {
+			$item = array_filter($item, static function ($v) {
 					// Filter out empty properties, potentially reported as empty objects
 					return (is_string($v) && trim($v) !== '') || !empty($v);
 				});
@@ -267,7 +267,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 	 *
 	 * $article_file the JSON file content.
 	 * true if articles from the file must be starred.
-	 * @return boolean false if an error occurred, true otherwise.
+	 * @return bool false if an error occurred, true otherwise.
 	 */
 	private function importJson(string $article_file, bool $starred = false): bool {
 		$article_object = json_decode($article_file, true);
@@ -575,10 +575,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 	 *   - export_starred (default: false)
 	 *   - export_labelled (default: false)
 	 *   - export_feeds (default: array()) a list of feed ids
-	 *
-	 * @return void|null
 	 */
-	public function exportAction() {
+	public function exportAction(): void {
 		if (!Minz_Request::isPost()) {
 			Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
 			return;
@@ -654,7 +652,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		header('Content-Type: ' . $content_type);
 		header('Content-disposition: attachment; filename="' . $filename . '"');
 
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		$this->view->content = $content;
 	}
 

+ 6 - 6
app/Controllers/indexController.php

@@ -58,10 +58,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 
 		FreshRSS_Context::$id_max = time() . '000000';
 
-		$this->view->callbackBeforeFeeds = function ($view) {
+		$this->view->callbackBeforeFeeds = function (FreshRSS_View $view) {
 			try {
 				$tagDAO = FreshRSS_Factory::createTagDao();
-				$view->tags = $tagDAO->listTags(true);
+				$view->tags = $tagDAO->listTags(true) ?: [];
 				$view->nbUnreadTags = 0;
 				foreach ($view->tags as $tag) {
 					$view->nbUnreadTags += $tag->nbUnread();
@@ -71,7 +71,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 			}
 		};
 
-		$this->view->callbackBeforeEntries = function ($view) {
+		$this->view->callbackBeforeEntries = function (FreshRSS_View $view) {
 			try {
 				FreshRSS_Context::$number++;	//+1 for articles' page
 				$view->entries = FreshRSS_index_Controller::listEntriesByContext();
@@ -83,7 +83,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 			}
 		};
 
-		$this->view->callbackBeforePagination = function ($view, $nbEntries, $lastEntry) {
+		$this->view->callbackBeforePagination = function (?FreshRSS_View $view, int $nbEntries, FreshRSS_Entry $lastEntry) {
 			if ($nbEntries >= FreshRSS_Context::$number) {
 				//We have enough entries: we discard the last one to use it for the next articles' page
 				ob_clean();
@@ -170,7 +170,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		// No layout for RSS output.
 		$this->view->rss_url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
 		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		header('Content-Type: application/rss+xml; charset=utf-8');
 	}
 
@@ -238,7 +238,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		}
 
 		// No layout for OPML output.
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		header('Content-Type: application/xml; charset=utf-8');
 	}
 

+ 3 - 3
app/Controllers/javascriptController.php

@@ -3,7 +3,7 @@
 class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 
 	public function firstAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 	}
 
 	public function actualizeAction(): void {
@@ -20,9 +20,9 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 	public function nbUnreadsPerFeedAction(): void {
 		header('Content-Type: application/json; charset=UTF-8');
 		$catDAO = FreshRSS_Factory::createCategoryDao();
-		$this->view->categories = $catDAO->listCategories(true, false);
+		$this->view->categories = $catDAO->listCategories(true, false) ?: [];
 		$tagDAO = FreshRSS_Factory::createTagDao();
-		$this->view->tags = $tagDAO->listTags(true);
+		$this->view->tags = $tagDAO->listTags(true) ?: [];
 	}
 
 	//For Web-form login

+ 2 - 2
app/Controllers/statsController.php

@@ -26,7 +26,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
 
 		$catDAO->checkDefault();
 		$feedDAO->updateTTL();
-		$this->view->categories = $catDAO->listSortedCategories(false);
+		$this->view->categories = $catDAO->listSortedCategories(false) ?: [];
 		$this->view->default_category = $catDAO->getDefault();
 
 		FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
@@ -207,7 +207,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
 			$id = null;
 		}
 
-		$this->view->categories 	= $categoryDAO->listCategories();
+		$this->view->categories 	= $categoryDAO->listCategories() ?: [];
 		$this->view->feed 			= $id === null ? null : $feedDAO->searchById($id);
 		$this->view->days 			= $statsDAO->getDays();
 		$this->view->months 		= $statsDAO->getMonths();

+ 6 - 6
app/Controllers/subscriptionController.php

@@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 
 		$catDAO->checkDefault();
 		$feedDAO->updateTTL();
-		$this->view->categories = $catDAO->listSortedCategories(false, true);
+		$this->view->categories = $catDAO->listSortedCategories(false, true) ?: [];
 		$this->view->default_category = $catDAO->getDefault();
 
 		$signalError = false;
@@ -90,7 +90,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 	 */
 	public function feedAction(): void {
 		if (Minz_Request::paramBoolean('ajax')) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 			FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
 		}
@@ -200,7 +200,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				]);
 			}
 
-			$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::paramString('filteractions_read')));
+			$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::paramString('filteractions_read')) ?: []);
 
 			$feed->_kind(Minz_Request::paramInt('feed_kind') ?: FreshRSS_Feed::KIND_RSS);
 			if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
@@ -235,8 +235,8 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				'name' => Minz_Request::paramString('name'),
 				'kind' => $feed->kind(),
 				'description' => sanitizeHTML(Minz_Request::paramString('description', true)),
-				'website' => checkUrl(Minz_Request::paramString('website')),
-				'url' => checkUrl(Minz_Request::paramString('url')),
+				'website' => checkUrl(Minz_Request::paramString('website')) ?: '',
+				'url' => checkUrl(Minz_Request::paramString('url')) ?: '',
 				'category' => Minz_Request::paramInt('category'),
 				'pathEntries' => Minz_Request::paramString('path_entries'),
 				'priority' => Minz_Request::paramTernary('priority') === null ? FreshRSS_Feed::PRIORITY_MAIN_STREAM : Minz_Request::paramInt('priority'),
@@ -283,7 +283,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 	}
 
 	public function categoryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 

+ 7 - 8
app/Controllers/tagController.php

@@ -23,7 +23,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		if ($this->ajax) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 			Minz_Request::_param('ajax');
 		}
 	}
@@ -39,7 +39,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 			$checked = Minz_Request::paramTernary('checked');
 			if ($id_entry != '') {
 				$tagDAO = FreshRSS_Factory::createTagDao();
-				if ($id_tag === 0 && $name_tag !== '' && $checked) {
+				if ($id_tag == 0 && $name_tag !== '' && $checked) {
 					if ($existing_tag = $tagDAO->searchByName($name_tag)) {
 						// Use existing tag
 						$tagDAO->tagEntry($existing_tag->id(), $id_entry, $checked);
@@ -48,7 +48,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 						$id_tag = $tagDAO->addTag(array('name' => $name_tag));
 					}
 				}
-				if ($id_tag !== 0) {
+				if ($id_tag != false) {
 					$tagDAO->tagEntry($id_tag, $id_entry, $checked);
 				}
 			}
@@ -82,12 +82,12 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 	}
 
 	public function getTagsForEntryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Cache-Control: private, no-cache, no-store, must-revalidate');
 		$id_entry = Minz_Request::paramString('id_entry');
 		$tagDAO = FreshRSS_Factory::createTagDao();
-		$this->view->tagsForEntry = $tagDAO->getTagsForEntry($id_entry);
+		$this->view->tagsForEntry = $tagDAO->getTagsForEntry($id_entry) ?: [];
 	}
 
 	public function addAction(): void {
@@ -110,11 +110,10 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 	}
 
 	/**
-	 * @return void|null
 	 * @throws Minz_ConfigurationNamespaceException
 	 * @throws Minz_PDOConnectionException
 	 */
-	public function renameAction() {
+	public function renameAction(): void {
 		if (!Minz_Request::isPost()) {
 			Minz_Error::error(405);
 		}
@@ -145,6 +144,6 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 
 	public function indexAction(): void {
 		$tagDAO = FreshRSS_Factory::createTagDao();
-		$this->view->tags = $tagDAO->listTags();
+		$this->view->tags = $tagDAO->listTags() ?: [];
 	}
 }

+ 3 - 3
app/Models/Auth.php

@@ -135,9 +135,9 @@ class FreshRSS_Auth {
 	 * Returns if current user has access to the given scope.
 	 *
 	 * @param string $scope general (default) or admin
-	 * @return boolean true if user has corresponding access, false else.
+	 * @return bool true if user has corresponding access, false else.
 	 */
-	public static function hasAccess($scope = 'general'): bool {
+	public static function hasAccess(string $scope = 'general'): bool {
 		if (FreshRSS_Context::$user_conf == null) {
 			return false;
 		}
@@ -154,7 +154,7 @@ class FreshRSS_Auth {
 		default:
 			$ok = false;
 		}
-		return $ok;
+		return (bool)$ok;
 	}
 
 	/**

+ 1 - 1
app/Models/BooleanSearch.php

@@ -230,7 +230,7 @@ class FreshRSS_BooleanSearch {
 		if ($input == '') {
 			return;
 		}
-		$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
+		$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
 
 		$segment = '';
 		$ns = count($splits);

+ 1 - 1
app/Models/Category.php

@@ -262,7 +262,7 @@ class FreshRSS_Category extends Minz_Model {
 		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$catDAO->updateLastUpdate($this->id(), !$ok);
 
-		return $ok;
+		return (bool)$ok;
 	}
 
 	private function sortFeeds(): void {

+ 1 - 1
app/Models/CategoryDAO.php

@@ -477,7 +477,7 @@ SQL;
 			$cat->_id($previousLine['c_id']);
 			$cat->_kind($previousLine['c_kind']);
 			$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
-			$cat->_error($previousLine['c_error'] ?? false);
+			$cat->_error($previousLine['c_error'] ?? 0);
 			$cat->_attributes('', $previousLine['c_attributes']);
 			$list[$previousLine['c_id']] = $cat;
 		}

+ 4 - 5
app/Models/Context.php

@@ -110,10 +110,9 @@ final class FreshRSS_Context {
 
 	/**
 	 * Initialize the context for the current user.
-	 * @return FreshRSS_UserConfiguration|false
 	 * @throws Minz_ConfigurationParamException
 	 */
-	public static function initUser(string $username = '', bool $userMustExist = true) {
+	public static function initUser(string $username = '', bool $userMustExist = true): ?FreshRSS_UserConfiguration {
 		FreshRSS_Context::$user_conf = null;
 		if (!isset($_SESSION)) {
 			Minz_Session::init('FreshRSS');
@@ -145,7 +144,7 @@ final class FreshRSS_Context {
 		Minz_Session::unlock();
 
 		if (FreshRSS_Context::$user_conf == null) {
-			return false;
+			return null;
 		}
 
 		FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
@@ -249,8 +248,8 @@ final class FreshRSS_Context {
 	/**
 	 * Return the current get as a string or an array.
 	 *
-	 * If $array is true, the first item of the returned value is 'f' or 'c' and
-	 * the second is the id.
+	 * If $array is true, the first item of the returned value is 'f' or 'c' or 't' and the second is the id.
+	 * @phpstan-return ($asArray is true ? array{'c'|'f'|'t',bool|int} : string)
 	 * @return string|array{string,bool|int}
 	 */
 	public static function currentGet(bool $asArray = false) {

+ 1 - 1
app/Models/DatabaseDAO.php

@@ -79,7 +79,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 			$ok &= in_array($c['name'], $schema);
 		}
 
-		return $ok;
+		return (bool)$ok;
 	}
 
 	public function categoryIsCorrect(): bool {

+ 6 - 3
app/Models/DatabaseDAOSQLite.php

@@ -8,7 +8,10 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	public function tablesAreCorrect(): bool {
 		$sql = 'SELECT name FROM sqlite_master WHERE type="table"';
 		$stm = $this->pdo->query($sql);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$res = $stm ? $stm->fetchAll(PDO::FETCH_ASSOC) : false;
+		if ($res === false) {
+			return false;
+		}
 
 		$tables = array(
 			$this->pdo->prefix() . 'category' => false,
@@ -29,7 +32,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	public function getSchema(string $table): array {
 		$sql = 'PRAGMA table_info(' . $table . ')';
 		$stm = $this->pdo->query($sql);
-		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
+		return $stm ? $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC) ?: []) : [];
 	}
 
 	public function entryIsCorrect(): bool {
@@ -62,7 +65,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	public function size(bool $all = false): int {
 		$sum = 0;
 		if ($all) {
-			foreach (glob(DATA_PATH . '/users/*/db.sqlite') as $filename) {
+			foreach (glob(DATA_PATH . '/users/*/db.sqlite') ?: [] as $filename) {
 				$sum += @filesize($filename);
 			}
 		} else {

+ 7 - 4
app/Models/Entry.php

@@ -106,7 +106,7 @@ class FreshRSS_Entry extends Minz_Model {
 		return $this->authors(true);
 	}
 	/**
-	 * @phpstan return ($asString ? string : array<string>)
+	 * @phpstan-return ($asString is true ? string : array<string>)
 	 * @return string|array<string>
 	 */
 	public function authors(bool $asString = false) {
@@ -285,7 +285,7 @@ HTML;
 	/**
 	 * @return array<string,string>|null
 	 */
-	public function thumbnail(bool $searchEnclosures = true) {
+	public function thumbnail(bool $searchEnclosures = true): ?array {
 		$thumbnail = $this->attributes('thumbnail');
 		if (!empty($thumbnail['url'])) {
 			return $thumbnail;
@@ -352,7 +352,10 @@ HTML;
 		return $this->feedId;
 	}
 
-	/** @return string|array<string> */
+	/**
+	 * @phpstan-return ($asString is true ? string : array<string>)
+	 * @return string|array<string>
+	 */
 	public function tags(bool $asString = false) {
 		if ($asString) {
 			return $this->tags == null ? '' : '#' . implode(' #', $this->tags);
@@ -609,7 +612,7 @@ HTML;
 				}
 			}
 		}
-		return $ok;
+		return (bool)$ok;
 	}
 
 	/** @param array<string,int> $titlesAsRead  */

+ 18 - 22
app/Models/EntryDAO.php

@@ -308,7 +308,7 @@ SQL;
 	 * there is an other way to do that.
 	 *
 	 * @param string|array<string> $ids
-	 * @return false|integer
+	 * @return int|false
 	 */
 	public function markFavorite($ids, bool $is_favorite = true) {
 		if (!is_array($ids)) {
@@ -348,12 +348,8 @@ SQL;
 	 * feeds from one category or on all feeds.
 	 *
 	 * @todo It can use the query builder refactoring to build that query
-	 *
-	 * @param false|integer $catId category ID
-	 * @param false|integer $feedId feed ID
-	 * @return boolean
 	 */
-	protected function updateCacheUnreads($catId = false, $feedId = false) {
+	protected function updateCacheUnreads(?int $catId = null, ?int $feedId = null): bool {
 		$sql = 'UPDATE `_feed` f '
 			. 'LEFT OUTER JOIN ('
 			.	'SELECT e.id_feed, '
@@ -365,13 +361,13 @@ SQL;
 			. 'SET f.`cache_nbUnreads`=COALESCE(x.nbUnreads, 0)';
 		$hasWhere = false;
 		$values = array();
-		if ($feedId !== false) {
+		if ($feedId != null) {
 			$sql .= ' WHERE';
 			$hasWhere = true;
 			$sql .= ' f.id=?';
 			$values[] = $feedId;
 		}
-		if ($catId !== false) {
+		if ($catId != null) {
 			$sql .= $hasWhere ? ' AND' : ' WHERE';
 			$hasWhere = true;
 			$sql .= ' f.category=?';
@@ -397,8 +393,8 @@ SQL;
 	 * same if it is an array or not.
 	 *
 	 * @param string|array<string> $ids
-	 * @param boolean $is_read
-	 * @return integer|false affected rows
+	 * @param bool $is_read
+	 * @return int|false affected rows
 	 */
 	public function markRead($ids, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -431,7 +427,7 @@ SQL;
 				return false;
 			}
 			$affected = $stm->rowCount();
-			if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 				return false;
 			}
 			return $affected;
@@ -469,7 +465,7 @@ SQL;
 	 * separated.
 	 *
 	 * @param string $idMax fail safe article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
 		?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
@@ -498,7 +494,7 @@ SQL;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 			return false;
 		}
 		return $affected;
@@ -511,9 +507,9 @@ SQL;
 	 *
 	 * If $idMax equals 0, a deprecated debug message is logged
 	 *
-	 * @param integer $id category ID
+	 * @param int $id category ID
 	 * @param string $idMax fail safe article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -536,7 +532,7 @@ SQL;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, null))) {
 			return false;
 		}
 		return $affected;
@@ -549,9 +545,9 @@ SQL;
 	 *
 	 * If $idMax equals 0, a deprecated debug message is logged
 	 *
-	 * @param integer $id_feed feed ID
+	 * @param int $id_feed feed ID
 	 * @param string $idMax fail safe article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadFeed(int $id_feed, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -597,9 +593,9 @@ SQL;
 
 	/**
 	 * Mark all the articles in a tag as read.
-	 * @param integer $id tag ID, or empty for targeting any tag
+	 * @param int $id tag ID, or empty for targeting any tag
 	 * @param string $idMax max article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadTag(int $id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null,
 		int $state = 0, bool $is_read = true) {
@@ -630,7 +626,7 @@ SQL;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 			return false;
 		}
 		return $affected;
@@ -758,7 +754,7 @@ SQL;
 	}
 
 	/** @return array{0:array<int|string>,1:string} */
-	public static function sqlBooleanSearch(string $alias, FreshRSS_BooleanSearch $filters, int $level = 0) {
+	public static function sqlBooleanSearch(string $alias, FreshRSS_BooleanSearch $filters, int $level = 0): array {
 		$search = '';
 		$values = [];
 

+ 18 - 18
app/Models/EntryDAOSQLite.php

@@ -25,7 +25,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	/** @param array<string> $errorInfo */
 	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) {
-			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
+			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1) ?: [];
 			foreach (['attributes'] as $column) {
 				if (!in_array($column, $columns)) {
 					return $this->addColumn($column);
@@ -34,14 +34,14 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 		}
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
-			if (stripos($showCreate, 'tag') === false) {
+			if (is_string($showCreate) && stripos($showCreate, 'tag') === false) {
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				return $tagDAO->createTagTable();	//v1.12.0
 			}
 		}
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
 			$showCreate = $tableInfo->fetchColumn();
-			if (stripos($showCreate, 'entrytmp') === false) {
+			if (is_string($showCreate) && stripos($showCreate, 'entrytmp') === false) {
 				return $this->createEntryTempTable();	//v1.7.0
 			}
 		}
@@ -78,20 +78,20 @@ DROP TABLE IF EXISTS `tmp`;
 		return $result;
 	}
 
-	protected function updateCacheUnreads($catId = false, $feedId = false) {
+	protected function updateCacheUnreads(?int $catId = null, ?int $feedId = null): bool {
 		$sql = 'UPDATE `_feed` '
 		 . 'SET `cache_nbUnreads`=('
 		 .	'SELECT COUNT(*) AS nbUnreads FROM `_entry` e '
 		 .	'WHERE e.id_feed=`_feed`.id AND e.is_read=0)';
 		$hasWhere = false;
 		$values = array();
-		if ($feedId !== false) {
+		if ($feedId != null) {
 			$sql .= ' WHERE';
 			$hasWhere = true;
 			$sql .= ' id=?';
 			$values[] = $feedId;
 		}
-		if ($catId !== false) {
+		if ($catId != null) {
 			$sql .= $hasWhere ? ' AND' : ' WHERE';
 			$hasWhere = true;
 			$sql .= ' category=?';
@@ -117,8 +117,8 @@ DROP TABLE IF EXISTS `tmp`;
 	 * same if it is an array or not.
 	 *
 	 * @param string|array<string> $ids
-	 * @param boolean $is_read
-	 * @return integer|false affected rows
+	 * @param bool $is_read
+	 * @return int|false affected rows
 	 */
 	public function markRead($ids, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -176,9 +176,9 @@ DROP TABLE IF EXISTS `tmp`;
 	 * separated.
 	 *
 	 * @param string $idMax fail safe article ID
-	 * @param boolean $onlyFavorites
-	 * @param integer $priorityMin
-	 * @return integer|false affected rows
+	 * @param bool $onlyFavorites
+	 * @param int $priorityMin
+	 * @return int|false affected rows
 	 */
 	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
 		?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
@@ -205,7 +205,7 @@ DROP TABLE IF EXISTS `tmp`;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 			return false;
 		}
 		return $affected;
@@ -218,9 +218,9 @@ DROP TABLE IF EXISTS `tmp`;
 	 *
 	 * If $idMax equals 0, a deprecated debug message is logged
 	 *
-	 * @param integer $id category ID
+	 * @param int $id category ID
 	 * @param string $idMax fail safe article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -244,7 +244,7 @@ DROP TABLE IF EXISTS `tmp`;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, null))) {
 			return false;
 		}
 		return $affected;
@@ -252,9 +252,9 @@ DROP TABLE IF EXISTS `tmp`;
 
 	/**
 	 * Mark all the articles in a tag as read.
-	 * @param integer $id tag ID, or empty for targeting any tag
+	 * @param int $id tag ID, or empty for targeting any tag
 	 * @param string $idMax max article ID
-	 * @return integer|false affected rows
+	 * @return int|false affected rows
 	 */
 	public function markReadTag($id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -283,7 +283,7 @@ DROP TABLE IF EXISTS `tmp`;
 			return false;
 		}
 		$affected = $stm->rowCount();
-		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+		if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 			return false;
 		}
 		return $affected;

+ 6 - 6
app/Models/Factory.php

@@ -13,7 +13,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createCategoryDao(?string $username = null): FreshRSS_CategoryDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_CategoryDAOSQLite($username);
 			default:
@@ -25,7 +25,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createFeedDao(?string $username = null): FreshRSS_FeedDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_FeedDAOSQLite($username);
 			default:
@@ -37,7 +37,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createEntryDao(?string $username = null): FreshRSS_EntryDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_EntryDAOSQLite($username);
 			case 'pgsql':
@@ -51,7 +51,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createTagDao(?string $username = null): FreshRSS_TagDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_TagDAOSQLite($username);
 			case 'pgsql':
@@ -65,7 +65,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createStatsDAO(?string $username = null): FreshRSS_StatsDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_StatsDAOSQLite($username);
 			case 'pgsql':
@@ -79,7 +79,7 @@ class FreshRSS_Factory {
 	 * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
 	 */
 	public static function createDatabaseDAO(?string $username = null): FreshRSS_DatabaseDAO {
-		switch (FreshRSS_Context::$system_conf->db['type']) {
+		switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
 			case 'sqlite':
 				return new FreshRSS_DatabaseDAOSQLite($username);
 			case 'pgsql':

+ 57 - 52
app/Models/Feed.php

@@ -71,6 +71,7 @@ class FreshRSS_Feed extends Minz_Model {
 	private $error = false;
 	/** @var int */
 	private $ttl = self::TTL_DEFAULT;
+	/** @var array<string,mixed> */
 	private $attributes = [];
 	/** @var bool */
 	private $mute = false;
@@ -93,10 +94,7 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
-	/**
-	 * @return FreshRSS_Feed
-	 */
-	public static function example() {
+	public static function example(): FreshRSS_Feed {
 		$f = new FreshRSS_Feed('http://example.net/', false);
 		$f->faviconPrepare();
 		return $f;
@@ -142,12 +140,16 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->categoryId;
 	}
 
-	public function entries() {
+	/**
+	 * @return array<FreshRSS_Entry>|null
+	 * @deprecated
+	 */
+	public function entries(): ?array {
 		Minz_Log::warning(__method__ . ' is deprecated since FreshRSS 1.16.1!');
 		$simplePie = $this->load(false, true);
 		return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie));
 	}
-	public function name($raw = false): string {
+	public function name(bool $raw = false): string {
 		return $raw || $this->name != '' ? $this->name : preg_replace('%^https?://(www[.])?%i', '', $this->url);
 	}
 	/** @return string HTML-encoded URL of the Web site of the feed */
@@ -167,7 +169,11 @@ class FreshRSS_Feed extends Minz_Model {
 	public function pathEntries(): string {
 		return $this->pathEntries;
 	}
-	public function httpAuth($raw = true) {
+	/**
+	 * @phpstan-return ($raw is true ? string : array{'username':string,'password':string})
+	 * @return array{'username':string,'password':string}|string
+	 */
+	public function httpAuth(bool $raw = true) {
 		if ($raw) {
 			return $this->httpAuth;
 		} else {
@@ -223,7 +229,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 		return $this->nbEntries;
 	}
-	public function nbNotRead($includePending = false): int {
+	public function nbNotRead(bool $includePending = false): int {
 		if ($this->nbNotRead < 0) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$this->nbNotRead = $feedDAO->countNotRead($this->id());
@@ -231,7 +237,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 		return $this->nbNotRead + ($includePending ? $this->nbPendingNotRead : 0);
 	}
-	public function faviconPrepare() {
+	public function faviconPrepare(): void {
 		require_once(LIB_PATH . '/favicons.php');
 		$url = $this->website;
 		if ($url == '') {
@@ -253,7 +259,7 @@ class FreshRSS_Feed extends Minz_Model {
 			}
 		}
 	}
-	public static function faviconDelete($hash) {
+	public static function faviconDelete(string $hash): void {
 		$path = DATA_PATH . '/favicons/' . $hash;
 		@unlink($path . '.ico');
 		@unlink($path . '.txt');
@@ -262,10 +268,11 @@ class FreshRSS_Feed extends Minz_Model {
 		return Minz_Url::display('/f.php?' . $this->hash());
 	}
 
-	public function _id($value) {
-		$this->id = intval($value);
+	public function _id(int $value): void {
+		$this->id = $value;
 	}
-	public function _url(string $value, bool $validate = true) {
+
+	public function _url(string $value, bool $validate = true): void {
 		$this->hash = '';
 		$url = $value;
 		if ($validate) {
@@ -276,26 +283,26 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 		$this->url = $url;
 	}
-	public function _kind(int $value) {
+
+	public function _kind(int $value): void {
 		$this->kind = $value;
 	}
 
-	/** @param FreshRSS_Category|null $cat */
-	public function _category($cat) {
+	public function _category(?FreshRSS_Category $cat): void {
 		$this->category = $cat;
 		$this->categoryId = $this->category == null ? 0 : $this->category->id();
 	}
 
 	/** @param int|string $id */
-	public function _categoryId($id) {
+	public function _categoryId($id): void {
 		$this->category = null;
 		$this->categoryId = intval($id);
 	}
 
-	public function _name(string $value) {
+	public function _name(string $value): void {
 		$this->name = $value == '' ? '' : trim($value);
 	}
-	public function _website(string $value, bool $validate = true) {
+	public function _website(string $value, bool $validate = true): void {
 		if ($validate) {
 			$value = checkUrl($value);
 		}
@@ -304,37 +311,37 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 		$this->website = $value;
 	}
-	public function _description(string $value) {
+	public function _description(string $value): void {
 		$this->description = $value == '' ? '' : $value;
 	}
-	public function _lastUpdate($value) {
-		$this->lastUpdate = intval($value);
+	public function _lastUpdate(int $value): void {
+		$this->lastUpdate = $value;
 	}
-	public function _priority($value) {
-		$this->priority = intval($value);
+	public function _priority(int $value): void {
+		$this->priority = $value;
 	}
 	/** @param string $value HTML-encoded CSS selector */
-	public function _pathEntries(string $value) {
+	public function _pathEntries(string $value): void {
 		$this->pathEntries = $value;
 	}
-	public function _httpAuth(string $value) {
+	public function _httpAuth(string $value): void {
 		$this->httpAuth = $value;
 	}
-	public function _error($value) {
+	/** @param bool|int $value */
+	public function _error($value): void {
 		$this->error = (bool)$value;
 	}
-	public function _mute(bool $value) {
+	public function _mute(bool $value): void {
 		$this->mute = $value;
 	}
-	public function _ttl($value) {
-		$value = intval($value);
+	public function _ttl(int $value): void {
 		$value = min($value, 100000000);
 		$this->ttl = abs($value);
 		$this->mute = $value < self::TTL_DEFAULT;
 	}
 
 	/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
-	public function _attributes(string $key, $value) {
+	public function _attributes(string $key, $value): void {
 		if ($key == '') {
 			if (is_string($value)) {
 				$value = json_decode($value, true);
@@ -349,17 +356,14 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
-	public function _nbNotRead($value) {
-		$this->nbNotRead = intval($value);
+	public function _nbNotRead(int $value): void {
+		$this->nbNotRead = $value;
 	}
-	public function _nbEntries($value) {
-		$this->nbEntries = intval($value);
+	public function _nbEntries(int $value): void {
+		$this->nbEntries = $value;
 	}
 
-	/**
-	 * @return SimplePie|null
-	 */
-	public function load(bool $loadDetails = false, bool $noCache = false) {
+	public function load(bool $loadDetails = false, bool $noCache = false): ?SimplePie {
 		if ($this->url != '') {
 			// @phpstan-ignore-next-line
 			if (CACHE_PATH === false) {
@@ -440,7 +444,7 @@ class FreshRSS_Feed extends Minz_Model {
 	/**
 	 * @return array<string>
 	 */
-	public function loadGuids(SimplePie $simplePie) {
+	public function loadGuids(SimplePie $simplePie): array {
 		$hasUniqueGuids = true;
 		$testGuids = [];
 		$guids = [];
@@ -474,6 +478,9 @@ class FreshRSS_Feed extends Minz_Model {
 		return $guids;
 	}
 
+	/**
+	 * @return iterable<FreshRSS_Entry>
+	 */
 	public function loadEntries(SimplePie $simplePie) {
 		$hasBadGuids = $this->attributes('hasBadGuids');
 
@@ -591,10 +598,7 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
-	/**
-	 * @return SimplePie|null
-	 */
-	public function loadHtmlXpath() {
+	public function loadHtmlXpath(): ?SimplePie {
 		if ($this->url == '') {
 			return null;
 		}
@@ -708,7 +712,7 @@ class FreshRSS_Feed extends Minz_Model {
 				if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
 					// HTML-encoding/escaping of the relevant fields (all except 'content')
 					foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) {
-						if (!empty($item[$key])) {
+						if (!empty($item[$key]) && is_string($item[$key])) {
 							$item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]);
 						}
 					}
@@ -731,7 +735,7 @@ class FreshRSS_Feed extends Minz_Model {
 	/**
 	 * To keep track of some new potentially unread articles since last commit+fetch from database
 	 */
-	public function incPendingUnread(int $n = 1) {
+	public function incPendingUnread(int $n = 1): void {
 		$this->nbPendingNotRead += $n;
 	}
 
@@ -770,6 +774,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 	/**
 	 * Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+	 * @return int|false
 	 */
 	public function cleanOldEntries() {
 		$archiving = $this->attributes('archiving');
@@ -785,7 +790,6 @@ class FreshRSS_Feed extends Minz_Model {
 			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$nb = $entryDAO->cleanOldEntries($this->id(), $archiving);
 			if ($nb > 0) {
-				$needFeedCacheRefresh = true;
 				Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving));
 			}
 			return $nb;
@@ -793,6 +797,7 @@ class FreshRSS_Feed extends Minz_Model {
 		return false;
 	}
 
+	/** @param array<string,mixed> $attributes */
 	public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
 		$simplePie = customSimplePie($attributes);
 		$filename = $simplePie->get_cache_filename($url);
@@ -851,12 +856,12 @@ class FreshRSS_Feed extends Minz_Model {
 	}
 
 	/**
-	 * @param array<FreshRSS_FilterAction> $filterActions
+	 * @param array<FreshRSS_FilterAction>|null $filterActions
 	 */
-	private function _filterActions($filterActions) {
+	private function _filterActions(?array $filterActions): void {
 		$this->filterActions = $filterActions;
 		if (is_array($this->filterActions) && !empty($this->filterActions)) {
-			$this->_attributes('filters', array_map(function ($af) {
+			$this->_attributes('filters', array_map(static function (?FreshRSS_FilterAction $af) {
 					return $af == null ? null : $af->toJSON();
 				}, $this->filterActions));
 		} else {
@@ -885,10 +890,10 @@ class FreshRSS_Feed extends Minz_Model {
 	/**
 	 * @param array<string> $filters
 	 */
-	public function _filtersAction(string $action, $filters) {
+	public function _filtersAction(string $action, array $filters): void {
 		$action = trim($action);
 		if ($action == '' || !is_array($filters)) {
-			return false;
+			return;
 		}
 		$filters = array_unique(array_map('trim', $filters));
 		$filterActions = $this->filterActions();

+ 4 - 4
app/Models/FeedDAO.php

@@ -268,7 +268,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 	 * @param bool|null $muted to include only muted feeds
 	 * @return int|false
 	 */
-	public function deleteFeedByCategory(int $id, $muted = null) {
+	public function deleteFeedByCategory(int $id, ?bool $muted = null) {
 		$sql = 'DELETE FROM `_feed` WHERE category=?';
 		if ($muted) {
 			$sql .= ' AND ttl < 0';
@@ -338,7 +338,7 @@ SQL;
 	}
 
 	/** @return array<string,string> */
-	public function listFeedsNewestItemUsec(?int $id_feed = null) {
+	public function listFeedsNewestItemUsec(?int $id_feed = null): array {
 		$sql = 'SELECT id_feed, MAX(id) as newest_item_us FROM `_entry` ';
 		if ($id_feed === null) {
 			$sql .= 'GROUP BY id_feed';
@@ -358,7 +358,7 @@ SQL;
 	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
 	 * @return array<FreshRSS_Feed>
 	 */
-	public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0) {
+	public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
 		$this->updateTTL();
 		$sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
 			. 'FROM `_feed` '
@@ -398,7 +398,7 @@ SQL;
 	 * @param bool|null $muted to include only muted feeds
 	 * @return array<FreshRSS_Feed>
 	 */
-	public function listByCategory(int $cat, $muted = null): array {
+	public function listByCategory(int $cat, ?bool $muted = null): array {
 		$sql = 'SELECT * FROM `_feed` WHERE category=?';
 		if ($muted) {
 			$sql .= ' AND ttl < 0';

+ 3 - 3
app/Models/Search.php

@@ -234,11 +234,11 @@ class FreshRSS_Search {
 	}
 
 	/**
-	 * @param array<string> $anArray
+	 * @param array<string>|null $anArray
 	 * @return array<string>
 	 */
-	private static function removeEmptyValues($anArray): array {
-		return empty($anArray) ? [] : array_filter($anArray, function($value) { return $value !== ''; });
+	private static function removeEmptyValues(?array $anArray): array {
+		return empty($anArray) ? [] : array_filter($anArray, static function(string $value) { return $value !== ''; });
 	}
 
 	/**

+ 2 - 2
app/Models/Share.php

@@ -49,7 +49,7 @@ class FreshRSS_Share {
 			self::register($share_options);
 		}
 
-		uasort(self::$list_sharing, function ($a, $b) {
+		uasort(self::$list_sharing, static function (FreshRSS_Share $a, FreshRSS_Share $b) {
 			return strcasecmp($a->name(), $b->name());
 		});
 	}
@@ -303,7 +303,7 @@ class FreshRSS_Share {
 	 * @param array<string> $transform an array containing a list of functions to apply.
 	 * @return string the transformed data.
 	 */
-	private static function transform(string $data, $transform): string {
+	private static function transform(string $data, array $transform): string {
 		if (!is_array($transform) || empty($transform)) {
 			return $data;
 		}

+ 5 - 5
app/Models/StatsDAO.php

@@ -13,7 +13,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 	 *
 	 * @return array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int},'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}}
 	 */
-	public function calculateEntryRepartition() {
+	public function calculateEntryRepartition(): array {
 		return array(
 			'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true),
 			'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false),
@@ -57,7 +57,7 @@ SQL;
 	 * Calculates entry count per day on a 30 days period.
 	 * @return array<int,int>
 	 */
-	public function calculateEntryCount() {
+	public function calculateEntryCount(): array {
 		$count = $this->initEntryCountArray();
 		$midnight = mktime(0, 0, 0);
 		$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
@@ -87,7 +87,7 @@ SQL;
 	 * Initialize an array for the entry count.
 	 * @return array<int,int>
 	 */
-	protected function initEntryCountArray() {
+	protected function initEntryCountArray(): array {
 		return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
 	}
 
@@ -348,8 +348,8 @@ SQL;
 	 * @param array<string> $data
 	 * @return array<string>
 	 */
-	private function convertToTranslatedJson(array $data = array()) {
-		$translated = array_map(function($a) {
+	private function convertToTranslatedJson(array $data = array()): array {
+		$translated = array_map(static function (string $a) {
 			return _t('gen.date.' . $a);
 		}, $data);
 

+ 4 - 1
app/Models/StatsDAOPGSQL.php

@@ -5,7 +5,7 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 	/**
 	 * Calculates the number of article per hour of the day per feed
 	 *
-	 * @param integer $feed id
+	 * @param int $feed id
 	 * @return array<int,int>
 	 */
 	public function calculateEntryRepartitionPerFeedPerHour(?int $feed = null): array {
@@ -48,6 +48,9 @@ ORDER BY period ASC
 SQL;
 
 		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return [];
+		}
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 		switch ($period) {

+ 3 - 0
app/Models/StatsDAOSQLite.php

@@ -25,6 +25,9 @@ ORDER BY period ASC
 SQL;
 
 		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return [];
+		}
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 		switch ($period) {

+ 1 - 1
app/Models/Tag.php

@@ -97,7 +97,7 @@ class FreshRSS_Tag extends Minz_Model {
 	}
 
 	/**
-	 * @param string|int$value
+	 * @param string|int $value
 	 */
 	public function _nbUnread($value): void {
 		$this->nbUnread = (int)$value;

+ 2 - 2
app/Models/TagDAO.php

@@ -245,7 +245,7 @@ SQL;
 	}
 
 	/** @return array<string,string> */
-	public function listTagsNewestItemUsec(?int $id_tag = null) {
+	public function listTagsNewestItemUsec(?int $id_tag = null): array {
 		$sql = 'SELECT t.id AS id_tag, MAX(e.id) AS newest_item_us '
 			 . 'FROM `_tag` t '
 			 . 'LEFT OUTER JOIN `_entrytag` et ON et.id_tag = t.id '
@@ -440,7 +440,7 @@ SQL;
 	 * @param array<array<string,string|int>>|array<string,string|int> $listDAO
 	 * @return array<FreshRSS_Tag>
 	 */
-	private static function daoToTag(array $listDAO) {
+	private static function daoToTag(array $listDAO): array {
 		$list = array();
 		if (!is_array($listDAO)) {
 			$listDAO = array($listDAO);

+ 1 - 1
app/Models/TagDAOSQLite.php

@@ -10,7 +10,7 @@ class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
 	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
-			if (stripos($showCreate, 'tag') === false) {
+			if (is_string($showCreate) && stripos($showCreate, 'tag') === false) {
 				return $this->createTagTable();	//v1.12.0
 			}
 		}

+ 1 - 1
app/Models/UserQuery.php

@@ -34,7 +34,7 @@ class FreshRSS_UserQuery {
 	private $tag_dao;
 
 	/**
-	 * @param array<string,string> $query
+	 * @param array<string,string|int> $query
 	 */
 	public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
 		$this->category_dao = $category_dao;

+ 2 - 2
app/Models/View.php

@@ -57,7 +57,7 @@ class FreshRSS_View extends Minz_View {
 	public $show_email_field;
 	/** @var string */
 	public $username;
-	/** @var array<array{'last_user_activity':int, 'language':string,'enabled':bool,'is_admin':bool, 'enabled':bool, 'article_count':int, 'database_size':int, 'last_user_activity', 'mail_login':string, 'feed_count':int, 'is_default':bool}>  */
+	/** @var array<array{'last_user_activity':int,'language':string,'enabled':bool,'is_admin':bool,'enabled':bool,'article_count':int,'database_size':int,'last_user_activity','mail_login':string,'feed_count':int,'is_default':bool}> */
 	public $users;
 
 	// Updates
@@ -73,7 +73,7 @@ class FreshRSS_View extends Minz_View {
 	public $status_database;
 
 	// Archiving
-	/** @var int|false */
+	/** @var int */
 	public $nb_total;
 	/** @var int */
 	public $size_total;

+ 2 - 2
app/Services/ExportService.php

@@ -87,8 +87,8 @@ class FreshRSS_Export_Service {
 
 	/**
 	 * Generate the entries file content for the given feed.
-	 * @param integer $feed_id
-	 * @param integer $max_number_entries
+	 * @param int $feed_id
+	 * @param int $max_number_entries
 	 * @return array{0:string,1:string}|null First item is the filename, second item is the content.
 	 *                    It also can return null if the feed doesn’t exist.
 	 */

+ 16 - 23
app/Services/ImportService.php

@@ -15,10 +15,8 @@ class FreshRSS_Import_Service {
 
 	/**
 	 * Initialize the service for the given user.
-	 *
-	 * @param string $username
 	 */
-	public function __construct($username = null) {
+	public function __construct(?string $username = null) {
 		$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 	}
@@ -33,9 +31,9 @@ class FreshRSS_Import_Service {
 	 *
 	 * @param string $opml_file the OPML file content.
 	 * @param FreshRSS_Category|null $forced_category force the feeds to be associated to this category.
-	 * @param boolean $dry_run true to not create categories and feeds in database.
+	 * @param bool $dry_run true to not create categories and feeds in database.
 	 */
-	public function importOpml(string $opml_file, $forced_category = null, $dry_run = false) {
+	public function importOpml(string $opml_file, ?FreshRSS_Category $forced_category = null, bool $dry_run = false): void {
 		@set_time_limit(300);
 		$this->lastStatus = true;
 		$opml_array = array();
@@ -132,20 +130,17 @@ class FreshRSS_Import_Service {
 				}
 			}
 		}
-
-		return;
 	}
 
 	/**
 	 * Create a feed from a feed element (i.e. OPML outline).
 	 *
-	 * @param array<string, string> $feed_elt An OPML element (must be a feed element).
+	 * @param array<string,string> $feed_elt An OPML element (must be a feed element).
 	 * @param FreshRSS_Category $category The category to associate to the feed.
-	 * @param boolean $dry_run true to not create the feed in database.
-	 *
+	 * @param bool $dry_run true to not create the feed in database.
 	 * @return FreshRSS_Feed|null The created feed, or null if it failed.
 	 */
-	private function createFeed($feed_elt, $category, $dry_run) {
+	private function createFeed(array $feed_elt, FreshRSS_Category $category, bool $dry_run): ?FreshRSS_Feed {
 		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
 		$name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
@@ -256,12 +251,11 @@ class FreshRSS_Import_Service {
 	/**
 	 * Create and return a category.
 	 *
-	 * @param array<string, string> $category_element An OPML element (must be a category element).
-	 * @param boolean $dry_run true to not create the category in database.
-	 *
+	 * @param array<string,string> $category_element An OPML element (must be a category element).
+	 * @param bool $dry_run true to not create the category in database.
 	 * @return FreshRSS_Category|null The created category, or null if it failed.
 	 */
-	private function createCategory($category_element, $dry_run) {
+	private function createCategory(array $category_element, bool $dry_run): ?FreshRSS_Category {
 		$name = $category_element['text'] ?? $category_element['title'] ?? '';
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
 		$category = new FreshRSS_Category($name);
@@ -295,14 +289,13 @@ class FreshRSS_Import_Service {
 	 * This method is applied to a list of outlines. It merges the different
 	 * list of feeds from several outlines into one array.
 	 *
-	 * @param array $outlines
+	 * @param array<mixed> $outlines
 	 *     The outlines from which to extract the outlines.
 	 * @param string $parent_category_name
 	 *     The name of the parent category of the current outlines.
-	 *
-	 * @return array[]
+	 * @return array{0:array<mixed>,1:array<mixed>}
 	 */
-	private function loadFromOutlines($outlines, $parent_category_name) {
+	private function loadFromOutlines(array $outlines, string $parent_category_name): array {
 		$categories_elements = [];
 		$categories_to_feeds = [];
 
@@ -342,14 +335,14 @@ class FreshRSS_Import_Service {
 	 * exists), it will add the outline to an array accessible by its category
 	 * name.
 	 *
-	 * @param array $outline
+	 * @param array<mixed> $outline
 	 *     The outline from which to extract the categories and feeds outlines.
 	 * @param string $parent_category_name
 	 *     The name of the parent category of the current outline.
 	 *
-	 * @return array[]
+	 * @return array{0:array<string,mixed>,1:array<string,mixed>}
 	 */
-	private function loadFromOutline($outline, $parent_category_name) {
+	private function loadFromOutline($outline, $parent_category_name): array {
 		$categories_elements = [];
 		$categories_to_feeds = [];
 
@@ -396,7 +389,7 @@ class FreshRSS_Import_Service {
 		return [$categories_elements, $categories_to_feeds];
 	}
 
-	private static function log($message) {
+	private static function log(string $message): void {
 		if (FreshRSS_Context::$isCli) {
 			fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
 		} else {

+ 1 - 1
app/Utils/feverUtil.php

@@ -57,7 +57,7 @@ class FreshRSS_fever_Util {
 	 *
 	 * @return bool true if the deletion succeeded, else false.
 	 */
-	public static function deleteKey(string $username) {
+	public static function deleteKey(string $username): bool {
 		$userConfig = get_user_configuration($username);
 		if ($userConfig === null) {
 			return false;

+ 3 - 3
app/actualize_script.php

@@ -54,7 +54,7 @@ if (($handle = @fopen($mutexFile, 'x')) === false) {
 }
 fclose($handle);
 
-register_shutdown_function(function () use ($mutexFile) {
+register_shutdown_function(static function () use ($mutexFile) {
 	unlink($mutexFile);
 });
 // </Mutex>
@@ -63,7 +63,7 @@ notice('FreshRSS starting feeds actualization at ' . $begin_date->format('c'));
 
 // make sure the PHP setup of the CLI environment is compatible with FreshRSS as well
 echo 'Failed requirements!', "\n";
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 ob_clean();
 
 echo 'Results: ', "\n";	//Buffered
@@ -100,7 +100,7 @@ foreach ($users as $user) {
 	// NB: Extensions and hooks are reinitialised there
 	$app->init();
 
-	Minz_ExtensionManager::addHook('feed_before_actualize', function ($feed) use ($mutexFile) {
+	Minz_ExtensionManager::addHook('feed_before_actualize', static function (FreshRSS_Feed $feed) use ($mutexFile) {
 		touch($mutexFile);
 		return $feed;
 	});

+ 48 - 40
app/install.php

@@ -18,7 +18,11 @@ if (STEP === 2 && isset($_POST['type'])) {
 	Minz_Session::_param('bd_type', $_POST['type']);
 }
 
-function param($key, $default = false) {
+/**
+ * @param mixed $default
+ * @return mixed
+ */
+function param(string $key, $default = false) {
 	if (isset($_POST[$key])) {
 		return $_POST[$key];
 	} else {
@@ -27,7 +31,7 @@ function param($key, $default = false) {
 }
 
 // gestion internationalisation
-function initTranslate() {
+function initTranslate(): void {
 	Minz_Translate::init();
 	$available_languages = Minz_Translate::availableLanguages();
 
@@ -42,14 +46,14 @@ function initTranslate() {
 	Minz_Translate::reset(Minz_Session::param('language'));
 }
 
-function get_best_language() {
+function get_best_language(): string {
 	$accept = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE'];
 	return strtolower(substr($accept, 0, 2));
 }
 
 
 /*** SAUVEGARDES ***/
-function saveLanguage() {
+function saveLanguage(): bool {
 	if (!empty($_POST)) {
 		if (!isset($_POST['language'])) {
 			return false;
@@ -60,9 +64,10 @@ function saveLanguage() {
 
 		header('Location: index.php?step=1');
 	}
+	return true;
 }
 
-function saveStep1() {
+function saveStep1(): void {
 	if (isset($_POST['freshrss-keep-install']) &&
 			$_POST['freshrss-keep-install'] === '1') {
 		// We want to keep our previous installation of FreshRSS
@@ -79,12 +84,12 @@ function saveStep1() {
 				'auth_type' => FreshRSS_Context::$system_conf->auth_type,
 				'default_user' => Minz_User::name(),
 				'passwordHash' => FreshRSS_Context::$user_conf->passwordHash,
-				'bd_type' => FreshRSS_Context::$system_conf->db['type'],
-				'bd_host' => FreshRSS_Context::$system_conf->db['host'],
-				'bd_user' => FreshRSS_Context::$system_conf->db['user'],
-				'bd_password' => FreshRSS_Context::$system_conf->db['password'],
-				'bd_base' => FreshRSS_Context::$system_conf->db['base'],
-				'bd_prefix' => FreshRSS_Context::$system_conf->db['prefix'],
+				'bd_type' => FreshRSS_Context::$system_conf->db['type'] ?? '',
+				'bd_host' => FreshRSS_Context::$system_conf->db['host'] ?? '',
+				'bd_user' => FreshRSS_Context::$system_conf->db['user'] ?? '',
+				'bd_password' => FreshRSS_Context::$system_conf->db['password'] ?? '',
+				'bd_base' => FreshRSS_Context::$system_conf->db['base'] ?? '',
+				'bd_prefix' => FreshRSS_Context::$system_conf->db['prefix'] ?? '',
 				'bd_error' => false,
 			]);
 
@@ -92,7 +97,7 @@ function saveStep1() {
 	}
 }
 
-function saveStep2() {
+function saveStep2(): void {
 	if (!empty($_POST)) {
 		if (Minz_Session::param('bd_type') === 'sqlite') {
 			Minz_Session::_params([
@@ -190,9 +195,9 @@ function saveStep2() {
 	invalidateHttpCache();
 }
 
-function saveStep3() {
+function saveStep3(): bool {
 	if (!empty($_POST)) {
-		$system_default_config = Minz_Configuration::get('default_system');
+		$system_default_config = FreshRSS_SystemConfiguration::get('default_system');
 		Minz_Session::_params([
 				'title' => $system_default_config->title,
 				'auth_type' => param('auth_type', 'form'),
@@ -242,10 +247,11 @@ function saveStep3() {
 
 		header('Location: index.php?step=4');
 	}
+	return true;
 }
 
 /*** VÉRIFICATIONS ***/
-function checkStep() {
+function checkStep(): void {
 	$s0 = checkStep0();
 	$s1 = checkRequirements();
 	$s2 = checkStep2();
@@ -262,7 +268,8 @@ function checkStep() {
 	Minz_Session::_param('actualize_feeds', true);
 }
 
-function checkStep0() {
+/** @return array<string,string> */
+function checkStep0(): array {
 	$languages = Minz_Translate::availableLanguages();
 	$language = Minz_Session::param('language') != '' && in_array(Minz_Session::param('language'), $languages);
 	$sessionWorking = Minz_Session::param('sessionWorking') === 'ok';
@@ -274,7 +281,7 @@ function checkStep0() {
 	);
 }
 
-function freshrss_already_installed() {
+function freshrss_already_installed(): bool {
 	$conf_path = join_path(DATA_PATH, 'config.php');
 	if (!file_exists($conf_path)) {
 		return false;
@@ -300,7 +307,8 @@ function freshrss_already_installed() {
 	return true;
 }
 
-function checkStep2() {
+/** @return array<string,string> */
+function checkStep2(): array {
 	$conf = is_writable(join_path(DATA_PATH, 'config.php'));
 
 	$bd = Minz_Session::param('bd_type') != '';
@@ -314,7 +322,8 @@ function checkStep2() {
 	];
 }
 
-function checkStep3() {
+/** @return array<string,string> */
+function checkStep3(): array {
 	$conf = Minz_Session::param('default_user') != '';
 
 	$form = Minz_Session::param('auth_type') != '';
@@ -335,7 +344,7 @@ function checkStep3() {
 
 
 /*** AFFICHAGE ***/
-function printStep0() {
+function printStep0(): void {
 	$actual = Minz_Translate::language();
 	$languages = Minz_Translate::availableLanguages();
 	$s0 = checkStep0();
@@ -373,7 +382,8 @@ function printStep0() {
 <?php
 }
 
-function printStep1Template($key, $value, $messageParams = []) {
+/** @param array<string> $messageParams */
+function printStep1Template(string $key, string $value, array $messageParams = []): void {
 	if ('ok' === $value) {
 		$message = _t("install.check.{$key}.ok", ...$messageParams);
 		?><p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= $message ?></p><?php
@@ -383,10 +393,12 @@ function printStep1Template($key, $value, $messageParams = []) {
 	}
 }
 
-function getProcessUsername() {
+function getProcessUsername(): string {
 	if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) {
-		$processUser = posix_getpwuid(posix_geteuid());
-		return $processUser['name'];
+		$processUser = posix_getpwuid(posix_geteuid()) ?: [];
+		if (!empty($processUser['name'])) {
+			return $processUser['name'];
+		}
 	}
 
 	if (function_exists('exec')) {
@@ -400,7 +412,7 @@ function getProcessUsername() {
 }
 
 // @todo refactor this view with the check_install action
-function printStep1() {
+function printStep1(): void {
 	$res = checkRequirements();
 	$processUsername = getProcessUsername();
 ?>
@@ -408,14 +420,10 @@ function printStep1() {
 	<noscript><p class="alert alert-warn"><span class="alert-head"><?= _t('gen.short.attention') ?></span> <?= _t('install.javascript_is_better') ?></p></noscript>
 
 	<?php
-	if (function_exists('curl_version')) {
-		$version = curl_version();
-	} else {
-		$version['version'] = '';
-	}
+	$version = function_exists('curl_version') ? curl_version() : [];
 	printStep1Template('php', $res['php'], [PHP_VERSION, FRESHRSS_MIN_PHP_VERSION]);
 	printStep1Template('pdo', $res['pdo']);
-	printStep1Template('curl', $res['curl'], [$version['version']]);
+	printStep1Template('curl', $res['curl'], [$version['version'] ?? '']);
 	printStep1Template('json', $res['json']);
 	printStep1Template('pcre', $res['pcre']);
 	printStep1Template('ctype', $res['ctype']);
@@ -466,8 +474,8 @@ function printStep1() {
 <?php
 }
 
-function printStep2() {
-	$system_default_config = Minz_Configuration::get('default_system');
+function printStep2(): void {
+	$system_default_config = FreshRSS_SystemConfiguration::get('default_system');
 	$s2 = checkStep2();
 	if ($s2['all'] == 'ok') { ?>
 	<p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= _t('install.bdd.conf.ok') ?></p>
@@ -509,7 +517,7 @@ function printStep2() {
 			<label class="group-name" for="host"><?= _t('install.bdd.host') ?></label>
 			<div class="group-controls">
 				<input type="text" id="host" name="host" pattern="[0-9A-Z/a-z_.-]{1,64}(:[0-9]{2,5})?" value="<?=
-					isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host'] ?>" tabindex="2" />
+					isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : ($system_default_config->db['host'] ?? '') ?>" tabindex="2" />
 			</div>
 		</div>
 
@@ -544,7 +552,7 @@ function printStep2() {
 			<label class="group-name" for="prefix"><?= _t('install.bdd.prefix') ?></label>
 			<div class="group-controls">
 				<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?=
-					isset($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] : $system_default_config->db['prefix'] ?>" tabindex="7" />
+					isset($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] : ($system_default_config->db['prefix'] ?? '') ?>" tabindex="7" />
 			</div>
 		</div>
 		</div>
@@ -562,11 +570,11 @@ function printStep2() {
 <?php
 }
 
-function no_auth($auth_type) {
+function no_auth(string $auth_type): bool {
 	return !in_array($auth_type, array('form', 'http_auth', 'none'));
 }
 
-function printStep3() {
+function printStep3(): void {
 	$auth_type = isset($_SESSION['auth_type']) ? $_SESSION['auth_type'] : '';
 	$s3 = checkStep3();
 	if ($s3['all'] == 'ok') { ?>
@@ -628,7 +636,7 @@ function printStep3() {
 <?php
 }
 
-function printStep4() {
+function printStep4(): void {
 ?>
 	<p class="alert alert-success"><span class="alert-head"><?= _t('install.congratulations') ?></span> <?= _t('install.ok') ?></p>
 	<div class="form-group form-actions">
@@ -639,7 +647,7 @@ function printStep4() {
 <?php
 }
 
-function printStep5() {
+function printStep5(): void {
 ?>
 	<p class="alert alert-error">
 		<span class="alert-head"><?= _t('gen.short.damn') ?></span>
@@ -676,7 +684,7 @@ case 5:
 }
 ?>
 <!DOCTYPE html>
-<html<?php
+<html <?php
 if (_t('gen.dir') === 'rtl') {
 	echo ' dir="rtl" class="rtl"';
 }

+ 1 - 1
app/views/helpers/export/articles.phtml

@@ -11,7 +11,7 @@ $articles = array(
 	'items' => array(),
 );
 
-echo rtrim(json_encode($articles, $options), " ]}\n\r\t"), "\n";
+echo rtrim(json_encode($articles, $options) ?: '', " ]}\n\r\t"), "\n";
 $first = true;
 
 if (empty($this->entryIdsTagNames)) {

+ 4 - 2
app/views/helpers/export/opml.phtml

@@ -4,7 +4,7 @@
  * @param array<FreshRSS_Feed> $feeds
  * @return array<array<string,string|null>>
  */
-function feedsToOutlines($feeds, bool $excludeMutedFeeds = false): array {
+function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
 	$outlines = [];
 	foreach ($feeds as $feed) {
 		if ($feed->mute() && $excludeMutedFeeds) {
@@ -98,4 +98,6 @@ if (!empty($this->feeds)) {
 }
 
 $libopml = new \marienfressinaud\LibOpml\LibOpml(true);
-echo $libopml->render($opml_array);
+$opml = $libopml->render($opml_array);
+/** @var string $opml */
+echo $opml;

+ 1 - 1
app/views/helpers/javascript_vars.phtml

@@ -72,4 +72,4 @@ echo htmlspecialchars(json_encode(array(
 		'unread' => rawurlencode(_i('unread')),
 	),
 	'extensions' => $extData,
-), JSON_UNESCAPED_UNICODE), ENT_NOQUOTES, 'UTF-8');
+), JSON_UNESCAPED_UNICODE) ?: '', ENT_NOQUOTES, 'UTF-8');

+ 2 - 2
app/views/index/logs.phtml

@@ -14,7 +14,7 @@
 	?>
 
 	<?php if (!empty($items)) { ?>
-	<?php $this->logsPaginator->render('logs_pagination.phtml', 0); ?>
+	<?php $this->logsPaginator->render('logs_pagination.phtml'); ?>
 	<div id="loglist-wrapper" class="table-wrapper">
 		<table id="loglist">
 			<thead>
@@ -41,7 +41,7 @@
 		</tbody>
 		</table>
 	</div>
-	<?php $this->logsPaginator->render('logs_pagination.phtml', 0); ?>
+	<?php $this->logsPaginator->render('logs_pagination.phtml'); ?>
 
 
 

+ 1 - 1
app/views/index/normal.phtml

@@ -40,7 +40,7 @@ $today = @strtotime('today');
 		// We most likely already have the feed object in cache
 		$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId());
 		if ($this->feed == null) {
-			$this->feed = $this->entry->feed();
+			$this->feed = $this->entry->feed() ?: null;
 			if ($this->feed == null) {
 				$this->feed = FreshRSS_Feed::example();
 			}

+ 1 - 1
app/views/stats/idle.phtml

@@ -45,7 +45,7 @@
 					<li class="item feed<?= $error_class, $empty_class, $mute_class ?>" title="<?= $error_title, $empty_title ?>">
 						<a class="configure open-slider" href="<?= _url('stats', 'feed', 'id', $feedInPeriod['id'], 'sub', 'idle') ?>" title="<?= _t('gen.action.manage') ?>"><?= _i('configure') ?></a>
 						<?php if (FreshRSS_Context::$user_conf->show_favicons): ?><img class="favicon" src="<?= $feedInPeriod['favicon'] ?>" alt="✇" loading="lazy" /><?php endif; ?>
-						<span title="<?= timestamptodate($feedInPeriod['last_date'], false) ?>"><?= $feedInPeriod['name'] ?>
+						<span title="<?= timestamptodate((int)($feedInPeriod['last_date']), false) ?>"><?= $feedInPeriod['name'] ?>
 							(<?= _t('admin.stats.number_entries', $feedInPeriod['nb_articles']) ?>)</span>
 					</li>
 				<?php } ?>

+ 2 - 2
cli/_cli.php

@@ -77,10 +77,10 @@ function performRequirementCheck(string $databaseType): void {
  * @return array<string>
  */
 function getLongOptions(array $options, string $regex): array {
-	$longOptions = array_filter($options, function($a) use ($regex) {
+	$longOptions = array_filter($options, static function (string $a) use ($regex) {
 		return preg_match($regex, $a);
 	});
-	return array_map(function($a) use ($regex) {
+	return array_map(static function (string $a) use ($regex) {
 		return preg_replace($regex, '', $a);
 	}, $longOptions);
 }

+ 1 - 1
cli/_update-or-create-user.php

@@ -1,7 +1,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 		'user:',

+ 2 - 2
cli/actualize-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -10,7 +10,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
 

+ 2 - 2
cli/db-optimize.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -10,7 +10,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
 

+ 2 - 2
cli/delete-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -10,7 +10,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
 $username = $options['user'];

+ 2 - 2
cli/export-opml-for-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -10,7 +10,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
 }
 

+ 2 - 2
cli/export-sqlite-for-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = [
 	'user:',
@@ -11,7 +11,7 @@ $params = [
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
 	fail('Usage: ' . basename(__FILE__) . ' --user username --filename /path/to/db.sqlite');
 }
 

+ 2 - 2
cli/export-zip-for-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -11,7 +11,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
 }
 

+ 2 - 2
cli/i18n/I18nData.php

@@ -127,7 +127,7 @@ class I18nData {
 	 * the parent key is 'a.b.c.d'.
 	 */
 	private function getParentKey(string $key): string {
-		return substr($key, 0, strrpos($key, '.'));
+		return substr($key, 0, strrpos($key, '.') ?: null);
 	}
 
 	/**
@@ -183,7 +183,7 @@ class I18nData {
 		}
 
 		$keys = array_keys($this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)]);
-		$children = array_values(array_filter($keys, function ($element) use ($key) {
+		$children = array_values(array_filter($keys, static function (string $element) use ($key) {
 			if ($element === $key) {
 				return false;
 			}

+ 2 - 2
cli/import-for-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = array(
 	'user:',
@@ -11,7 +11,7 @@ $params = array(
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
 }
 

+ 2 - 2
cli/import-sqlite-for-user.php

@@ -2,7 +2,7 @@
 <?php
 require(__DIR__ . '/_cli.php');
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 $params = [
 	'user:',
@@ -12,7 +12,7 @@ $params = [
 
 $options = getopt('', $params);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename'])) {
+if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
 	fail('Usage: ' . basename(__FILE__) . ' --user username --force-overwrite --filename /path/to/db.sqlite');
 }
 

+ 6 - 6
cli/user-info.php

@@ -71,12 +71,12 @@ foreach ($users as $username) {
 		'enabled' => FreshRSS_Context::$user_conf->enabled ? '*' : '',
 		'last_user_activity' => FreshRSS_UserDAO::mtime($username),
 		'database_size' => $databaseDAO->size(),
-		'categories' => (int) $catDAO->count(),
-		'feeds' => (int) count($feedDAO->listFeedsIds()),
-		'reads' => (int) $nbEntries['read'],
-		'unreads' => (int) $nbEntries['unread'],
-		'favourites' => (int) $nbFavorites['all'],
-		'tags' => (int) $tagDAO->count(),
+		'categories' => $catDAO->count(),
+		'feeds' => count($feedDAO->listFeedsIds()),
+		'reads' => (int)$nbEntries['read'],
+		'unreads' => (int)$nbEntries['unread'],
+		'favourites' => (int)$nbFavorites['all'],
+		'tags' => (int)$tagDAO->count(),
 		'lang' => FreshRSS_Context::$user_conf->language,
 		'mail_login' => FreshRSS_Context::$user_conf->mail_login,
 	);

+ 8 - 2
composer.json

@@ -49,7 +49,8 @@
         "ext-phar": "*",
         "ext-tokenizer": "*",
         "ext-xmlwriter": "*",
-        "phpstan/phpstan": "~1.9.17",
+        "phpstan/phpstan": "~1.10.13",
+        "phpstan/phpstan-phpunit": "^1.3",
         "phpunit/phpunit": "^9",
         "squizlabs/php_codesniffer": "^3.7"
     },
@@ -59,7 +60,7 @@
         "phpcs": "phpcs . -s",
         "phpcbf": "phpcbf . -p -s",
         "phpstan": "phpstan analyse --memory-limit 512M .",
-        "phpstan-next": "phpstan analyse --level 6 --memory-limit 512M $(find . -type d -name 'vendor' -prune -o -name '*.php' -o -name '*.phtml' | grep -Fxvf ./tests/phpstan-next.txt | sort | paste -s)",
+        "phpstan-next": "phpstan analyse --level 7 --memory-limit 512M $(find . -type d -name 'vendor' -prune -o -name '*.php' -o -name '*.phtml' | grep -Fxvf ./tests/phpstan-next.txt | sort | paste -s)",
         "phpunit": "phpunit --bootstrap ./tests/bootstrap.php --verbose ./tests",
         "translations": "cli/manipulate.translation.php -a format",
         "test": [
@@ -74,5 +75,10 @@
             "@translations",
             "@phpcbf"
         ]
+    },
+    "config": {
+        "allow-plugins": {
+            "phpstan/extension-installer": false
+        }
     }
 }

+ 136 - 77
composer.lock

@@ -4,35 +4,35 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "4dcbcd3ba9c1dbed63612651a725dab8",
+    "content-hash": "194c10da954d3f120fef1e0b21c34546",
     "packages": [],
     "packages-dev": [
         {
             "name": "doctrine/instantiator",
-            "version": "1.5.0",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+                "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
-                "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+                "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1 || ^8.0"
+                "php": "^8.1"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^9 || ^11",
+                "doctrine/coding-standard": "^11",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpbench/phpbench": "^0.16 || ^1",
-                "phpstan/phpstan": "^1.4",
-                "phpstan/phpstan-phpunit": "^1",
-                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
-                "vimeo/psalm": "^4.30 || ^5.4"
+                "phpbench/phpbench": "^1.2",
+                "phpstan/phpstan": "^1.9.4",
+                "phpstan/phpstan-phpunit": "^1.3",
+                "phpunit/phpunit": "^9.5.27",
+                "vimeo/psalm": "^5.4"
             },
             "type": "library",
             "autoload": {
@@ -59,7 +59,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/instantiator/issues",
-                "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+                "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
             },
             "funding": [
                 {
@@ -75,20 +75,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-30T00:15:36+00:00"
+            "time": "2022-12-30T00:23:10+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.11.0",
+            "version": "1.11.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
+                "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
-                "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+                "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
                 "shasum": ""
             },
             "require": {
@@ -126,7 +126,7 @@
             ],
             "support": {
                 "issues": "https://github.com/myclabs/DeepCopy/issues",
-                "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
             },
             "funding": [
                 {
@@ -134,20 +134,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-03-03T13:19:32+00:00"
+            "time": "2023-03-08T13:26:56+00:00"
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.15.2",
+            "version": "v4.15.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
+                "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
-                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
+                "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
                 "shasum": ""
             },
             "require": {
@@ -188,9 +188,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4"
             },
-            "time": "2022-11-12T15:38:23+00:00"
+            "time": "2023-03-05T19:49:14+00:00"
         },
         {
             "name": "phar-io/manifest",
@@ -305,16 +305,16 @@
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.9.17",
+            "version": "1.10.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2"
+                "reference": "f07bf8c6980b81bf9e49d44bd0caf2e737614a70"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/204e459e7822f2c586463029f5ecec31bb45a1f2",
-                "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f07bf8c6980b81bf9e49d44bd0caf2e737614a70",
+                "reference": "f07bf8c6980b81bf9e49d44bd0caf2e737614a70",
                 "shasum": ""
             },
             "require": {
@@ -343,8 +343,11 @@
                 "static analysis"
             ],
             "support": {
+                "docs": "https://phpstan.org/user-guide/getting-started",
+                "forum": "https://github.com/phpstan/phpstan/discussions",
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/1.9.17"
+                "security": "https://github.com/phpstan/phpstan/security/policy",
+                "source": "https://github.com/phpstan/phpstan-src"
             },
             "funding": [
                 {
@@ -360,27 +363,79 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-02-08T12:25:00+00:00"
+            "time": "2023-04-12T19:29:52+00:00"
+        },
+        {
+            "name": "phpstan/phpstan-phpunit",
+            "version": "1.3.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpstan/phpstan-phpunit.git",
+                "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c",
+                "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "phpstan/phpstan": "^1.10"
+            },
+            "conflict": {
+                "phpunit/phpunit": "<7.0"
+            },
+            "require-dev": {
+                "nikic/php-parser": "^4.13.0",
+                "php-parallel-lint/php-parallel-lint": "^1.2",
+                "phpstan/phpstan-strict-rules": "^1.0",
+                "phpunit/phpunit": "^9.5"
+            },
+            "type": "phpstan-extension",
+            "extra": {
+                "phpstan": {
+                    "includes": [
+                        "extension.neon",
+                        "rules.neon"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PHPStan\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "PHPUnit extensions and rules for PHPStan",
+            "support": {
+                "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
+                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.11"
+            },
+            "time": "2023-03-25T19:42:13+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.23",
+            "version": "9.2.26",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c"
+                "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
-                "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
+                "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
                 "ext-xmlwriter": "*",
-                "nikic/php-parser": "^4.14",
+                "nikic/php-parser": "^4.15",
                 "php": ">=7.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-text-template": "^2.0.2",
@@ -395,8 +450,8 @@
                 "phpunit/phpunit": "^9.3"
             },
             "suggest": {
-                "ext-pcov": "*",
-                "ext-xdebug": "*"
+                "ext-pcov": "PHP extension that provides line coverage",
+                "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
             },
             "type": "library",
             "extra": {
@@ -429,7 +484,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26"
             },
             "funding": [
                 {
@@ -437,7 +492,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2022-12-28T12:41:10+00:00"
+            "time": "2023-03-06T12:58:08+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
@@ -682,20 +737,20 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.5.27",
+            "version": "9.6.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38"
+                "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38",
-                "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2",
+                "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.3.1",
+                "doctrine/instantiator": "^1.3.1 || ^2",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
@@ -724,8 +779,8 @@
                 "sebastian/version": "^3.0.2"
             },
             "suggest": {
-                "ext-soap": "*",
-                "ext-xdebug": "*"
+                "ext-soap": "To be able to generate mocks based on WSDL files",
+                "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
             },
             "bin": [
                 "phpunit"
@@ -733,7 +788,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "9.5-dev"
+                    "dev-master": "9.6-dev"
                 }
             },
             "autoload": {
@@ -764,7 +819,8 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27"
+                "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7"
             },
             "funding": [
                 {
@@ -780,7 +836,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-09T07:31:23+00:00"
+            "time": "2023-04-14T08:58:40+00:00"
         },
         {
             "name": "sebastian/cli-parser",
@@ -1148,16 +1204,16 @@
         },
         {
             "name": "sebastian/environment",
-            "version": "5.1.4",
+            "version": "5.1.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
+                "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
-                "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+                "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
                 "shasum": ""
             },
             "require": {
@@ -1199,7 +1255,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/environment/issues",
-                "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
+                "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
             },
             "funding": [
                 {
@@ -1207,7 +1263,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2022-04-03T09:37:03+00:00"
+            "time": "2023-02-03T06:03:51+00:00"
         },
         {
             "name": "sebastian/exporter",
@@ -1521,16 +1577,16 @@
         },
         {
             "name": "sebastian/recursion-context",
-            "version": "4.0.4",
+            "version": "4.0.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+                "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
-                "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+                "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
                 "shasum": ""
             },
             "require": {
@@ -1569,10 +1625,10 @@
                 }
             ],
             "description": "Provides functionality to recursively process PHP variables",
-            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "homepage": "https://github.com/sebastianbergmann/recursion-context",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
-                "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
             },
             "funding": [
                 {
@@ -1580,7 +1636,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-10-26T13:17:30+00:00"
+            "time": "2023-02-03T06:07:39+00:00"
         },
         {
             "name": "sebastian/resource-operations",
@@ -1639,16 +1695,16 @@
         },
         {
             "name": "sebastian/type",
-            "version": "3.2.0",
+            "version": "3.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/type.git",
-                "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e"
+                "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
-                "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+                "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
                 "shasum": ""
             },
             "require": {
@@ -1683,7 +1739,7 @@
             "homepage": "https://github.com/sebastianbergmann/type",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/type/issues",
-                "source": "https://github.com/sebastianbergmann/type/tree/3.2.0"
+                "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
             },
             "funding": [
                 {
@@ -1691,7 +1747,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2022-09-12T14:47:03+00:00"
+            "time": "2023-02-03T06:13:03+00:00"
         },
         {
             "name": "sebastian/version",
@@ -1748,16 +1804,16 @@
         },
         {
             "name": "squizlabs/php_codesniffer",
-            "version": "3.7.1",
+            "version": "3.7.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
+                "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
-                "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
+                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
+                "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
                 "shasum": ""
             },
             "require": {
@@ -1793,14 +1849,15 @@
             "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
             "keywords": [
                 "phpcs",
-                "standards"
+                "standards",
+                "static analysis"
             ],
             "support": {
                 "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
                 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
                 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
             },
-            "time": "2022-06-18T07:21:10+00:00"
+            "time": "2023-02-22T23:07:41+00:00"
         },
         {
             "name": "theseer/tokenizer",
@@ -1867,9 +1924,11 @@
         "ext-gmp": "*",
         "ext-intl": "*",
         "ext-json": "*",
+        "ext-libxml": "*",
         "ext-mbstring": "*",
         "ext-openssl": "*",
         "ext-pcre": "*",
+        "ext-pdo": "*",
         "ext-pdo_sqlite": "*",
         "ext-session": "*",
         "ext-simplexml": "*",
@@ -1885,5 +1944,5 @@
         "ext-tokenizer": "*",
         "ext-xmlwriter": "*"
     },
-    "plugin-api-version": "2.3.0"
+    "plugin-api-version": "2.2.0"
 }

+ 15 - 13
lib/Minz/Configuration.php

@@ -3,7 +3,8 @@
 /**
  * Manage configuration for the application.
  * @property-read string $base_url
- * @property array<string|array<int,string>> $db
+ * @property array{'type'?:string,'host'?:string,'user'?:string,'password'?:string,'base'?:string,'prefix'?:string,
+ *  'connection_uri_params'?:string,'pdo_options'?:array<string|int,string|int|bool>} $db
  * @property-read string $disable_update
  * @property-read string $environment
  * @property array<string,bool> $extensions_enabled
@@ -24,9 +25,10 @@ class Minz_Configuration {
 	 * @param string $namespace the name of the current configuration
 	 * @param string $config_filename the filename of the configuration
 	 * @param string $default_filename a filename containing default values for the configuration
-	 * @param object $configuration_setter an optional helper to set values in configuration
+	 * @param Minz_ConfigurationSetterInterface $configuration_setter an optional helper to set values in configuration
 	 */
-	public static function register(string $namespace, string $config_filename, string $default_filename = null, object $configuration_setter = null): void {
+	public static function register(string $namespace, string $config_filename, string $default_filename = null,
+		Minz_ConfigurationSetterInterface $configuration_setter = null): void {
 		self::$config_list[$namespace] = new static(
 			$namespace, $config_filename, $default_filename, $configuration_setter
 		);
@@ -92,9 +94,9 @@ class Minz_Configuration {
 
 	/**
 	 * An object which help to set good values in configuration.
-	 * @var object|null
+	 * @var Minz_ConfigurationSetterInterface|null
 	 */
-	private $configuration_setter = null;
+	private $configuration_setter;
 
 	/**
 	 * Create a new Minz_Configuration object.
@@ -102,9 +104,10 @@ class Minz_Configuration {
 	 * @param string $namespace the name of the current configuration.
 	 * @param string $config_filename the file containing configuration values.
 	 * @param string $default_filename the file containing default values, null by default.
-	 * @param object $configuration_setter an optional helper to set values in configuration
+	 * @param Minz_ConfigurationSetterInterface $configuration_setter an optional helper to set values in configuration
 	 */
-	private final function __construct(string $namespace, string $config_filename, string $default_filename = null, object $configuration_setter = null) {
+	private final function __construct(string $namespace, string $config_filename, string $default_filename = null,
+		Minz_ConfigurationSetterInterface $configuration_setter = null) {
 		$this->namespace = $namespace;
 		$this->config_filename = $config_filename;
 		$this->default_filename = $default_filename;
@@ -127,16 +130,15 @@ class Minz_Configuration {
 
 	/**
 	 * Set a configuration setter for the current configuration.
-	 * @param object|null $configuration_setter the setter to call when modifying data. It
-	 *        must implement an handle($key, $value) method.
+	 * @param Minz_ConfigurationSetterInterface|null $configuration_setter the setter to call when modifying data.
 	 */
-	public function _configurationSetter(?object $configuration_setter): void {
+	public function _configurationSetter(?Minz_ConfigurationSetterInterface $configuration_setter): void {
 		if (is_callable(array($configuration_setter, 'handle'))) {
 			$this->configuration_setter = $configuration_setter;
 		}
 	}
 
-	public function configurationSetter(): object {
+	public function configurationSetter(): ?Minz_ConfigurationSetterInterface {
 		return $this->configuration_setter;
 	}
 
@@ -181,11 +183,11 @@ class Minz_Configuration {
 	 * @param mixed $value the value to set. If null, the key is removed from the configuration.
 	 */
 	public function _param(string $key, $value = null): void {
-		if (!is_null($this->configuration_setter) && $this->configuration_setter->support($key)) {
+		if ($this->configuration_setter !== null && $this->configuration_setter->support($key)) {
 			$this->configuration_setter->handle($this->data, $key, $value);
 		} elseif (isset($this->data[$key]) && is_null($value)) {
 			unset($this->data[$key]);
-		} elseif (!is_null($value)) {
+		} elseif ($value !== null) {
 			$this->data[$key] = $value;
 		}
 	}

+ 19 - 0
lib/Minz/ConfigurationSetterInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+interface Minz_ConfigurationSetterInterface {
+
+	/**
+	 * Return whether the given key is supported by this setter.
+	 * @param string $key the key to test.
+	 * @return bool true if the key is supported, false otherwise.
+	 */
+	public function support(string $key): bool;
+
+	/**
+	 * Set the given key in data with the current value.
+	 * @param array<string,mixed> $data an array containing the list of all configuration data.
+	 * @param string $key the key to update.
+	 * @param mixed $value the value to set.
+	 */
+	public function handle(&$data, string $key, $value): void;
+}

+ 11 - 14
lib/Minz/Dispatcher.php

@@ -87,19 +87,21 @@ class Minz_Dispatcher {
 			$controller_name = 'FreshRSS_' . $base_name . '_Controller';
 		}
 
-		if (!class_exists ($controller_name)) {
+		if (!class_exists($controller_name)) {
 			throw new Minz_ControllerNotExistException (
 				Minz_Exception::ERROR
 			);
 		}
-		$this->controller = new $controller_name ();
+		$controller = new $controller_name();
 
-		if (! ($this->controller instanceof Minz_ActionController)) {
+		if (!($controller instanceof Minz_ActionController)) {
 			throw new Minz_ControllerNotActionControllerException (
 				$controller_name,
 				Minz_Exception::ERROR
 			);
 		}
+
+		$this->controller = $controller;
 	}
 
 	/**
@@ -108,20 +110,15 @@ class Minz_Dispatcher {
 	 * @throws Minz_ActionException if the action cannot be executed on the controller
 	 */
 	private function launchAction(string $action_name): void {
-		if (!is_callable (array (
-			$this->controller,
-			$action_name
-		))) {
+		$call = [$this->controller, $action_name];
+		if (!is_callable($call)) {
 			throw new Minz_ActionException (
-				get_class ($this->controller),
+				get_class($this->controller),
 				$action_name,
 				Minz_Exception::ERROR
 			);
 		}
-		call_user_func (array (
-			$this->controller,
-			$action_name
-		));
+		call_user_func($call);
 	}
 
 	/**
@@ -140,9 +137,9 @@ class Minz_Dispatcher {
 	 * Return if a controller is registered.
 	 *
 	 * @param string $base_name the base name of the controller.
-	 * @return boolean true if the controller has been registered, false else.
+	 * @return bool true if the controller has been registered, false else.
 	 */
-	public static function isRegistered(string $base_name) {
+	public static function isRegistered(string $base_name): bool {
 		return isset(self::$registrations[$base_name]);
 	}
 

+ 1 - 1
lib/Minz/Error.php

@@ -52,7 +52,7 @@ class Minz_Error {
 	 * @param array<string,string>|string $logs logs sorted by category (error, warning, notice)
 	 * @return array<string> list of matching logs, without the category, according to environment preferences (production / development)
 	 */
-	private static function processLogs($logs) {
+	private static function processLogs($logs): array {
 		$conf = Minz_Configuration::get('system');
 		$env = $conf->environment;
 		$logs_ok = array ();

+ 4 - 4
lib/Minz/Extension.php

@@ -217,7 +217,7 @@ abstract class Minz_Extension {
 	}
 
 	/** @param 'system'|'user' $type */
-	private function isConfigurationEnabled($type): bool {
+	private function isConfigurationEnabled(string $type): bool {
 		if (!class_exists('FreshRSS_Context', false)) {
 			return false;
 		}
@@ -229,7 +229,7 @@ abstract class Minz_Extension {
 	}
 
 	/** @param 'system'|'user' $type */
-	private function isExtensionConfigured($type): bool {
+	private function isExtensionConfigured(string $type): bool {
 		switch ($type) {
 			case 'system':
 				$conf = FreshRSS_Context::$user_conf;
@@ -248,7 +248,7 @@ abstract class Minz_Extension {
 	}
 
 	/**
-	 * @param 'system'|'user' $type
+	 * @phpstan-param 'system'|'user' $type
 	 * @return array<string,mixed>
 	 */
 	private function getConfiguration(string $type): array {
@@ -338,7 +338,7 @@ abstract class Minz_Extension {
 		$this->user_configuration = $configuration;
 	}
 
-	/** @param 'system'|'user' $type */
+	/** @phpstan-param 'system'|'user' $type */
 	private function removeConfiguration(string $type): void {
 		if (!$this->isConfigurationEnabled($type)) {
 			return;

+ 1 - 1
lib/Minz/ExtensionManager.php

@@ -364,7 +364,7 @@ final class Minz_ExtensionManager {
 		foreach (self::$hook_list[$hook_name]['list'] as $function) {
 			$result = call_user_func($function, $arg);
 
-			if (is_null($result)) {
+			if ($result === null) {
 				break;
 			}
 

+ 3 - 3
lib/Minz/FrontController.php

@@ -39,11 +39,11 @@ class Minz_FrontController {
 			Minz_Request::init();
 
 			$url = Minz_Url::build();
-			$url['params'] = array_merge (
-				$url['params'],
+			$url['params'] = array_merge(
+				empty($url['params']) || !is_array($url['params']) ? [] : $url['params'],
 				$_POST
 			);
-			Minz_Request::forward ($url);
+			Minz_Request::forward($url);
 		} catch (Minz_Exception $e) {
 			Minz_Log::error($e->getMessage());
 			self::killApp($e->getMessage());

+ 2 - 3
lib/Minz/Mailer.php

@@ -44,7 +44,7 @@ class Minz_Mailer {
 	 */
 	public function __construct () {
 		$this->view = new Minz_View();
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		$this->view->attributeParams();
 
 		$conf = Minz_Configuration::get('system');
@@ -66,10 +66,9 @@ class Minz_Mailer {
 	 *
 	 * @param string $to The recipient of the email
 	 * @param string $subject The subject of the email
-	 *
 	 * @return bool true on success, false if a SMTP error happens
 	 */
-	public function mail($to, $subject) {
+	public function mail(string $to, string $subject): bool {
 		ob_start();
 		$this->view->render();
 		$body = ob_get_contents();

+ 4 - 5
lib/Minz/Migrator.php

@@ -38,11 +38,11 @@ class Minz_Migrator
 		$applied_migrations = array_filter(explode("\n", $applied_migrations));
 
 		$migration_files = scandir($migrations_path);
-		$migration_files = array_filter($migration_files, function ($filename) {
+		$migration_files = array_filter($migration_files, static function (string $filename) {
 			$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
 			return $file_extension === 'php';
 		});
-		$migration_versions = array_map(function ($filename) {
+		$migration_versions = array_map(static function (string $filename) {
 			return basename($filename, '.php');
 		}, $migration_files);
 
@@ -225,9 +225,8 @@ class Minz_Migrator
 	}
 
 	/**
-	 * @return boolean Return true if the application is up-to-date, false
-	 *                 otherwise. If no migrations are registered, it always
-	 *                 returns true.
+	 * @return bool Return true if the application is up-to-date, false otherwise.
+	 * If no migrations are registered, it always returns true.
 	 */
 	public function upToDate(): bool {
 		// Counting versions is enough since we cannot apply a version which

+ 1 - 1
lib/Minz/ModelArray.php

@@ -19,7 +19,7 @@ class Minz_ModelArray {
 	 * @param string $filename le nom du fichier à ouvrir contenant un tableau
 	 * Remarque : $array sera obligatoirement un tableau
 	 */
-	public function __construct ($filename) {
+	public function __construct(string $filename) {
 		$this->filename = $filename;
 	}
 

+ 1 - 1
lib/Minz/ModelPdo.php

@@ -102,7 +102,7 @@ class Minz_ModelPdo {
 	 * @throws Minz_ConfigurationNamespaceException
 	 * @throws Minz_PDOConnectionException
 	 */
-	public function __construct($currentUser = null, $currentPdo = null) {
+	public function __construct(?string $currentUser = null, ?Minz_Pdo $currentPdo = null) {
 		if ($currentUser === null) {
 			$currentUser = Minz_User::name();
 		}

+ 28 - 30
lib/Minz/Paginator.php

@@ -37,11 +37,11 @@ class Minz_Paginator {
 	 * Constructeur
 	 * @param array<Minz_Model> $items les éléments à gérer
 	 */
-	public function __construct ($items) {
-		$this->_items ($items);
-		$this->_nbItems (count ($this->items (true)));
-		$this->_nbItemsPerPage ($this->nbItemsPerPage);
-		$this->_currentPage ($this->currentPage);
+	public function __construct(array $items) {
+		$this->_items($items);
+		$this->_nbItems(count($this->items(true)));
+		$this->_nbItemsPerPage($this->nbItemsPerPage);
+		$this->_currentPage($this->currentPage);
 	}
 
 	/**
@@ -49,25 +49,25 @@ class Minz_Paginator {
 	 * @param string $view nom du fichier de vue situé dans /app/views/helpers/
 	 * @param int $getteur variable de type $_GET[] permettant de retrouver la page
 	 */
-	public function render ($view, $getteur) {
+	public function render(string $view, int $getteur = 0): void {
 		$view = APP_PATH . '/views/helpers/' . $view;
 
-		if (file_exists ($view)) {
-			include ($view);
+		if (file_exists($view)) {
+			include($view);
 		}
 	}
 
 	/**
 	 * Permet de retrouver la page d'un élément donné
 	 * @param Minz_Model $item l'élément à retrouver
-	 * @return float|false la page à laquelle se trouve l’élément, false si non trouvé
+	 * @return int|false la page à laquelle se trouve l’élément, false si non trouvé
 	 */
-	public function pageByItem ($item) {
+	public function pageByItem($item) {
 		$i = 0;
 
 		do {
 			if ($item == $this->items[$i]) {
-				return ceil(($i + 1) / $this->nbItemsPerPage);
+				return (int)(ceil(($i + 1) / $this->nbItemsPerPage));
 			}
 			$i++;
 		} while ($i < $this->nbItems());
@@ -80,7 +80,7 @@ class Minz_Paginator {
 	 * @param Minz_Model $item the element to search
 	 * @return int|false the position of the element, or false if not found
 	 */
-	public function positionByItem ($item) {
+	public function positionByItem($item) {
 		$i = 0;
 
 		do {
@@ -96,9 +96,9 @@ class Minz_Paginator {
 	/**
 	 * Permet de récupérer un item par sa position
 	 * @param int $pos la position de l'élément
-	 * @return mixed item situé à $pos (dernier item si $pos<0, 1er si $pos>=count($items))
+	 * @return Minz_Model item situé à $pos (dernier item si $pos<0, 1er si $pos>=count($items))
 	 */
-	public function itemByPosition ($pos) {
+	public function itemByPosition(int $pos): Minz_Model {
 		if ($pos < 0) {
 			$pos = $this->nbItems () - 1;
 		}
@@ -116,7 +116,7 @@ class Minz_Paginator {
 	 * @param bool $all si à true, retourne tous les éléments sans prendre en compte la pagination
 	 * @return array<Minz_Model>
 	 */
-	public function items ($all = false) {
+	public function items(bool $all = false): array {
 		$array = array ();
 		$nbItems = $this->nbItems ();
 
@@ -141,30 +141,28 @@ class Minz_Paginator {
 
 		return $array;
 	}
-	public function nbItemsPerPage () {
+	public function nbItemsPerPage(): int {
 		return $this->nbItemsPerPage;
 	}
-	public function currentPage () {
+	public function currentPage(): int {
 		return $this->currentPage;
 	}
-	public function nbPage () {
+	public function nbPage(): int {
 		return $this->nbPage;
 	}
-	public function nbItems () {
+	public function nbItems(): int {
 		return $this->nbItems;
 	}
 
 	/**
 	 * SETTEURS
 	 */
-	public function _items ($items) {
-		if (is_array ($items)) {
-			$this->items = $items;
-		}
-
-		$this->_nbPage ();
+	/** @param array<Minz_Model> $items */
+	public function _items(?array $items): void {
+		$this->items = $items ?? [];
+		$this->_nbPage();
 	}
-	public function _nbItemsPerPage ($nbItemsPerPage) {
+	public function _nbItemsPerPage(int $nbItemsPerPage): void {
 		if ($nbItemsPerPage > $this->nbItems ()) {
 			$nbItemsPerPage = $this->nbItems ();
 		}
@@ -173,21 +171,21 @@ class Minz_Paginator {
 		}
 
 		$this->nbItemsPerPage = $nbItemsPerPage;
-		$this->_nbPage ();
+		$this->_nbPage();
 	}
-	public function _currentPage ($page) {
+	public function _currentPage(int $page): void {
 		if ($page < 1 || ($page > $this->nbPage && $this->nbPage > 0)) {
 			throw new Minz_CurrentPagePaginationException($page);
 		}
 
 		$this->currentPage = $page;
 	}
-	private function _nbPage () {
+	private function _nbPage(): void {
 		if ($this->nbItemsPerPage > 0) {
 			$this->nbPage = (int)ceil($this->nbItems() / $this->nbItemsPerPage);
 		}
 	}
-	public function _nbItems ($value) {
+	public function _nbItems(int $value): void {
 		$this->nbItems = $value;
 	}
 }

+ 2 - 2
lib/Minz/Request.php

@@ -298,7 +298,7 @@ class Minz_Request {
 	 * localhost address.
 	 *
 	 * @param string $address the address to test, can be an IP or a URL.
-	 * @return boolean true if server is accessible, false otherwise.
+	 * @return bool true if server is accessible, false otherwise.
 	 * @todo improve test with a more valid technique (e.g. test with an external server?)
 	 */
 	public static function serverIsPublic(string $address): bool {
@@ -360,7 +360,7 @@ class Minz_Request {
 		$requests = Minz_Session::param('requests');
 		if ($requests) {
 			//Delete abandoned notifications
-			$requests = array_filter($requests, function ($r) { return isset($r['time']) && $r['time'] > time() - 3600; });
+			$requests = array_filter($requests, static function (array $r) { return isset($r['time']) && $r['time'] > time() - 3600; });
 
 			$requestId = self::requestId();
 			if (!empty($requests[$requestId]['notification'])) {

+ 3 - 4
lib/Minz/Translate.php

@@ -133,8 +133,8 @@ class Minz_Translate {
 		}
 
 		$list_i18n_files = array_values(array_diff(
-			scandir($lang_path),
-			array('..', '.')
+			scandir($lang_path) ?: [],
+			['..', '.']
 		));
 
 		// Each file basename correspond to a top-level i18n key. For each of
@@ -199,8 +199,7 @@ class Minz_Translate {
 
 		// If $translates[$top_level] is null it means we have to load the
 		// corresponding files.
-		if (!isset(self::$translates[$top_level]) ||
-				is_null(self::$translates[$top_level])) {
+		if (empty(self::$translates[$top_level])) {
 			$res = self::loadKey($top_level);
 			if (!$res) {
 				return $key;

+ 13 - 18
lib/Minz/Url.php

@@ -60,7 +60,7 @@ class Minz_Url {
 	 * @param string $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @return string uri sous la forme ?key=value&key2=value2
 	 */
-	private static function printUri($url, string $encodage): string {
+	private static function printUri(array $url, string $encodage): string {
 		$uri = '';
 		$separator = '?';
 		$anchor = '';
@@ -108,23 +108,15 @@ class Minz_Url {
 
 	/**
 	 * Check that all array elements representing the controller URL are OK
-	 * @param array<string,array<string,string>> $url controller URL as array
+	 * @param array<string,string|array<string,mixed>> $url controller URL as array
 	 * @return array{'c':string,'a':string,'params':array<string,mixed>} Verified controller URL as array
 	 */
-	public static function checkControllerUrl(array $url) {
-		$url_checked = $url;
-
-		if (empty($url['c'])) {
-			$url_checked['c'] = Minz_Request::defaultControllerName();
-		}
-		if (empty($url['a'])) {
-			$url_checked['a'] = Minz_Request::defaultActionName();
-		}
-		if (empty($url['params'])) {
-			$url_checked['params'] = [];
-		}
-
-		return $url_checked;
+	public static function checkControllerUrl(array $url): array {
+		return [
+			'c' => empty($url['c']) || !is_string($url['c']) ? Minz_Request::defaultControllerName() : $url['c'],
+			'a' => empty($url['a']) || !is_string($url['a']) ? Minz_Request::defaultActionName() : $url['a'],
+			'params' => empty($url['params']) || !is_array($url['params']) ? [] : $url['params'],
+		];
 	}
 
 	/** @param array<string,string|array<string,string>>|null $url */
@@ -139,7 +131,10 @@ class Minz_Url {
 		}
 	}
 
-	/** @return array<string,string|array<string,string>> */
+	/**
+	 * @phpstan-return array{'c'?:string,'a'?:string,'params'?:array<string,mixed>}
+	 * @return array<string,string|array<string,string>>
+	 */
 	public static function unserialize(string $url = ''): array {
 		try {
 			return json_decode(base64_decode($url), true, JSON_THROW_ON_ERROR) ?? [];
@@ -150,7 +145,7 @@ class Minz_Url {
 
 	/**
 	 * Returns an array representing the URL as passed in the address bar
-	 * @return array<string,string|array<string,string>> URL representation
+	 * @return array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} URL representation
 	 */
 	public static function build(): array {
 		$url = [

+ 5 - 6
lib/Minz/View.php

@@ -22,7 +22,7 @@ class Minz_View {
 	private static $title = '';
 	/** @var array<array{'media':string,'url':string}> */
 	private static $styles = [];
-	/** @var array<array{'url':string,'id':string,'defer':string,'async':string}> */
+	/** @var array<array{'url':string,'id':string,'defer':bool,'async':bool}> */
 	private static $scripts = [];
 	/** @var string|array{'dark'?:string,'light'?:string,'default'?:string} */
 	private static $themeColors;
@@ -83,7 +83,7 @@ class Minz_View {
 	 * The file is searched inside list of $base_pathnames.
 	 *
 	 * @param string $filename the name of the file to include.
-	 * @return boolean true if the file has been included, false else.
+	 * @return bool true if the file has been included, false else.
 	 */
 	private function includeFile(string $filename): bool {
 		// We search the filename in the list of base pathnames. Only the first view
@@ -158,9 +158,9 @@ class Minz_View {
 
 	/**
 	 * Choose the current view layout.
-	 * @param string|false $layout the layout name to use, false to use no layouts.
+	 * @param string|null $layout the layout name to use, false to use no layouts.
 	 */
-	public function _layout($layout): void {
+	public function _layout(?string $layout): void {
 		if ($layout) {
 			$this->layout_filename = self::LAYOUT_PATH_NAME . $layout . '.phtml';
 		} else {
@@ -178,7 +178,7 @@ class Minz_View {
 		if ($use) {
 			$this->_layout(self::LAYOUT_DEFAULT);
 		} else {
-			$this->_layout(false);
+			$this->_layout(null);
 		}
 	}
 
@@ -326,7 +326,6 @@ class Minz_View {
 
 	/**
 	 * Management of parameters added to the view
-	 * @param string $key
 	 * @param mixed $value
 	 */
 	public static function _param(string $key, $value): void {

+ 2 - 2
lib/lib_date.php

@@ -34,7 +34,7 @@ example('PT6M/');
 example('PT7S/');
 example('P1DT1H/');
 
-function example($dateInterval) {
+function example(string $dateInterval) {
 	$dateIntervalArray = parseDateInterval($dateInterval);
 	echo $dateInterval, "\t=>\t",
 		$dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
@@ -84,7 +84,7 @@ function _dateRelative(?string $d1, ?string $d2): ?string {
  * @return array{int|null|false,int|null|false} an array with the minimum and maximum Unix timestamp of this interval,
  *  or null if open interval, or false if error.
  */
-function parseDateInterval(string $dateInterval) {
+function parseDateInterval(string $dateInterval): array {
 	$dateInterval = trim($dateInterval);
 	$dateInterval = str_replace('--', '/', $dateInterval);
 	$dateInterval = strtoupper($dateInterval);

+ 8 - 4
lib/lib_install.php

@@ -1,7 +1,7 @@
 <?php
 
-Minz_Configuration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php'));
-Minz_Configuration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php'));
+FreshRSS_SystemConfiguration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php'));
+FreshRSS_UserConfiguration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php'));
 
 /** @return array<string,string> */
 function checkRequirements(string $dbType = ''): array {
@@ -76,7 +76,7 @@ function checkRequirements(string $dbType = ''): array {
 }
 
 function generateSalt(): string {
-	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__)));
+	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__) ?: []));
 }
 
 function initDb(): string {
@@ -88,10 +88,14 @@ function initDb(): string {
 	$db['pdo_options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
 	$conf->db = $db;	//TODO: Remove this Minz limitation "Indirect modification of overloaded property"
 
+	if (empty($db['type'])) {
+		$db['type'] = 'sqlite';
+	}
+
 	//Attempt to auto-create database if it does not already exist
 	if ($db['type'] !== 'sqlite') {
 		Minz_ModelPdo::$usesSharedPdo = false;
-		$dbBase = isset($db['base']) ? $db['base'] : '';
+		$dbBase = $db['base'] ?? '';
 		//For first connection, use default database for PostgreSQL, empty database for MySQL / MariaDB:
 		$db['base'] = $db['type'] === 'pgsql' ? 'postgres' : '';
 		$conf->db = $db;

+ 14 - 27
lib/lib_rss.php

@@ -132,11 +132,7 @@ function checkUrl(string $url, bool $fixScheme = true) {
 	}
 }
 
-/**
- * @param string $text
- * @return string
- */
-function safe_ascii($text) {
+function safe_ascii(string $text): string {
 	return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?: '';
 }
 
@@ -154,12 +150,7 @@ if (function_exists('mb_convert_encoding')) {
 	}
 }
 
-/**
- * @param string $text
- * @param bool $extended
- * @return string
- */
-function escapeToUnicodeAlternative($text, $extended = true) {
+function escapeToUnicodeAlternative(string $text, bool $extended = true): string {
 	$text = htmlspecialchars_decode($text, ENT_QUOTES);
 
 	//Problematic characters
@@ -176,7 +167,8 @@ function escapeToUnicodeAlternative($text, $extended = true) {
 	return trim(str_replace($problem, $replace, $text));
 }
 
-function format_number(float $n, int $precision = 0): string {
+/** @param int|float $n */
+function format_number($n, int $precision = 0): string {
 	// number_format does not seem to be Unicode-compatible
 	return str_replace(' ', ' ',	// Thin non-breaking space
 		number_format($n, $precision, '.', ' ')
@@ -255,7 +247,7 @@ function sensitive_log($log) {
 /**
  * @param array<string,mixed> $attributes
  */
-function customSimplePie($attributes = array()): SimplePie {
+function customSimplePie(array $attributes = array()): SimplePie {
 	if (FreshRSS_Context::$system_conf === null) {
 		throw new FreshRSS_Context_Exception('System configuration not initialised!');
 	}
@@ -339,10 +331,8 @@ function customSimplePie($attributes = array()): SimplePie {
 	return $simplePie;
 }
 
-/**
- * @param string $data
- */
-function sanitizeHTML($data, string $base = '', ?int $maxLength = null): string {
+/** @param string $data */
+function sanitizeHTML(string $data, string $base = '', ?int $maxLength = null): string {
 	if (!is_string($data) || ($maxLength !== null && $maxLength <= 0)) {
 		return '';
 	}
@@ -579,7 +569,7 @@ function listUsers(): array {
  * Return if the maximum number of registrations has been reached.
  * Note a max_registrations of 0 means there is no limit.
  *
- * @return boolean true if number of users >= max registrations, false else.
+ * @return bool true if number of users >= max registrations, false else.
  */
 function max_registrations_reached(): bool {
 	if (FreshRSS_Context::$system_conf === null) {
@@ -601,13 +591,13 @@ function max_registrations_reached(): bool {
  * @param string $username the name of the user of which we want the configuration.
  * @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded.
  */
-function get_user_configuration(string $username) {
+function get_user_configuration(string $username): ?FreshRSS_UserConfiguration {
 	if (!FreshRSS_user_Controller::checkUsername($username)) {
 		return null;
 	}
 	$namespace = 'user_' . $username;
 	try {
-		Minz_Configuration::register($namespace,
+		FreshRSS_UserConfiguration::register($namespace,
 			USERS_PATH . '/' . $username . '/config.php',
 			FRESHRSS_PATH . '/config-user.default.php');
 	} catch (Minz_ConfigurationNamespaceException $e) {
@@ -618,10 +608,7 @@ function get_user_configuration(string $username) {
 		return null;
 	}
 
-	/**
-	 * @var FreshRSS_UserConfiguration $user_conf
-	 */
-	$user_conf = Minz_Configuration::get($namespace);
+	$user_conf = FreshRSS_UserConfiguration::get($namespace);
 	return $user_conf;
 }
 
@@ -644,7 +631,7 @@ function ipToBits(string $ip): string {
  *
  * @param string $ip the IP that we want to verify (ex: 192.168.16.1)
  * @param string $range the range to check against (ex: 192.168.16.0/24)
- * @return boolean true if the IP is in the range, otherwise false
+ * @return bool true if the IP is in the range, otherwise false
  */
 function checkCIDR(string $ip, string $range): bool {
 	$binary_ip = ipToBits($ip);
@@ -663,7 +650,7 @@ function checkCIDR(string $ip, string $range): bool {
  * This uses the REMOTE_ADDR header to determine the sender's IP
  * and the configuration option "trusted_sources" to get an array of the authorized ranges
  *
- * @return boolean, true if the sender's IP is in one of the ranges defined in the configuration, else false
+ * @return bool, true if the sender's IP is in one of the ranges defined in the configuration, else false
  */
 function checkTrustedIP(): bool {
 	if (FreshRSS_Context::$system_conf === null) {
@@ -840,7 +827,7 @@ const SHORTCUT_KEYS = [
 function getNonStandardShortcuts(array $shortcuts): array {
 	$standard = strtolower(implode(' ', SHORTCUT_KEYS));
 
-	$nonStandard = array_filter($shortcuts, function ($shortcut) use ($standard) {
+	$nonStandard = array_filter($shortcuts, static function (string $shortcut) use ($standard) {
 		$shortcut = trim($shortcut);
 		return $shortcut !== '' & stripos($standard, $shortcut) === false;
 	});

+ 7 - 8
p/api/fever.php

@@ -336,9 +336,8 @@ final class FeverAPI
 		$groups = array();
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
-		$categories = $categoryDAO->listCategories(false, false);
+		$categories = $categoryDAO->listCategories(false, false) ?: [];
 
-		/** @var FreshRSS_Category $category */
 		foreach ($categories as $category) {
 			$groups[] = array(
 				'id' => $category->id(),
@@ -430,28 +429,28 @@ final class FeverAPI
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setItemAsRead(string $id) {
 		return $this->entryDAO->markRead($id, true);
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setItemAsUnread(string $id) {
 		return $this->entryDAO->markRead($id, false);
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setItemAsSaved(string $id) {
 		return $this->entryDAO->markFavorite($id, true);
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setItemAsUnsaved(string $id) {
 		return $this->entryDAO->markFavorite($id, false);
@@ -540,7 +539,7 @@ final class FeverAPI
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setFeedAsRead(int $id, int $before) {
 		$before = $this->convertBeforeToId($before);
@@ -548,7 +547,7 @@ final class FeverAPI
 	}
 
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	private function setGroupAsRead(int $id, int $before) {
 		$before = $this->convertBeforeToId($before);

+ 3 - 3
p/f.php

@@ -5,7 +5,7 @@ require(LIB_PATH . '/favicons.php');
 require(LIB_PATH . '/http-conditional.php');
 
 function show_default_favicon(int $cacheSeconds = 3600): void {
-	$default_mtime = @filemtime(DEFAULT_FAVICON);
+	$default_mtime = @filemtime(DEFAULT_FAVICON) ?: 0;
 	if (!httpConditional($default_mtime, $cacheSeconds, 2)) {
 		header('Content-Type: image/x-icon');
 		header('Content-Disposition: inline; filename="default_favicon.ico"');
@@ -21,8 +21,8 @@ if (!ctype_xdigit($id)) {
 $txt = FAVICONS_DIR . $id . '.txt';
 $ico = FAVICONS_DIR . $id . '.ico';
 
-$ico_mtime = @filemtime($ico);
-$txt_mtime = @filemtime($txt);
+$ico_mtime = @filemtime($ico) ?: 0;
+$txt_mtime = @filemtime($txt) ?: 0;
 
 if ($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (mt_rand(15, 20) * 86400))) {
 	if ($txt_mtime == false) {

+ 2 - 2
p/i/index.php

@@ -35,8 +35,8 @@ if (!file_exists($applied_migrations_path)) {
 		require(LIB_PATH . '/http-conditional.php');
 		$currentUser = Minz_User::name();
 		$dateLastModification = $currentUser === null ? time() : max(
-			@filemtime(USERS_PATH . '/' . $currentUser . '/' . LOG_FILENAME),
-			@filemtime(DATA_PATH . '/config.php')
+			@filemtime(USERS_PATH . '/' . $currentUser . '/' . LOG_FILENAME) ?: 0,
+			@filemtime(DATA_PATH . '/config.php') ?: 0
 		);
 		if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) {
 			Minz_Session::init('FreshRSS');

+ 7 - 4
phpstan.neon

@@ -1,6 +1,7 @@
 parameters:
 	# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
-	level: 5
+	level: 6
+	treatPhpDocTypesAsCertain: false
 	fileExtensions:
 		- php
 		- phtml
@@ -9,14 +10,16 @@ parameters:
 	excludePaths:
 		analyse:
 			- lib/marienfressinaud/*
+			- lib/phpgt/*
 			- lib/phpmailer/*
 			- lib/SimplePie/*
+			- vendor/*
 		analyseAndScan:
 			- .git/*
 			- node_modules/*
-			# TODO: include tests
-			- tests/*
-			- vendor/*
 	bootstrapFiles:
 		- cli/_cli.php
 		- lib/favicons.php
+includes:
+	- vendor/phpstan/phpstan-phpunit/extension.neon
+	- vendor/phpstan/phpstan-phpunit/rules.neon

+ 5 - 6
tests/app/Models/CategoryTest.php

@@ -2,23 +2,22 @@
 
 class CategoryTest extends PHPUnit\Framework\TestCase {
 
-	public function test__construct_whenNoParameters_createsObjectWithDefaultValues() {
+	public function test__construct_whenNoParameters_createsObjectWithDefaultValues(): void {
 		$category = new FreshRSS_Category();
 		$this->assertEquals(0, $category->id());
 		$this->assertEquals('', $category->name());
 	}
 
 	/**
-	 * @param string $input
-	 * @param string $expected
 	 * @dataProvider provideValidNames
 	 */
-	public function test_name_whenValidValue_storesModifiedValue($input, $expected) {
+	public function test_name_whenValidValue_storesModifiedValue(string $input, string $expected): void {
 		$category = new FreshRSS_Category($input);
 		$this->assertEquals($expected, $category->name());
 	}
 
-	public function provideValidNames() {
+	/** @return array<array{string,string}> */
+	public function provideValidNames(): array {
 		return array(
 			array('', ''),
 			array('this string does not need trimming', 'this string does not need trimming'),
@@ -30,7 +29,7 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
 		);
 	}
 
-	public function test_feedOrdering() {
+	public function test_feedOrdering(): void {
 		$feed_1 = $this->getMockBuilder(FreshRSS_Feed::class)
 			->disableOriginalConstructor()
 			->getMock();

+ 1 - 1
tests/app/Models/LogDAOTest.php

@@ -36,7 +36,7 @@ class LogDAOTest extends TestCase {
 
 		$this->logDAO::truncate(self::LOG_FILE_TEST);
 
-		$this->assertStringContainsString('', file_get_contents($this->logPath));
+		$this->assertStringContainsString('', file_get_contents($this->logPath) ?: '');
 	}
 
 	protected function tearDown(): void {

+ 52 - 65
tests/app/Models/SearchTest.php

@@ -6,9 +6,8 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideEmptyInput
-	 * @param string|null $input
 	 */
-	public function test__construct_whenInputIsEmpty_getsOnlyNullValues($input) {
+	public function test__construct_whenInputIsEmpty_getsOnlyNullValues(?string $input): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals('', $search->getRawInput());
 		$this->assertNull($search->getIntitle());
@@ -24,9 +23,9 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 	/**
 	 * Return an array of values for the search object.
 	 * Here is the description of the values
-	 * @return array
+	 * @return array{array{''},array{null}}
 	 */
-	public function provideEmptyInput() {
+	public function provideEmptyInput(): array {
 		return array(
 			array(''),
 			array(null),
@@ -35,20 +34,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideIntitleSearch
-	 * @param string $input
-	 * @param string $intitle_value
-	 * @param string|null $search_value
+	 * @param array<string>|null $intitle_value
+	 * @param array<string>|null $search_value
 	 */
-	public function test__construct_whenInputContainsIntitle_setsIntitleProperty($input, $intitle_value, $search_value) {
+	public function test__construct_whenInputContainsIntitle_setsIntitleProperty(string $input, ?array $intitle_value, ?array $search_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($intitle_value, $search->getIntitle());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
-	public function provideIntitleSearch() {
+	public function provideIntitleSearch(): array {
 		return array(
 			array('intitle:word1', array('word1'), null),
 			array('intitle:word1-word2', array('word1-word2'), null),
@@ -73,20 +71,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideAuthorSearch
-	 * @param string $input
-	 * @param string $author_value
-	 * @param string|null $search_value
+	 * @param array<string>|null $author_value
+	 * @param array<string>|null $search_value
 	 */
-	public function test__construct_whenInputContainsAuthor_setsAuthorValue($input, $author_value, $search_value) {
+	public function test__construct_whenInputContainsAuthor_setsAuthorValue(string $input, ?array $author_value, ?array $search_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
-	public function provideAuthorSearch() {
+	public function provideAuthorSearch(): array {
 		return array(
 			array('author:word1', array('word1'), null),
 			array('author:word1-word2', array('word1-word2'), null),
@@ -111,20 +108,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideInurlSearch
-	 * @param string $input
-	 * @param string $inurl_value
-	 * @param string|null $search_value
+	 * @param array<string>|null $inurl_value
+	 * @param array<string>|null $search_value
 	 */
-	public function test__construct_whenInputContainsInurl_setsInurlValue($input, $inurl_value, $search_value) {
+	public function test__construct_whenInputContainsInurl_setsInurlValue(string $input, ?array $inurl_value, ?array $search_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($inurl_value, $search->getInurl());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
-	public function provideInurlSearch() {
+	public function provideInurlSearch(): array {
 		return array(
 			array('inurl:word1', array('word1'), null),
 			array('inurl: word1', array(), array('word1')),
@@ -139,72 +135,65 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideDateSearch
-	 * @param string $input
-	 * @param string $min_date_value
-	 * @param string $max_date_value
 	 */
-	public function test__construct_whenInputContainsDate_setsDateValues($input, $min_date_value, $max_date_value) {
+	public function test__construct_whenInputContainsDate_setsDateValues(string $input, ?int $min_date_value, ?int $max_date_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($min_date_value, $search->getMinDate());
 		$this->assertEquals($max_date_value, $search->getMaxDate());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
-	public function provideDateSearch() {
+	public function provideDateSearch(): array {
 		return array(
-			array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
-			array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210519799'),
-			array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172754001', '1210519800'),
+			array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', 1172754000, 1210519800),
+			array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', 1172754000, 1210519799),
+			array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', 1172754001, 1210519800),
 			array('date:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1),
-			array('date:2007-03-01/', strtotime('2007-03-01'), ''),
-			array('date:/2008-05-11', '', strtotime('2008-05-12') - 1),
+			array('date:2007-03-01/', strtotime('2007-03-01'), null),
+			array('date:/2008-05-11', null, strtotime('2008-05-12') - 1),
 		);
 	}
 
 	/**
 	 * @dataProvider providePubdateSearch
-	 * @param string $input
-	 * @param string $min_pubdate_value
-	 * @param string $max_pubdate_value
 	 */
-	public function test__construct_whenInputContainsPubdate_setsPubdateValues($input, $min_pubdate_value, $max_pubdate_value) {
+	public function test__construct_whenInputContainsPubdate_setsPubdateValues(string $input, ?int $min_pubdate_value, ?int $max_pubdate_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($min_pubdate_value, $search->getMinPubdate());
 		$this->assertEquals($max_pubdate_value, $search->getMaxPubdate());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
-	public function providePubdateSearch() {
+	public function providePubdateSearch(): array {
 		return array(
-			array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
-			array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210519799'),
-			array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172754001', '1210519800'),
+			array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', 1172754000, 1210519800),
+			array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', 1172754000, 1210519799),
+			array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', 1172754001, 1210519800),
 			array('pubdate:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1),
-			array('pubdate:2007-03-01/', strtotime('2007-03-01'), ''),
-			array('pubdate:/2008-05-11', '', strtotime('2008-05-12') - 1),
+			array('pubdate:2007-03-01/', strtotime('2007-03-01'), null),
+			array('pubdate:/2008-05-11', null, strtotime('2008-05-12') - 1),
 		);
 	}
 
 	/**
 	 * @dataProvider provideTagsSearch
-	 * @param string $input
-	 * @param string $tags_value
-	 * @param string|null $search_value
+	 * @param array<string>|null $tags_value
+	 * @param array<string>|null $search_value
 	 */
-	public function test__construct_whenInputContainsTags_setsTagsValue($input, $tags_value, $search_value) {
+	public function test__construct_whenInputContainsTags_setsTagsValue(string $input, ?array $tags_value, ?array $search_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($tags_value, $search->getTags());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 
 	/**
-	 * @return array
+	 * @return array<array<string|array<string>|null>>
 	 */
-	public function provideTagsSearch() {
+	public function provideTagsSearch(): array {
 		return array(
 			array('#word1', array('word1'), null),
 			array('# word1', array(), array('#', 'word1')),
@@ -219,19 +208,15 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 	/**
 	 * @dataProvider provideMultipleSearch
-	 * @param string $input
-	 * @param string $author_value
-	 * @param string $min_date_value
-	 * @param string $max_date_value
-	 * @param string $intitle_value
-	 * @param string $inurl_value
-	 * @param string $min_pubdate_value
-	 * @param string $max_pubdate_value
-	 * @param array $tags_value
-	 * @param string|null $search_value
+	 * @param array<string>|null $author_value
+	 * @param array<string> $intitle_value
+	 * @param array<string>|null $inurl_value
+	 * @param array<string>|null $tags_value
+	 * @param array<string>|null $search_value
 	 */
-	public function test__construct_whenInputContainsMultipleKeywords_setsValues($input, $author_value, $min_date_value,
-			$max_date_value, $intitle_value, $inurl_value, $min_pubdate_value, $max_pubdate_value, $tags_value, $search_value) {
+	public function test__construct_whenInputContainsMultipleKeywords_setsValues(string $input, ?array $author_value, ?int $min_date_value,
+			?int $max_date_value, ?array $intitle_value, ?array $inurl_value, ?int $min_pubdate_value,
+			?int $max_pubdate_value, ?array $tags_value, ?array $search_value): void {
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($min_date_value, $search->getMinDate());
@@ -245,7 +230,8 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($input, $search->getRawInput());
 	}
 
-	public function provideMultipleSearch() {
+	/** @return array<array<mixed>> */
+	public function provideMultipleSearch(): array {
 		return array(
 			array(
 				'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5',
@@ -302,13 +288,14 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 	 * @dataProvider provideParentheses
 	 * @param array<string> $values
 	 */
-	public function test__construct_parentheses(string $input, string $sql, $values) {
+	public function test__construct_parentheses(string $input, string $sql, array $values): void {
 		list($filterValues, $filterSearch) = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
 		$this->assertEquals($sql, $filterSearch);
 		$this->assertEquals($values, $filterValues);
 	}
 
-	public function provideParentheses() {
+	/** @return array<array<mixed>> */
+	public function provideParentheses(): array {
 		return [
 			[
 				'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))',

+ 37 - 27
tests/app/Models/UserQueryTest.php

@@ -5,21 +5,21 @@
  */
 class UserQueryTest extends PHPUnit\Framework\TestCase {
 
-	public function test__construct_whenAllQuery_storesAllParameters() {
+	public function test__construct_whenAllQuery_storesAllParameters(): void {
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals('all', $user_query->getGetName());
 		$this->assertEquals('all', $user_query->getGetType());
 	}
 
-	public function test__construct_whenFavoriteQuery_storesFavoriteParameters() {
+	public function test__construct_whenFavoriteQuery_storesFavoriteParameters(): void {
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals('favorite', $user_query->getGetName());
 		$this->assertEquals('favorite', $user_query->getGetType());
 	}
 
-	public function test__construct_whenCategoryQueryAndNoDao_throwsException() {
+	public function test__construct_whenCategoryQueryAndNoDao_throwsException(): void {
 		$this->expectException(FreshRSS_DAO_Exception::class);
 		$this->expectExceptionMessage('Category DAO is not loaded in UserQuery');
 
@@ -27,13 +27,15 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		new FreshRSS_UserQuery($query);
 	}
 
-	public function test__construct_whenCategoryQuery_storesCategoryParameters() {
+	public function test__construct_whenCategoryQuery_storesCategoryParameters(): void {
 		$category_name = 'some category name';
+		/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
 		$cat = $this->createMock('FreshRSS_Category');
 		$cat->expects($this->atLeastOnce())
 			->method('name')
 			->withAnyParameters()
 			->willReturn($category_name);
+		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -45,7 +47,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('category', $user_query->getGetType());
 	}
 
-	public function test__construct_whenFeedQueryAndNoDao_throwsException() {
+	public function test__construct_whenFeedQueryAndNoDao_throwsException(): void {
 		$this->expectException(FreshRSS_DAO_Exception::class);
 		$this->expectExceptionMessage('Feed DAO is not loaded in UserQuery');
 
@@ -53,13 +55,15 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		new FreshRSS_UserQuery($query);
 	}
 
-	public function test__construct_whenFeedQuery_storesFeedParameters() {
+	public function test__construct_whenFeedQuery_storesFeedParameters(): void {
 		$feed_name = 'some feed name';
-		$feed = $this->createMock('FreshRSS_Feed', array(), array('', false));
+		/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
+		$feed = $this->createMock('FreshRSS_Feed');
 		$feed->expects($this->atLeastOnce())
 			->method('name')
 			->withAnyParameters()
 			->willReturn($feed_name);
+		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
 		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -71,48 +75,48 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('feed', $user_query->getGetType());
 	}
 
-	public function test__construct_whenUnknownQuery_doesStoreParameters() {
+	public function test__construct_whenUnknownQuery_doesStoreParameters(): void {
 		$query = array('get' => 'q');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEmpty($user_query->getGetName());
 		$this->assertEmpty($user_query->getGetType());
 	}
 
-	public function test__construct_whenName_storesName() {
+	public function test__construct_whenName_storesName(): void {
 		$name = 'some name';
 		$query = array('name' => $name);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($name, $user_query->getName());
 	}
 
-	public function test__construct_whenOrder_storesOrder() {
+	public function test__construct_whenOrder_storesOrder(): void {
 		$order = 'some order';
 		$query = array('order' => $order);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($order, $user_query->getOrder());
 	}
 
-	public function test__construct_whenState_storesState() {
+	public function test__construct_whenState_storesState(): void {
 		$state = FreshRSS_Entry::STATE_ALL;
 		$query = array('state' => $state);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($state, $user_query->getState());
 	}
 
-	public function test__construct_whenUrl_storesUrl() {
+	public function test__construct_whenUrl_storesUrl(): void {
 		$url = 'some url';
 		$query = array('url' => $url);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($url, $user_query->getUrl());
 	}
 
-	public function testToArray_whenNoData_returnsEmptyArray() {
+	public function testToArray_whenNoData_returnsEmptyArray(): void {
 		$user_query = new FreshRSS_UserQuery(array());
 		$this->assertIsIterable($user_query->toArray());
 		$this->assertCount(0, $user_query->toArray());
 	}
 
-	public function testToArray_whenData_returnsArray() {
+	public function testToArray_whenData_returnsArray(): void {
 		$query = array(
 			'get' => 's',
 			'name' => 'some name',
@@ -127,7 +131,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($query, $user_query->toArray());
 	}
 
-	public function testHasSearch_whenSearch_returnsTrue() {
+	public function testHasSearch_whenSearch_returnsTrue(): void {
 		$query = array(
 			'search' => 'some search',
 		);
@@ -135,31 +139,33 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->hasSearch());
 	}
 
-	public function testHasSearch_whenNoSearch_returnsFalse() {
+	public function testHasSearch_whenNoSearch_returnsFalse(): void {
 		$user_query = new FreshRSS_UserQuery(array());
 		$this->assertFalse($user_query->hasSearch());
 	}
 
-	public function testHasParameters_whenAllQuery_returnsFalse() {
+	public function testHasParameters_whenAllQuery_returnsFalse(): void {
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->hasParameters());
 	}
 
-	public function testHasParameters_whenNoParameter_returnsFalse() {
+	public function testHasParameters_whenNoParameter_returnsFalse(): void {
 		$query = array();
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->hasParameters());
 	}
 
-	public function testHasParameters_whenParameter_returnTrue() {
+	public function testHasParameters_whenParameter_returnTrue(): void {
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertTrue($user_query->hasParameters());
 	}
 
-	public function testIsDeprecated_whenCategoryExists_returnFalse() {
+	public function testIsDeprecated_whenCategoryExists_returnFalse(): void {
+		/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
 		$cat = $this->createMock('FreshRSS_Category');
+		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -170,7 +176,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue() {
+	public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue(): void {
+		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -181,8 +188,10 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenFeedExists_returnFalse() {
-		$feed = $this->createMock('FreshRSS_Feed', array(), array('', false));
+	public function testIsDeprecated_whenFeedExists_returnFalse(): void {
+		/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
+		$feed = $this->createMock('FreshRSS_Feed');
+		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
 		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -193,7 +202,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenFeedDoesNotExist_returnTrue() {
+	public function testIsDeprecated_whenFeedDoesNotExist_returnTrue(): void {
+		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
 		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
@@ -204,19 +214,19 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenAllQuery_returnFalse() {
+	public function testIsDeprecated_whenAllQuery_returnFalse(): void {
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenFavoriteQuery_returnFalse() {
+	public function testIsDeprecated_whenFavoriteQuery_returnFalse(): void {
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());
 	}
 
-	public function testIsDeprecated_whenUnknownQuery_returnFalse() {
+	public function testIsDeprecated_whenUnknownQuery_returnFalse(): void {
 		$query = array('get' => 'q');
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());

+ 3 - 3
tests/app/Utils/passwordUtilTest.php

@@ -1,7 +1,7 @@
 <?php
 
 class passwordUtilTest extends PHPUnit\Framework\TestCase {
-	public function testCheck() {
+	public function testCheck(): void {
 		$password = '1234567';
 
 		$ok = FreshRSS_password_Util::check($password);
@@ -9,7 +9,7 @@ class passwordUtilTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($ok);
 	}
 
-	public function testCheckReturnsFalseIfEmpty() {
+	public function testCheckReturnsFalseIfEmpty(): void {
 		$password = '';
 
 		$ok = FreshRSS_password_Util::check($password);
@@ -17,7 +17,7 @@ class passwordUtilTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($ok);
 	}
 
-	public function testCheckReturnsFalseIfLessThan7Characters() {
+	public function testCheckReturnsFalseIfLessThan7Characters(): void {
 		$password = '123456';
 
 		$ok = FreshRSS_password_Util::check($password);

+ 7 - 6
tests/cli/i18n/I18nCompletionValidatorTest.php

@@ -4,6 +4,7 @@ require_once __DIR__ . '/../../../cli/i18n/I18nCompletionValidator.php';
 require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 
 class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
+	/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
 	private $value;
 
 	public function setUp(): void {
@@ -12,7 +13,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 			->getMock();
 	}
 
-	public function testDisplayReport() {
+	public function testDisplayReport(): void {
 		$validator = new I18nCompletionValidator([], []);
 
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
@@ -40,13 +41,13 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$validator->displayReport();
 	}
 
-	public function testValidateWhenNoData() {
+	public function testValidateWhenNoData(): void {
 		$validator = new I18nCompletionValidator([], []);
 		$this->assertTrue($validator->validate());
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenKeyIsMissing() {
+	public function testValidateWhenKeyIsMissing(): void {
 		$validator = new I18nCompletionValidator([
 			'file1.php' => [
 				'file1.l1.l2.k1' => $this->value,
@@ -60,7 +61,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult());
 	}
 
-	public function testValidateWhenKeyIsIgnored() {
+	public function testValidateWhenKeyIsIgnored(): void {
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->willReturn(true);
@@ -85,7 +86,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenValueIsEqual() {
+	public function testValidateWhenValueIsEqual(): void {
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->willReturn(false);
@@ -113,7 +114,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult());
 	}
 
-	public function testValidateWhenValueIsDifferent() {
+	public function testValidateWhenValueIsDifferent(): void {
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->willReturn(false);

+ 35 - 33
tests/cli/i18n/I18nDataTest.php

@@ -4,7 +4,9 @@ require_once __DIR__ . '/../../../cli/i18n/I18nData.php';
 require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 
 class I18nDataTest extends PHPUnit\Framework\TestCase {
+	/** @var array<string,array<string,array<string,I18nValue>>> */
 	private $referenceData;
+	/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
 	private $value;
 
 	public function setUp(): void {
@@ -31,12 +33,12 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		];
 	}
 
-	public function testConstructWhenReferenceOnly() {
+	public function testConstructWhenReferenceOnly(): void {
 		$data = new I18nData($this->referenceData);
 		$this->assertEquals($this->referenceData, $data->getData());
 	}
 
-	public function testConstructorWhenLanguageIsMissingFile() {
+	public function testConstructorWhenLanguageIsMissingFile(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 				'file1.php' => [
@@ -79,7 +81,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testConstructorWhenLanguageIsMissingKeys() {
+	public function testConstructorWhenLanguageIsMissingKeys(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 				'file1.php' => [
@@ -125,7 +127,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testConstructorWhenLanguageHasExtraKeys() {
+	public function testConstructorWhenLanguageHasExtraKeys(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 				'file1.php' => [
@@ -179,7 +181,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testConstructorWhenValueIsIdenticalAndIsMarkedAsIgnore() {
+	public function testConstructorWhenValueIsIdenticalAndIsMarkedAsIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -204,7 +206,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 	}
 
-	public function testConstructorWhenValueIsIdenticalAndIsNotMarkedAsIgnore() {
+	public function testConstructorWhenValueIsIdenticalAndIsNotMarkedAsIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -229,7 +231,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 	}
 
-	public function testConstructorWhenValueIsDifferentAndIsMarkedAsToDo() {
+	public function testConstructorWhenValueIsDifferentAndIsMarkedAsToDo(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -249,7 +251,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 	}
 
-	public function testConstructorWhenValueIsDifferentAndIsNotMarkedAsTodo() {
+	public function testConstructorWhenValueIsDifferentAndIsNotMarkedAsTodo(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -269,7 +271,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 	}
 
-	public function testGetAvailableLanguagesWhenTheyAreSorted() {
+	public function testGetAvailableLanguagesWhenTheyAreSorted(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'nl' => [],
@@ -282,7 +284,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getAvailableLanguages());
 	}
 
-	public function testGetAvailableLanguagesWhenTheyAreNotSorted() {
+	public function testGetAvailableLanguagesWhenTheyAreNotSorted(): void {
 		$rawData = array_merge($this->referenceData, [
 			'nl' => [],
 			'fr' => [],
@@ -297,14 +299,14 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getAvailableLanguages());
 	}
 
-	public function testAddLanguageWhenLanguageExists() {
+	public function testAddLanguageWhenLanguageExists(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected language already exist.');
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('en');
 	}
 
-	public function testAddLanguageWhenNoReferenceProvided() {
+	public function testAddLanguageWhenNoReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr');
 		$this->assertEquals([
@@ -341,7 +343,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testAddLanguageWhenUnknownReferenceProvided() {
+	public function testAddLanguageWhenUnknownReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr', 'unknown');
 		$this->assertEquals([
@@ -378,7 +380,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testAddLanguageWhenKnownReferenceProvided() {
+	public function testAddLanguageWhenKnownReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr', 'en');
 		$this->assertEquals([
@@ -415,24 +417,24 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testIsKnownWhenKeyExists() {
+	public function testIsKnownWhenKeyExists(): void {
 		$data = new I18nData($this->referenceData);
 		$this->assertTrue($data->isKnown('file2.l1.l2.k2'));
 	}
 
-	public function testIsKnownWhenKeyDoesNotExist() {
+	public function testIsKnownWhenKeyDoesNotExist(): void {
 		$data = new I18nData($this->referenceData);
 		$this->assertFalse($data->isKnown('file2.l1.l2.k3'));
 	}
 
-	public function testAddKeyWhenKeyExists() {
+	public function testAddKeyWhenKeyExists(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key already exist.');
 		$data = new I18nData($this->referenceData);
 		$data->addKey('file2.l1.l2.k1', 'value');
 	}
 
-	public function testAddKeyWhenParentKeyExists() {
+	public function testAddKeyWhenParentKeyExists(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 		]);
@@ -447,7 +449,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($data->isKnown('file2.l1.l2.k1.sk1'));
 	}
 
-	public function testAddKeyWhenKeyIsParent() {
+	public function testAddKeyWhenKeyIsParent(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 		]);
@@ -462,7 +464,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($data->isKnown('file1.l1.l2.k2'));
 	}
 
-	public function testAddKey() {
+	public function testAddKey(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k3'];
 		};
@@ -484,21 +486,21 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($frValue, $enValue);
 	}
 
-	public function testAddValueWhenLanguageDoesNotExist() {
+	public function testAddValueWhenLanguageDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected language does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data->addValue('file2.l1.l2.k2', 'new value', 'fr');
 	}
 
-	public function testAddValueWhenKeyDoesNotExist() {
+	public function testAddValueWhenKeyDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist for the selected language.');
 		$data = new I18nData($this->referenceData);
 		$data->addValue('unknown key', 'new value', 'en');
 	}
 
-	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasNotChange() {
+	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasNotChange(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 		};
@@ -526,7 +528,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('new value', $afterFrValue->getValue());
 	}
 
-	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange() {
+	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 		};
@@ -561,7 +563,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($value, $afterFrValue);
 	}
 
-	public function testAddValueWhenLanguageIsNotReference() {
+	public function testAddValueWhenLanguageIsNotReference(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 		};
@@ -583,21 +585,21 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('new value', $afterFrValue->getValue());
 	}
 
-	public function testRemoveKeyWhenKeyDoesNotExist() {
+	public function testRemoveKeyWhenKeyDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data->removeKey('Unknown key');
 	}
 
-	public function testRemoveKeyWhenKeyHasNoEmptySibling() {
+	public function testRemoveKeyWhenKeyHasNoEmptySibling(): void {
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data->removeKey('file1.l1.l2');
 	}
 
-	public function testRemoveKeyWhenKeyIsEmptySibling() {
+	public function testRemoveKeyWhenKeyIsEmptySibling(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 		]);
@@ -635,7 +637,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testRemoveKeyWhenKeyIsTheOnlyChild() {
+	public function testRemoveKeyWhenKeyIsTheOnlyChild(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 		]);
@@ -673,7 +675,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 	}
 
-	public function testIgnore() {
+	public function testIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -695,7 +697,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$data->ignore('file1.l1.l2.k1', 'fr', false);
 	}
 
-	public function testIgnoreUnmodified() {
+	public function testIgnoreUnmodified(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->getMock();
@@ -722,7 +724,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$data->ignore_unmodified('fr', false);
 	}
 
-	public function testGetLanguage() {
+	public function testGetLanguage(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'nl' => [],
@@ -731,7 +733,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($this->referenceData['en'], $data->getLanguage('en'));
 	}
 
-	public function testGetReferenceLanguage() {
+	public function testGetReferenceLanguage(): void {
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'nl' => [],

+ 3 - 2
tests/cli/i18n/I18nFileTest.php

@@ -3,7 +3,7 @@
 require_once __DIR__ . '/../../../cli/i18n/I18nFile.php';
 
 class I18nFileTest extends PHPUnit\Framework\TestCase {
-	public function test() {
+	public function test(): void {
 		$before = $this->computeFilesHash();
 
 		$file = new I18nFile();
@@ -15,7 +15,8 @@ class I18nFileTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($before, $after);
 	}
 
-	private function computeFilesHash() {
+	/** @return array<string,string> */
+	private function computeFilesHash(): array {
 		$hashes = [];
 
 		$dirs = new DirectoryIterator(I18N_PATH);

+ 8 - 7
tests/cli/i18n/I18nUsageValidatorTest.php

@@ -4,6 +4,7 @@ require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 require_once __DIR__ . '/../../../cli/i18n/I18nUsageValidator.php';
 
 class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
+	/** @var I18nValue */
 	private $value;
 
 	public function setUp(): void {
@@ -12,7 +13,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 			->getMock();
 	}
 
-	public function testDisplayReport() {
+	public function testDisplayReport(): void {
 		$validator = new I18nUsageValidator([], []);
 
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
@@ -40,13 +41,13 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$validator->displayReport();
 	}
 
-	public function testValidateWhenNoData() {
+	public function testValidateWhenNoData(): void {
 		$validator = new I18nUsageValidator([], []);
 		$this->assertTrue($validator->validate());
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenParentKeyExistsWithoutTransformation() {
+	public function testValidateWhenParentKeyExistsWithoutTransformation(): void {
 		$validator = new I18nUsageValidator([
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
@@ -62,7 +63,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenParentKeyExistsWithTransformation() {
+	public function testValidateWhenParentKeyExistsWithTransformation(): void {
 		$validator = new I18nUsageValidator([
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
@@ -78,7 +79,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenParentKeyDoesNotExist() {
+	public function testValidateWhenParentKeyDoesNotExist(): void {
 		$validator = new I18nUsageValidator([
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
@@ -91,7 +92,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals("Unused key file1.l1.l2._ - \nUnused key file2.l1.l2._ - \n", $validator->displayResult());
 	}
 
-	public function testValidateWhenChildKeyExists() {
+	public function testValidateWhenChildKeyExists(): void {
 		$validator = new I18nUsageValidator([
 			'file1' => [
 				'file1.l1.l2.k1' => $this->value,
@@ -107,7 +108,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 	}
 
-	public function testValidateWhenChildKeyDoesNotExist() {
+	public function testValidateWhenChildKeyDoesNotExist(): void {
 		$validator = new I18nUsageValidator([
 			'file1' => [
 				'file1.l1.l2.k1' => $this->value,

+ 9 - 9
tests/cli/i18n/I18nValueTest.php

@@ -3,35 +3,35 @@
 require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 
 class I18nValueTest extends PHPUnit\Framework\TestCase {
-	public function testConstructorWithoutState() {
+	public function testConstructorWithoutState(): void {
 		$value = new I18nValue('some value');
 		$this->assertEquals('some value', $value->getValue());
 		$this->assertFalse($value->isIgnore());
 		$this->assertFalse($value->isTodo());
 	}
 
-	public function testConstructorWithUnknownState() {
+	public function testConstructorWithUnknownState(): void {
 		$value = new I18nValue('some value -> unknown');
 		$this->assertEquals('some value', $value->getValue());
 		$this->assertFalse($value->isIgnore());
 		$this->assertFalse($value->isTodo());
 	}
 
-	public function testConstructorWithTodoState() {
+	public function testConstructorWithTodoState(): void {
 		$value = new I18nValue('some value -> todo');
 		$this->assertEquals('some value', $value->getValue());
 		$this->assertFalse($value->isIgnore());
 		$this->assertTrue($value->isTodo());
 	}
 
-	public function testConstructorWithIgnoreState() {
+	public function testConstructorWithIgnoreState(): void {
 		$value = new I18nValue('some value -> ignore');
 		$this->assertEquals('some value', $value->getValue());
 		$this->assertTrue($value->isIgnore());
 		$this->assertFalse($value->isTodo());
 	}
 
-	public function testClone() {
+	public function testClone(): void {
 		$value = new I18nValue('some value');
 		$clonedValue = clone $value;
 		$this->assertEquals('some value', $value->getValue());
@@ -42,21 +42,21 @@ class I18nValueTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($clonedValue->isTodo());
 	}
 
-	public function testEqualWhenValueIsIdentical() {
+	public function testEqualWhenValueIsIdentical(): void {
 		$value = new I18nValue('some value');
 		$clonedValue = clone $value;
 		$this->assertTrue($value->equal($clonedValue));
 		$this->assertTrue($clonedValue->equal($value));
 	}
 
-	public function testEqualWhenValueIsDifferent() {
+	public function testEqualWhenValueIsDifferent(): void {
 		$value = new I18nValue('some value');
 		$otherValue = new I18nValue('some other value');
 		$this->assertFalse($value->equal($otherValue));
 		$this->assertFalse($otherValue->equal($value));
 	}
 
-	public function testStates() {
+	public function testStates(): void {
 		$reflectionProperty = new ReflectionProperty(I18nValue::class, 'state');
 		$reflectionProperty->setAccessible(true);
 
@@ -74,7 +74,7 @@ class I18nValueTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('todo', $reflectionProperty->getValue($value));
 	}
 
-	public function testToString() {
+	public function testToString(): void {
 		$value = new I18nValue('some value');
 		$this->assertEquals('some value', $value->__toString());
 		$value->markAsTodo();

+ 2 - 2
tests/fixtures/migrations/2019_12_22_FooBar.php

@@ -2,9 +2,9 @@
 
 class FreshRSS_Migration_2019_12_22_FooBar {
 	/**
-	 * @return boolean true if the migration was successful, false otherwise
+	 * @return bool true if the migration was successful, false otherwise
 	 */
-	public static function migrate() {
+	public static function migrate(): bool {
 		return true;
 	}
 }

+ 2 - 2
tests/fixtures/migrations/2019_12_23_Baz.php

@@ -2,9 +2,9 @@
 
 class FreshRSS_Migration_2019_12_23_Baz {
 	/**
-	 * @return boolean true if the migration was successful, false otherwise
+	 * @return bool true if the migration was successful, false otherwise
 	 */
-	public static function migrate() {
+	public static function migrate(): bool {
 		return true;
 	}
 }

+ 2 - 2
tests/fixtures/migrations_with_failing/2020_01_11_FooBar.php

@@ -2,9 +2,9 @@
 
 class FreshRSS_Migration_2020_01_11_FooBar {
 	/**
-	 * @return boolean true if the migration was successful, false otherwise
+	 * @return bool true if the migration was successful, false otherwise
 	 */
-	public static function migrate() {
+	public static function migrate(): bool {
 		return true;
 	}
 }

+ 2 - 2
tests/fixtures/migrations_with_failing/2020_01_12_Baz.php

@@ -2,9 +2,9 @@
 
 class FreshRSS_Migration_2020_01_12_Baz {
 	/**
-	 * @return boolean true if the migration was successful, false otherwise
+	 * @return bool true if the migration was successful, false otherwise
 	 */
-	public static function migrate() {
+	public static function migrate(): bool {
 		return false;
 	}
 }

+ 1 - 1
tests/lib/CssXPath/CssXPathTest.php

@@ -2,7 +2,7 @@
 
 class CssXPathTest extends PHPUnit\Framework\TestCase
 {
-	public function testCssXPathTranslatorClassExists() {
+	public function testCssXPathTranslatorClassExists(): void {
 		$this->assertTrue(class_exists('Gt\\CssXPath\\Translator'));
 	}
 }

+ 23 - 23
tests/lib/Minz/MigratorTest.php

@@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase;
 
 class MigratorTest extends TestCase
 {
-	public function testAddMigration() {
+	public function testAddMigration(): void {
 		$migrator = new Minz_Migrator();
 
 		$migrator->addMigration('foo', function () {
@@ -17,7 +17,7 @@ class MigratorTest extends TestCase
 		$this->assertTrue($result);
 	}
 
-	public function testAddMigrationFailsIfUncallableMigration() {
+	public function testAddMigrationFailsIfUncallableMigration(): void {
 		$this->expectException(BadFunctionCallException::class);
 		$this->expectExceptionMessage('foo migration cannot be called.');
 
@@ -25,7 +25,7 @@ class MigratorTest extends TestCase
 		$migrator->addMigration('foo', null);
 	}
 
-	public function testMigrationsIsSorted() {
+	public function testMigrationsIsSorted(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('2_foo', function () {
 			return true;
@@ -43,7 +43,7 @@ class MigratorTest extends TestCase
 		$this->assertSame($expected_versions, array_keys($migrations));
 	}
 
-	public function testSetAppliedVersions() {
+	public function testSetAppliedVersions(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('foo', function () {
 			return true;
@@ -54,7 +54,7 @@ class MigratorTest extends TestCase
 		$this->assertSame(['foo'], $migrator->appliedVersions());
 	}
 
-	public function testSetAppliedVersionsTrimArgument() {
+	public function testSetAppliedVersionsTrimArgument(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('foo', function () {
 			return true;
@@ -65,7 +65,7 @@ class MigratorTest extends TestCase
 		$this->assertSame(['foo'], $migrator->appliedVersions());
 	}
 
-	public function testSetAppliedVersionsFailsIfMigrationDoesNotExist() {
+	public function testSetAppliedVersionsFailsIfMigrationDoesNotExist(): void {
 		$this->expectException(DomainException::class);
 		$this->expectExceptionMessage('foo migration does not exist.');
 
@@ -74,7 +74,7 @@ class MigratorTest extends TestCase
 		$migrator->setAppliedVersions(['foo']);
 	}
 
-	public function testVersions() {
+	public function testVersions(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('foo', function () {
 			return true;
@@ -88,7 +88,7 @@ class MigratorTest extends TestCase
 		$this->assertSame(['bar', 'foo'], $versions);
 	}
 
-	public function testMigrate() {
+	public function testMigrate(): void {
 		$migrator = new Minz_Migrator();
 		$spy = false;
 		$migrator->addMigration('foo', function () use (&$spy) {
@@ -106,7 +106,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testMigrateCallsMigrationsInSortedOrder() {
+	public function testMigrateCallsMigrationsInSortedOrder(): void {
 		$migrator = new Minz_Migrator();
 		$spy_foo_1_is_called = false;
 		$migrator->addMigration('2_foo', function () use (&$spy_foo_1_is_called) {
@@ -126,7 +126,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testMigrateDoesNotCallAppliedMigrations() {
+	public function testMigrateDoesNotCallAppliedMigrations(): void {
 		$migrator = new Minz_Migrator();
 		$spy = false;
 		$migrator->addMigration('1_foo', function () use (&$spy) {
@@ -141,7 +141,7 @@ class MigratorTest extends TestCase
 		$this->assertSame([], $result);
 	}
 
-	public function testMigrateCallNonAppliedBetweenTwoApplied() {
+	public function testMigrateCallNonAppliedBetweenTwoApplied(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('1_foo', function () {
 			return true;
@@ -162,7 +162,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testMigrateWithMigrationReturningFalseDoesNotApplyVersion() {
+	public function testMigrateWithMigrationReturningFalseDoesNotApplyVersion(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('1_foo', function () {
 			return true;
@@ -180,7 +180,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testMigrateWithMigrationReturningFalseDoesNotExecuteNextMigrations() {
+	public function testMigrateWithMigrationReturningFalseDoesNotExecuteNextMigrations(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('1_foo', function () {
 			return false;
@@ -200,7 +200,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testMigrateWithFailingMigration() {
+	public function testMigrateWithFailingMigration(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('foo', function () {
 			throw new \Exception('Oops, it failed.');
@@ -214,7 +214,7 @@ class MigratorTest extends TestCase
 		], $result);
 	}
 
-	public function testUpToDate() {
+	public function testUpToDate(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('foo', function () {
 			return true;
@@ -226,7 +226,7 @@ class MigratorTest extends TestCase
 		$this->assertTrue($upToDate);
 	}
 
-	public function testUpToDateIfRemainingMigration() {
+	public function testUpToDateIfRemainingMigration(): void {
 		$migrator = new Minz_Migrator();
 		$migrator->addMigration('1_foo', function () {
 			return true;
@@ -241,7 +241,7 @@ class MigratorTest extends TestCase
 		$this->assertFalse($upToDate);
 	}
 
-	public function testUpToDateIfNoMigrations() {
+	public function testUpToDateIfNoMigrations(): void {
 		$migrator = new Minz_Migrator();
 
 		$upToDate = $migrator->upToDate();
@@ -249,7 +249,7 @@ class MigratorTest extends TestCase
 		$this->assertTrue($upToDate);
 	}
 
-	public function testConstructorLoadsDirectory() {
+	public function testConstructorLoadsDirectory(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations/';
 		$migrator = new Minz_Migrator($migrations_path);
 		$expected_versions = ['2019_12_22_FooBar', '2019_12_23_Baz'];
@@ -259,7 +259,7 @@ class MigratorTest extends TestCase
 		$this->assertSame($expected_versions, array_keys($migrations));
 	}
 
-	public function testExecute() {
+	public function testExecute(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations/';
 		$applied_migrations_path = tempnam('/tmp', 'applied_migrations.txt');
 
@@ -271,7 +271,7 @@ class MigratorTest extends TestCase
 		@unlink($applied_migrations_path);
 	}
 
-	public function testExecuteWithAlreadyAppliedMigration() {
+	public function testExecuteWithAlreadyAppliedMigration(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations/';
 		$applied_migrations_path = tempnam('/tmp', 'applied_migrations.txt');
 		file_put_contents($applied_migrations_path, '2019_12_22_FooBar');
@@ -284,7 +284,7 @@ class MigratorTest extends TestCase
 		@unlink($applied_migrations_path);
 	}
 
-	public function testExecuteWithAppliedMigrationInDifferentOrder() {
+	public function testExecuteWithAppliedMigrationInDifferentOrder(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations/';
 		$applied_migrations_path = tempnam('/tmp', 'applied_migrations.txt');
 		file_put_contents($applied_migrations_path, "2019_12_23_Baz\n2019_12_22_FooBar");
@@ -299,7 +299,7 @@ class MigratorTest extends TestCase
 		@unlink($applied_migrations_path);
 	}
 
-	public function testExecuteFailsIfVersionPathDoesNotExist() {
+	public function testExecuteFailsIfVersionPathDoesNotExist(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations/';
 		$applied_migrations_path = tempnam('/tmp', 'applied_migrations.txt');
 		$expected_result = "Cannot open the {$applied_migrations_path} file";
@@ -311,7 +311,7 @@ class MigratorTest extends TestCase
 		@unlink($applied_migrations_path);
 	}
 
-	public function testExecuteFailsIfAMigrationIsFailing() {
+	public function testExecuteFailsIfAMigrationIsFailing(): void {
 		$migrations_path = TESTS_PATH . '/fixtures/migrations_with_failing/';
 		$applied_migrations_path = tempnam('/tmp', 'applied_migrations.txt');
 		$expected_result = 'A migration failed to be applied, please see previous logs.';

+ 1 - 1
tests/lib/PHPMailer/PHPMailerTest.php

@@ -2,7 +2,7 @@
 
 class PHPMailerTest extends PHPUnit\Framework\TestCase
 {
-	public function testPHPMailerClassExists() {
+	public function testPHPMailerClassExists(): void {
 		$this->assertTrue(class_exists('PHPMailer\\PHPMailer\\PHPMailer'));
 	}
 }

+ 43 - 5
tests/phpstan-next.txt

@@ -1,10 +1,48 @@
-# List of files, which are not yet passing PHPStan level 6 https://phpstan.org/user-guide/rule-levels
-# https://github.com/FreshRSS/FreshRSS/issues/4112
+# List of files, which are not yet passing PHPStan level 7 https://phpstan.org/user-guide/rule-levels
 # Used for automated tests to avoid regressions in files already passing that level.
 # Can be regenerated with something like:
-# find . -type d -name 'vendor' -prune -o -name '*.php' -exec sh -c 'vendor/bin/phpstan analyse --level 6 --memory-limit 512M {} >/dev/null 2>/dev/null || echo {}' \;
+# find . -type d -name 'vendor' -prune -o -name '*.php' -exec sh -c 'vendor/bin/phpstan analyse --level 7 --memory-limit 512M {} >/dev/null 2>/dev/null || echo {}' \;
 
-./app/install.php
+./app/Controllers/configureController.php
+./app/Controllers/feedController.php
+./app/Controllers/importExportController.php
+./app/Controllers/indexController.php
+./app/Controllers/updateController.php
+./app/Controllers/userController.php
+./app/Models/CategoryDAO.php
+./app/Models/Context.php
+./app/Models/DatabaseDAO.php
+./app/Models/DatabaseDAOPGSQL.php
+./app/Models/Entry.php
+./app/Models/EntryDAO.php
 ./app/Models/Feed.php
+./app/Models/FeedDAO.php
+./app/Models/ReadingMode.php
+./app/Models/Search.php
+./app/Models/Share.php
+./app/Models/StatsDAO.php
+./app/Models/TagDAO.php
+./app/Models/Themes.php
+./app/Models/UserQuery.php
+./app/Services/ExportService.php
 ./app/Services/ImportService.php
-./lib/Minz/Paginator.php
+./app/views/helpers/logs_pagination.phtml
+./app/views/index/reader.phtml
+./app/views/stats/index.phtml
+./app/views/stats/repartition.phtml
+./app/views/user/details.phtml
+./cli/check.translation.php
+./cli/delete-user.php
+./cli/do-install.php
+./cli/manipulate.translation.php
+./cli/user-info.php
+./lib/lib_date.php
+./lib/Minz/ActionController.php
+./lib/Minz/Error.php
+./lib/Minz/Mailer.php
+./lib/Minz/Migrator.php
+./lib/Minz/ModelPdo.php
+./lib/Minz/Request.php
+./p/api/greader.php
+./tests/cli/i18n/I18nFileTest.php
+./tests/lib/Minz/MigratorTest.php