Browse Source

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 years ago
parent
commit
f3760f138d
100 changed files with 836 additions and 719 deletions
  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');
 		$url_redirect = array('c' => 'subscription', 'a' => 'add');
 
 
 		$limits = FreshRSS_Context::$system_conf->limits;
 		$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']) {
 		if (count($this->view->categories) >= $limits['max_categories']) {
 			Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), $url_redirect);
 			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')) {
 			if (Minz_Request::paramBoolean('ajax')) {
 				Minz_Request::setGoodNotification(_t('feedback.sub.category.updated'));
 				Minz_Request::setGoodNotification(_t('feedback.sub.category.updated'));
-				$this->view->_layout(false);
+				$this->view->_layout(null);
 			} else {
 			} else {
 				if ($ok) {
 				if ($ok) {
 					Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
 					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;
 		FreshRSS_Context::$user_conf->volatile = $volatile;
 
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
-		$this->view->nb_total = $entryDAO->count();
+		$this->view->nb_total = $entryDAO->count() ?: 0;
 
 
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$this->view->size_user = $databaseDAO->size();
 		$this->view->size_user = $databaseDAO->size();
@@ -338,7 +338,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 	 * applied to the selected query.
 	 * applied to the selected query.
 	 */
 	 */
 	public function queryAction(): void {
 	public function queryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 
 		$id = Minz_Request::paramInt('id');
 		$id = Minz_Request::paramInt('id');
 		if ($id !== 0 || !isset(FreshRSS_Context::$user_conf->queries[$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
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		if ($this->ajax) {
 		if ($this->ajax) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 			Minz_Request::_param('ajax');
 			Minz_Request::_param('ajax');
 		}
 		}
 	}
 	}
@@ -107,7 +107,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 			$ids = is_array($id) ? $id : array($id);
 			$ids = is_array($id) ? $id : array($id);
 			$entryDAO->markRead($ids, $is_read);
 			$entryDAO->markRead($ids, $is_read);
 			$tagDAO = FreshRSS_Factory::createTagDao();
 			$tagDAO = FreshRSS_Factory::createTagDao();
-			$tagsForEntries = $tagDAO->getTagsForEntries($ids);
+			$tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: [];
 			$tags = array();
 			$tags = array();
 			foreach ($tagsForEntries as $line) {
 			foreach ($tagsForEntries as $line) {
 				$tags['t_' . $line['id_tag']][] = $line['id_entry'];
 				$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 {
 	public function configureAction(): void {
 		if (Minz_Request::paramBoolean('ajax')) {
 		if (Minz_Request::paramBoolean('ajax')) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 		} else {
 			$this->indexAction();
 			$this->indexAction();
 			$this->view->_path('extension/index.phtml');
 			$this->view->_path('extension/index.phtml');
@@ -143,7 +143,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 
 			if ($conf !== null && $res === true) {
 			if ($conf !== null && $res === true) {
 				$ext_list = $conf->extensions_enabled;
 				$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
 					// Remove from list the extensions that have disappeared or changed type
 					$extension = Minz_ExtensionManager::findExtension($key);
 					$extension = Minz_ExtensionManager::findExtension($key);
 					return $extension !== null && $extension->getType() === $type;
 					return $extension !== null && $extension->getType() === $type;
@@ -205,7 +205,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 
 			if ($conf !== null && $res === true) {
 			if ($conf !== null && $res === true) {
 				$ext_list = $conf->extensions_enabled;
 				$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
 					// Remove from list the extensions that have disappeared or changed type
 					$extension = Minz_ExtensionManager::findExtension($key);
 					$extension = Minz_ExtensionManager::findExtension($key);
 					return $extension !== null && $extension->getType() === $type;
 					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') . ' · ');
 			FreshRSS_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 
 
 			$catDAO = FreshRSS_Factory::createCategoryDao();
 			$catDAO = FreshRSS_Factory::createCategoryDao();
-			$this->view->categories = $catDAO->listCategories(false);
+			$this->view->categories = $catDAO->listCategories(false) ?: [];
 			$this->view->feed = new FreshRSS_Feed($url);
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
 			try {
 				// We try to get more information about the feed.
 				// 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}
 	 * @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);
 		@set_time_limit(300);
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -666,7 +667,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			// are several updated feeds.
 			// are several updated feeds.
 			Minz_Request::setGoodNotification(_t('feedback.sub.feed.actualizeds'));
 			Minz_Request::setGoodNotification(_t('feedback.sub.feed.actualizeds'));
 			// No layout in ajax request.
 			// No layout in ajax request.
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 		} else {
 			// Redirect to the main page with correct notification.
 			// Redirect to the main page with correct notification.
 			if ($updated_feeds === 1) {
 			if ($updated_feeds === 1) {
@@ -903,7 +904,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$this->view->selectorSuccess = false;
 		$this->view->selectorSuccess = false;
 		$this->view->htmlContent = '';
 		$this->view->htmlContent = '';
 
 
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 
 		$this->_csp([
 		$this->_csp([
 			'default-src' => "'self'",
 			'default-src' => "'self'",

+ 4 - 6
app/Controllers/importExportController.php

@@ -226,7 +226,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		unset($table['article']);
 		unset($table['article']);
 		for ($i = count($table['items']) - 1; $i >= 0; $i--) {
 		for ($i = count($table['items']) - 1; $i >= 0; $i--) {
 			$item = (array)($table['items'][$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
 					// Filter out empty properties, potentially reported as empty objects
 					return (is_string($v) && trim($v) !== '') || !empty($v);
 					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.
 	 * $article_file the JSON file content.
 	 * true if articles from the file must be starred.
 	 * 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 {
 	private function importJson(string $article_file, bool $starred = false): bool {
 		$article_object = json_decode($article_file, true);
 		$article_object = json_decode($article_file, true);
@@ -575,10 +575,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 	 *   - export_starred (default: false)
 	 *   - export_starred (default: false)
 	 *   - export_labelled (default: false)
 	 *   - export_labelled (default: false)
 	 *   - export_feeds (default: array()) a list of feed ids
 	 *   - export_feeds (default: array()) a list of feed ids
-	 *
-	 * @return void|null
 	 */
 	 */
-	public function exportAction() {
+	public function exportAction(): void {
 		if (!Minz_Request::isPost()) {
 		if (!Minz_Request::isPost()) {
 			Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
 			Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
 			return;
 			return;
@@ -654,7 +652,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		header('Content-Type: ' . $content_type);
 		header('Content-Type: ' . $content_type);
 		header('Content-disposition: attachment; filename="' . $filename . '"');
 		header('Content-disposition: attachment; filename="' . $filename . '"');
 
 
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		$this->view->content = $content;
 		$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';
 		FreshRSS_Context::$id_max = time() . '000000';
 
 
-		$this->view->callbackBeforeFeeds = function ($view) {
+		$this->view->callbackBeforeFeeds = function (FreshRSS_View $view) {
 			try {
 			try {
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				$tagDAO = FreshRSS_Factory::createTagDao();
-				$view->tags = $tagDAO->listTags(true);
+				$view->tags = $tagDAO->listTags(true) ?: [];
 				$view->nbUnreadTags = 0;
 				$view->nbUnreadTags = 0;
 				foreach ($view->tags as $tag) {
 				foreach ($view->tags as $tag) {
 					$view->nbUnreadTags += $tag->nbUnread();
 					$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 {
 			try {
 				FreshRSS_Context::$number++;	//+1 for articles' page
 				FreshRSS_Context::$number++;	//+1 for articles' page
 				$view->entries = FreshRSS_index_Controller::listEntriesByContext();
 				$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) {
 			if ($nbEntries >= FreshRSS_Context::$number) {
 				//We have enough entries: we discard the last one to use it for the next articles' page
 				//We have enough entries: we discard the last one to use it for the next articles' page
 				ob_clean();
 				ob_clean();
@@ -170,7 +170,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		// No layout for RSS output.
 		// No layout for RSS output.
 		$this->view->rss_url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
 		$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->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');
 		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.
 		// No layout for OPML output.
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		header('Content-Type: application/xml; charset=utf-8');
 		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 {
 class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 
 
 	public function firstAction(): void {
 	public function firstAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 	}
 	}
 
 
 	public function actualizeAction(): void {
 	public function actualizeAction(): void {
@@ -20,9 +20,9 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 	public function nbUnreadsPerFeedAction(): void {
 	public function nbUnreadsPerFeedAction(): void {
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Content-Type: application/json; charset=UTF-8');
 		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$catDAO = FreshRSS_Factory::createCategoryDao();
-		$this->view->categories = $catDAO->listCategories(true, false);
+		$this->view->categories = $catDAO->listCategories(true, false) ?: [];
 		$tagDAO = FreshRSS_Factory::createTagDao();
 		$tagDAO = FreshRSS_Factory::createTagDao();
-		$this->view->tags = $tagDAO->listTags(true);
+		$this->view->tags = $tagDAO->listTags(true) ?: [];
 	}
 	}
 
 
 	//For Web-form login
 	//For Web-form login

+ 2 - 2
app/Controllers/statsController.php

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

+ 6 - 6
app/Controllers/subscriptionController.php

@@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 
 
 		$catDAO->checkDefault();
 		$catDAO->checkDefault();
 		$feedDAO->updateTTL();
 		$feedDAO->updateTTL();
-		$this->view->categories = $catDAO->listSortedCategories(false, true);
+		$this->view->categories = $catDAO->listSortedCategories(false, true) ?: [];
 		$this->view->default_category = $catDAO->getDefault();
 		$this->view->default_category = $catDAO->getDefault();
 
 
 		$signalError = false;
 		$signalError = false;
@@ -90,7 +90,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 	 */
 	 */
 	public function feedAction(): void {
 	public function feedAction(): void {
 		if (Minz_Request::paramBoolean('ajax')) {
 		if (Minz_Request::paramBoolean('ajax')) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 		} else {
 		} else {
 			FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
 			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);
 			$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) {
 			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'),
 				'name' => Minz_Request::paramString('name'),
 				'kind' => $feed->kind(),
 				'kind' => $feed->kind(),
 				'description' => sanitizeHTML(Minz_Request::paramString('description', true)),
 				'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'),
 				'category' => Minz_Request::paramInt('category'),
 				'pathEntries' => Minz_Request::paramString('path_entries'),
 				'pathEntries' => Minz_Request::paramString('path_entries'),
 				'priority' => Minz_Request::paramTernary('priority') === null ? FreshRSS_Feed::PRIORITY_MAIN_STREAM : Minz_Request::paramInt('priority'),
 				'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 {
 	public function categoryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$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
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		if ($this->ajax) {
 		if ($this->ajax) {
-			$this->view->_layout(false);
+			$this->view->_layout(null);
 			Minz_Request::_param('ajax');
 			Minz_Request::_param('ajax');
 		}
 		}
 	}
 	}
@@ -39,7 +39,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 			$checked = Minz_Request::paramTernary('checked');
 			$checked = Minz_Request::paramTernary('checked');
 			if ($id_entry != '') {
 			if ($id_entry != '') {
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				$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)) {
 					if ($existing_tag = $tagDAO->searchByName($name_tag)) {
 						// Use existing tag
 						// Use existing tag
 						$tagDAO->tagEntry($existing_tag->id(), $id_entry, $checked);
 						$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));
 						$id_tag = $tagDAO->addTag(array('name' => $name_tag));
 					}
 					}
 				}
 				}
-				if ($id_tag !== 0) {
+				if ($id_tag != false) {
 					$tagDAO->tagEntry($id_tag, $id_entry, $checked);
 					$tagDAO->tagEntry($id_tag, $id_entry, $checked);
 				}
 				}
 			}
 			}
@@ -82,12 +82,12 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 	}
 	}
 
 
 	public function getTagsForEntryAction(): void {
 	public function getTagsForEntryAction(): void {
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Cache-Control: private, no-cache, no-store, must-revalidate');
 		header('Cache-Control: private, no-cache, no-store, must-revalidate');
 		$id_entry = Minz_Request::paramString('id_entry');
 		$id_entry = Minz_Request::paramString('id_entry');
 		$tagDAO = FreshRSS_Factory::createTagDao();
 		$tagDAO = FreshRSS_Factory::createTagDao();
-		$this->view->tagsForEntry = $tagDAO->getTagsForEntry($id_entry);
+		$this->view->tagsForEntry = $tagDAO->getTagsForEntry($id_entry) ?: [];
 	}
 	}
 
 
 	public function addAction(): void {
 	public function addAction(): void {
@@ -110,11 +110,10 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 	}
 	}
 
 
 	/**
 	/**
-	 * @return void|null
 	 * @throws Minz_ConfigurationNamespaceException
 	 * @throws Minz_ConfigurationNamespaceException
 	 * @throws Minz_PDOConnectionException
 	 * @throws Minz_PDOConnectionException
 	 */
 	 */
-	public function renameAction() {
+	public function renameAction(): void {
 		if (!Minz_Request::isPost()) {
 		if (!Minz_Request::isPost()) {
 			Minz_Error::error(405);
 			Minz_Error::error(405);
 		}
 		}
@@ -145,6 +144,6 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 
 
 	public function indexAction(): void {
 	public function indexAction(): void {
 		$tagDAO = FreshRSS_Factory::createTagDao();
 		$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.
 	 * Returns if current user has access to the given scope.
 	 *
 	 *
 	 * @param string $scope general (default) or admin
 	 * @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) {
 		if (FreshRSS_Context::$user_conf == null) {
 			return false;
 			return false;
 		}
 		}
@@ -154,7 +154,7 @@ class FreshRSS_Auth {
 		default:
 		default:
 			$ok = false;
 			$ok = false;
 		}
 		}
-		return $ok;
+		return (bool)$ok;
 	}
 	}
 
 
 	/**
 	/**

+ 1 - 1
app/Models/BooleanSearch.php

@@ -230,7 +230,7 @@ class FreshRSS_BooleanSearch {
 		if ($input == '') {
 		if ($input == '') {
 			return;
 			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 = '';
 		$segment = '';
 		$ns = count($splits);
 		$ns = count($splits);

+ 1 - 1
app/Models/Category.php

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

+ 1 - 1
app/Models/CategoryDAO.php

@@ -477,7 +477,7 @@ SQL;
 			$cat->_id($previousLine['c_id']);
 			$cat->_id($previousLine['c_id']);
 			$cat->_kind($previousLine['c_kind']);
 			$cat->_kind($previousLine['c_kind']);
 			$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
 			$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
-			$cat->_error($previousLine['c_error'] ?? false);
+			$cat->_error($previousLine['c_error'] ?? 0);
 			$cat->_attributes('', $previousLine['c_attributes']);
 			$cat->_attributes('', $previousLine['c_attributes']);
 			$list[$previousLine['c_id']] = $cat;
 			$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.
 	 * Initialize the context for the current user.
-	 * @return FreshRSS_UserConfiguration|false
 	 * @throws Minz_ConfigurationParamException
 	 * @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;
 		FreshRSS_Context::$user_conf = null;
 		if (!isset($_SESSION)) {
 		if (!isset($_SESSION)) {
 			Minz_Session::init('FreshRSS');
 			Minz_Session::init('FreshRSS');
@@ -145,7 +144,7 @@ final class FreshRSS_Context {
 		Minz_Session::unlock();
 		Minz_Session::unlock();
 
 
 		if (FreshRSS_Context::$user_conf == null) {
 		if (FreshRSS_Context::$user_conf == null) {
-			return false;
+			return null;
 		}
 		}
 
 
 		FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
 		FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
@@ -249,8 +248,8 @@ final class FreshRSS_Context {
 	/**
 	/**
 	 * Return the current get as a string or an array.
 	 * 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}
 	 * @return string|array{string,bool|int}
 	 */
 	 */
 	public static function currentGet(bool $asArray = false) {
 	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);
 			$ok &= in_array($c['name'], $schema);
 		}
 		}
 
 
-		return $ok;
+		return (bool)$ok;
 	}
 	}
 
 
 	public function categoryIsCorrect(): bool {
 	public function categoryIsCorrect(): bool {

+ 6 - 3
app/Models/DatabaseDAOSQLite.php

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

+ 7 - 4
app/Models/Entry.php

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

+ 18 - 22
app/Models/EntryDAO.php

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

+ 18 - 18
app/Models/EntryDAOSQLite.php

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

+ 6 - 6
app/Models/Factory.php

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

+ 57 - 52
app/Models/Feed.php

@@ -71,6 +71,7 @@ class FreshRSS_Feed extends Minz_Model {
 	private $error = false;
 	private $error = false;
 	/** @var int */
 	/** @var int */
 	private $ttl = self::TTL_DEFAULT;
 	private $ttl = self::TTL_DEFAULT;
+	/** @var array<string,mixed> */
 	private $attributes = [];
 	private $attributes = [];
 	/** @var bool */
 	/** @var bool */
 	private $mute = false;
 	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 = new FreshRSS_Feed('http://example.net/', false);
 		$f->faviconPrepare();
 		$f->faviconPrepare();
 		return $f;
 		return $f;
@@ -142,12 +140,16 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->categoryId;
 		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!');
 		Minz_Log::warning(__method__ . ' is deprecated since FreshRSS 1.16.1!');
 		$simplePie = $this->load(false, true);
 		$simplePie = $this->load(false, true);
 		return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie));
 		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 $raw || $this->name != '' ? $this->name : preg_replace('%^https?://(www[.])?%i', '', $this->url);
 	}
 	}
 	/** @return string HTML-encoded URL of the Web site of the feed */
 	/** @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 {
 	public function pathEntries(): string {
 		return $this->pathEntries;
 		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) {
 		if ($raw) {
 			return $this->httpAuth;
 			return $this->httpAuth;
 		} else {
 		} else {
@@ -223,7 +229,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 
 		return $this->nbEntries;
 		return $this->nbEntries;
 	}
 	}
-	public function nbNotRead($includePending = false): int {
+	public function nbNotRead(bool $includePending = false): int {
 		if ($this->nbNotRead < 0) {
 		if ($this->nbNotRead < 0) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$this->nbNotRead = $feedDAO->countNotRead($this->id());
 			$this->nbNotRead = $feedDAO->countNotRead($this->id());
@@ -231,7 +237,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 
 		return $this->nbNotRead + ($includePending ? $this->nbPendingNotRead : 0);
 		return $this->nbNotRead + ($includePending ? $this->nbPendingNotRead : 0);
 	}
 	}
-	public function faviconPrepare() {
+	public function faviconPrepare(): void {
 		require_once(LIB_PATH . '/favicons.php');
 		require_once(LIB_PATH . '/favicons.php');
 		$url = $this->website;
 		$url = $this->website;
 		if ($url == '') {
 		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;
 		$path = DATA_PATH . '/favicons/' . $hash;
 		@unlink($path . '.ico');
 		@unlink($path . '.ico');
 		@unlink($path . '.txt');
 		@unlink($path . '.txt');
@@ -262,10 +268,11 @@ class FreshRSS_Feed extends Minz_Model {
 		return Minz_Url::display('/f.php?' . $this->hash());
 		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 = '';
 		$this->hash = '';
 		$url = $value;
 		$url = $value;
 		if ($validate) {
 		if ($validate) {
@@ -276,26 +283,26 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 		}
 		$this->url = $url;
 		$this->url = $url;
 	}
 	}
-	public function _kind(int $value) {
+
+	public function _kind(int $value): void {
 		$this->kind = $value;
 		$this->kind = $value;
 	}
 	}
 
 
-	/** @param FreshRSS_Category|null $cat */
-	public function _category($cat) {
+	public function _category(?FreshRSS_Category $cat): void {
 		$this->category = $cat;
 		$this->category = $cat;
 		$this->categoryId = $this->category == null ? 0 : $this->category->id();
 		$this->categoryId = $this->category == null ? 0 : $this->category->id();
 	}
 	}
 
 
 	/** @param int|string $id */
 	/** @param int|string $id */
-	public function _categoryId($id) {
+	public function _categoryId($id): void {
 		$this->category = null;
 		$this->category = null;
 		$this->categoryId = intval($id);
 		$this->categoryId = intval($id);
 	}
 	}
 
 
-	public function _name(string $value) {
+	public function _name(string $value): void {
 		$this->name = $value == '' ? '' : trim($value);
 		$this->name = $value == '' ? '' : trim($value);
 	}
 	}
-	public function _website(string $value, bool $validate = true) {
+	public function _website(string $value, bool $validate = true): void {
 		if ($validate) {
 		if ($validate) {
 			$value = checkUrl($value);
 			$value = checkUrl($value);
 		}
 		}
@@ -304,37 +311,37 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 		}
 		$this->website = $value;
 		$this->website = $value;
 	}
 	}
-	public function _description(string $value) {
+	public function _description(string $value): void {
 		$this->description = $value == '' ? '' : $value;
 		$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 */
 	/** @param string $value HTML-encoded CSS selector */
-	public function _pathEntries(string $value) {
+	public function _pathEntries(string $value): void {
 		$this->pathEntries = $value;
 		$this->pathEntries = $value;
 	}
 	}
-	public function _httpAuth(string $value) {
+	public function _httpAuth(string $value): void {
 		$this->httpAuth = $value;
 		$this->httpAuth = $value;
 	}
 	}
-	public function _error($value) {
+	/** @param bool|int $value */
+	public function _error($value): void {
 		$this->error = (bool)$value;
 		$this->error = (bool)$value;
 	}
 	}
-	public function _mute(bool $value) {
+	public function _mute(bool $value): void {
 		$this->mute = $value;
 		$this->mute = $value;
 	}
 	}
-	public function _ttl($value) {
-		$value = intval($value);
+	public function _ttl(int $value): void {
 		$value = min($value, 100000000);
 		$value = min($value, 100000000);
 		$this->ttl = abs($value);
 		$this->ttl = abs($value);
 		$this->mute = $value < self::TTL_DEFAULT;
 		$this->mute = $value < self::TTL_DEFAULT;
 	}
 	}
 
 
 	/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
 	/** @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 ($key == '') {
 			if (is_string($value)) {
 			if (is_string($value)) {
 				$value = json_decode($value, true);
 				$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 != '') {
 		if ($this->url != '') {
 			// @phpstan-ignore-next-line
 			// @phpstan-ignore-next-line
 			if (CACHE_PATH === false) {
 			if (CACHE_PATH === false) {
@@ -440,7 +444,7 @@ class FreshRSS_Feed extends Minz_Model {
 	/**
 	/**
 	 * @return array<string>
 	 * @return array<string>
 	 */
 	 */
-	public function loadGuids(SimplePie $simplePie) {
+	public function loadGuids(SimplePie $simplePie): array {
 		$hasUniqueGuids = true;
 		$hasUniqueGuids = true;
 		$testGuids = [];
 		$testGuids = [];
 		$guids = [];
 		$guids = [];
@@ -474,6 +478,9 @@ class FreshRSS_Feed extends Minz_Model {
 		return $guids;
 		return $guids;
 	}
 	}
 
 
+	/**
+	 * @return iterable<FreshRSS_Entry>
+	 */
 	public function loadEntries(SimplePie $simplePie) {
 	public function loadEntries(SimplePie $simplePie) {
 		$hasBadGuids = $this->attributes('hasBadGuids');
 		$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 == '') {
 		if ($this->url == '') {
 			return null;
 			return null;
 		}
 		}
@@ -708,7 +712,7 @@ class FreshRSS_Feed extends Minz_Model {
 				if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
 				if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
 					// HTML-encoding/escaping of the relevant fields (all except 'content')
 					// HTML-encoding/escaping of the relevant fields (all except 'content')
 					foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) {
 					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]);
 							$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
 	 * 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;
 		$this->nbPendingNotRead += $n;
 	}
 	}
 
 
@@ -770,6 +774,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 
 	/**
 	/**
 	 * Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
 	 * Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+	 * @return int|false
 	 */
 	 */
 	public function cleanOldEntries() {
 	public function cleanOldEntries() {
 		$archiving = $this->attributes('archiving');
 		$archiving = $this->attributes('archiving');
@@ -785,7 +790,6 @@ class FreshRSS_Feed extends Minz_Model {
 			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$nb = $entryDAO->cleanOldEntries($this->id(), $archiving);
 			$nb = $entryDAO->cleanOldEntries($this->id(), $archiving);
 			if ($nb > 0) {
 			if ($nb > 0) {
-				$needFeedCacheRefresh = true;
 				Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving));
 				Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving));
 			}
 			}
 			return $nb;
 			return $nb;
@@ -793,6 +797,7 @@ class FreshRSS_Feed extends Minz_Model {
 		return false;
 		return false;
 	}
 	}
 
 
+	/** @param array<string,mixed> $attributes */
 	public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
 	public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
 		$simplePie = customSimplePie($attributes);
 		$simplePie = customSimplePie($attributes);
 		$filename = $simplePie->get_cache_filename($url);
 		$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;
 		$this->filterActions = $filterActions;
 		if (is_array($this->filterActions) && !empty($this->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();
 					return $af == null ? null : $af->toJSON();
 				}, $this->filterActions));
 				}, $this->filterActions));
 		} else {
 		} else {
@@ -885,10 +890,10 @@ class FreshRSS_Feed extends Minz_Model {
 	/**
 	/**
 	 * @param array<string> $filters
 	 * @param array<string> $filters
 	 */
 	 */
-	public function _filtersAction(string $action, $filters) {
+	public function _filtersAction(string $action, array $filters): void {
 		$action = trim($action);
 		$action = trim($action);
 		if ($action == '' || !is_array($filters)) {
 		if ($action == '' || !is_array($filters)) {
-			return false;
+			return;
 		}
 		}
 		$filters = array_unique(array_map('trim', $filters));
 		$filters = array_unique(array_map('trim', $filters));
 		$filterActions = $this->filterActions();
 		$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
 	 * @param bool|null $muted to include only muted feeds
 	 * @return int|false
 	 * @return int|false
 	 */
 	 */
-	public function deleteFeedByCategory(int $id, $muted = null) {
+	public function deleteFeedByCategory(int $id, ?bool $muted = null) {
 		$sql = 'DELETE FROM `_feed` WHERE category=?';
 		$sql = 'DELETE FROM `_feed` WHERE category=?';
 		if ($muted) {
 		if ($muted) {
 			$sql .= ' AND ttl < 0';
 			$sql .= ' AND ttl < 0';
@@ -338,7 +338,7 @@ SQL;
 	}
 	}
 
 
 	/** @return array<string,string> */
 	/** @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` ';
 		$sql = 'SELECT id_feed, MAX(id) as newest_item_us FROM `_entry` ';
 		if ($id_feed === null) {
 		if ($id_feed === null) {
 			$sql .= 'GROUP BY id_feed';
 			$sql .= 'GROUP BY id_feed';
@@ -358,7 +358,7 @@ SQL;
 	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
 	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
 	 * @return array<FreshRSS_Feed>
 	 * @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();
 		$this->updateTTL();
 		$sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
 		$sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
 			. 'FROM `_feed` '
 			. 'FROM `_feed` '
@@ -398,7 +398,7 @@ SQL;
 	 * @param bool|null $muted to include only muted feeds
 	 * @param bool|null $muted to include only muted feeds
 	 * @return array<FreshRSS_Feed>
 	 * @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=?';
 		$sql = 'SELECT * FROM `_feed` WHERE category=?';
 		if ($muted) {
 		if ($muted) {
 			$sql .= ' AND ttl < 0';
 			$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>
 	 * @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);
 			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());
 			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.
 	 * @param array<string> $transform an array containing a list of functions to apply.
 	 * @return string the transformed data.
 	 * @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)) {
 		if (!is_array($transform) || empty($transform)) {
 			return $data;
 			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}}
 	 * @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(
 		return array(
 			'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true),
 			'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true),
 			'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false),
 			'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false),
@@ -57,7 +57,7 @@ SQL;
 	 * Calculates entry count per day on a 30 days period.
 	 * Calculates entry count per day on a 30 days period.
 	 * @return array<int,int>
 	 * @return array<int,int>
 	 */
 	 */
-	public function calculateEntryCount() {
+	public function calculateEntryCount(): array {
 		$count = $this->initEntryCountArray();
 		$count = $this->initEntryCountArray();
 		$midnight = mktime(0, 0, 0);
 		$midnight = mktime(0, 0, 0);
 		$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
 		$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
@@ -87,7 +87,7 @@ SQL;
 	 * Initialize an array for the entry count.
 	 * Initialize an array for the entry count.
 	 * @return array<int,int>
 	 * @return array<int,int>
 	 */
 	 */
-	protected function initEntryCountArray() {
+	protected function initEntryCountArray(): array {
 		return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
 		return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
 	}
 	}
 
 
@@ -348,8 +348,8 @@ SQL;
 	 * @param array<string> $data
 	 * @param array<string> $data
 	 * @return array<string>
 	 * @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);
 			return _t('gen.date.' . $a);
 		}, $data);
 		}, $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
 	 * Calculates the number of article per hour of the day per feed
 	 *
 	 *
-	 * @param integer $feed id
+	 * @param int $feed id
 	 * @return array<int,int>
 	 * @return array<int,int>
 	 */
 	 */
 	public function calculateEntryRepartitionPerFeedPerHour(?int $feed = null): array {
 	public function calculateEntryRepartitionPerFeedPerHour(?int $feed = null): array {
@@ -48,6 +48,9 @@ ORDER BY period ASC
 SQL;
 SQL;
 
 
 		$stm = $this->pdo->query($sql);
 		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return [];
+		}
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 
 		switch ($period) {
 		switch ($period) {

+ 3 - 0
app/Models/StatsDAOSQLite.php

@@ -25,6 +25,9 @@ ORDER BY period ASC
 SQL;
 SQL;
 
 
 		$stm = $this->pdo->query($sql);
 		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return [];
+		}
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 
 		switch ($period) {
 		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 {
 	public function _nbUnread($value): void {
 		$this->nbUnread = (int)$value;
 		$this->nbUnread = (int)$value;

+ 2 - 2
app/Models/TagDAO.php

@@ -245,7 +245,7 @@ SQL;
 	}
 	}
 
 
 	/** @return array<string,string> */
 	/** @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 '
 		$sql = 'SELECT t.id AS id_tag, MAX(e.id) AS newest_item_us '
 			 . 'FROM `_tag` t '
 			 . 'FROM `_tag` t '
 			 . 'LEFT OUTER JOIN `_entrytag` et ON et.id_tag = t.id '
 			 . '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
 	 * @param array<array<string,string|int>>|array<string,string|int> $listDAO
 	 * @return array<FreshRSS_Tag>
 	 * @return array<FreshRSS_Tag>
 	 */
 	 */
-	private static function daoToTag(array $listDAO) {
+	private static function daoToTag(array $listDAO): array {
 		$list = array();
 		$list = array();
 		if (!is_array($listDAO)) {
 		if (!is_array($listDAO)) {
 			$listDAO = 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 {
 	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			$showCreate = $tableInfo->fetchColumn();
-			if (stripos($showCreate, 'tag') === false) {
+			if (is_string($showCreate) && stripos($showCreate, 'tag') === false) {
 				return $this->createTagTable();	//v1.12.0
 				return $this->createTagTable();	//v1.12.0
 			}
 			}
 		}
 		}

+ 1 - 1
app/Models/UserQuery.php

@@ -34,7 +34,7 @@ class FreshRSS_UserQuery {
 	private $tag_dao;
 	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) {
 	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;
 		$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;
 	public $show_email_field;
 	/** @var string */
 	/** @var string */
 	public $username;
 	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;
 	public $users;
 
 
 	// Updates
 	// Updates
@@ -73,7 +73,7 @@ class FreshRSS_View extends Minz_View {
 	public $status_database;
 	public $status_database;
 
 
 	// Archiving
 	// Archiving
-	/** @var int|false */
+	/** @var int */
 	public $nb_total;
 	public $nb_total;
 	/** @var int */
 	/** @var int */
 	public $size_total;
 	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.
 	 * 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.
 	 * @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.
 	 *                    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.
 	 * 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->catDAO = FreshRSS_Factory::createCategoryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 	}
 	}
@@ -33,9 +31,9 @@ class FreshRSS_Import_Service {
 	 *
 	 *
 	 * @param string $opml_file the OPML file content.
 	 * @param string $opml_file the OPML file content.
 	 * @param FreshRSS_Category|null $forced_category force the feeds to be associated to this category.
 	 * @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);
 		@set_time_limit(300);
 		$this->lastStatus = true;
 		$this->lastStatus = true;
 		$opml_array = array();
 		$opml_array = array();
@@ -132,20 +130,17 @@ class FreshRSS_Import_Service {
 				}
 				}
 			}
 			}
 		}
 		}
-
-		return;
 	}
 	}
 
 
 	/**
 	/**
 	 * Create a feed from a feed element (i.e. OPML outline).
 	 * 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 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.
 	 * @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']);
 		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
 		$name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
 		$name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
@@ -256,12 +251,11 @@ class FreshRSS_Import_Service {
 	/**
 	/**
 	 * Create and return a category.
 	 * 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.
 	 * @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 = $category_element['text'] ?? $category_element['title'] ?? '';
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
 		$name = Minz_Helper::htmlspecialchars_utf8($name);
 		$category = new FreshRSS_Category($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
 	 * This method is applied to a list of outlines. It merges the different
 	 * list of feeds from several outlines into one array.
 	 * list of feeds from several outlines into one array.
 	 *
 	 *
-	 * @param array $outlines
+	 * @param array<mixed> $outlines
 	 *     The outlines from which to extract the outlines.
 	 *     The outlines from which to extract the outlines.
 	 * @param string $parent_category_name
 	 * @param string $parent_category_name
 	 *     The name of the parent category of the current outlines.
 	 *     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_elements = [];
 		$categories_to_feeds = [];
 		$categories_to_feeds = [];
 
 
@@ -342,14 +335,14 @@ class FreshRSS_Import_Service {
 	 * exists), it will add the outline to an array accessible by its category
 	 * exists), it will add the outline to an array accessible by its category
 	 * name.
 	 * name.
 	 *
 	 *
-	 * @param array $outline
+	 * @param array<mixed> $outline
 	 *     The outline from which to extract the categories and feeds outlines.
 	 *     The outline from which to extract the categories and feeds outlines.
 	 * @param string $parent_category_name
 	 * @param string $parent_category_name
 	 *     The name of the parent category of the current outline.
 	 *     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_elements = [];
 		$categories_to_feeds = [];
 		$categories_to_feeds = [];
 
 
@@ -396,7 +389,7 @@ class FreshRSS_Import_Service {
 		return [$categories_elements, $categories_to_feeds];
 		return [$categories_elements, $categories_to_feeds];
 	}
 	}
 
 
-	private static function log($message) {
+	private static function log(string $message): void {
 		if (FreshRSS_Context::$isCli) {
 		if (FreshRSS_Context::$isCli) {
 			fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
 			fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
 		} else {
 		} else {

+ 1 - 1
app/Utils/feverUtil.php

@@ -57,7 +57,7 @@ class FreshRSS_fever_Util {
 	 *
 	 *
 	 * @return bool true if the deletion succeeded, else false.
 	 * @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);
 		$userConfig = get_user_configuration($username);
 		if ($userConfig === null) {
 		if ($userConfig === null) {
 			return false;
 			return false;

+ 3 - 3
app/actualize_script.php

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

+ 48 - 40
app/install.php

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

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

@@ -11,7 +11,7 @@ $articles = array(
 	'items' => 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;
 $first = true;
 
 
 if (empty($this->entryIdsTagNames)) {
 if (empty($this->entryIdsTagNames)) {

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

@@ -4,7 +4,7 @@
  * @param array<FreshRSS_Feed> $feeds
  * @param array<FreshRSS_Feed> $feeds
  * @return array<array<string,string|null>>
  * @return array<array<string,string|null>>
  */
  */
-function feedsToOutlines($feeds, bool $excludeMutedFeeds = false): array {
+function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
 	$outlines = [];
 	$outlines = [];
 	foreach ($feeds as $feed) {
 	foreach ($feeds as $feed) {
 		if ($feed->mute() && $excludeMutedFeeds) {
 		if ($feed->mute() && $excludeMutedFeeds) {
@@ -98,4 +98,6 @@ if (!empty($this->feeds)) {
 }
 }
 
 
 $libopml = new \marienfressinaud\LibOpml\LibOpml(true);
 $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')),
 		'unread' => rawurlencode(_i('unread')),
 	),
 	),
 	'extensions' => $extData,
 	'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 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">
 	<div id="loglist-wrapper" class="table-wrapper">
 		<table id="loglist">
 		<table id="loglist">
 			<thead>
 			<thead>
@@ -41,7 +41,7 @@
 		</tbody>
 		</tbody>
 		</table>
 		</table>
 	</div>
 	</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
 		// We most likely already have the feed object in cache
 		$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId());
 		$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId());
 		if ($this->feed == null) {
 		if ($this->feed == null) {
-			$this->feed = $this->entry->feed();
+			$this->feed = $this->entry->feed() ?: null;
 			if ($this->feed == null) {
 			if ($this->feed == null) {
 				$this->feed = FreshRSS_Feed::example();
 				$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 ?>">
 					<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>
 						<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; ?>
 						<?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>
 							(<?= _t('admin.stats.number_entries', $feedInPeriod['nb_articles']) ?>)</span>
 					</li>
 					</li>
 				<?php } ?>
 				<?php } ?>

+ 2 - 2
cli/_cli.php

@@ -77,10 +77,10 @@ function performRequirementCheck(string $databaseType): void {
  * @return array<string>
  * @return array<string>
  */
  */
 function getLongOptions(array $options, string $regex): array {
 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 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);
 		return preg_replace($regex, '', $a);
 	}, $longOptions);
 	}, $longOptions);
 }
 }

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

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

+ 2 - 2
cli/actualize-user.php

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

+ 2 - 2
cli/db-optimize.php

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

+ 2 - 2
cli/delete-user.php

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

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

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

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

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

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

@@ -2,7 +2,7 @@
 <?php
 <?php
 require(__DIR__ . '/_cli.php');
 require(__DIR__ . '/_cli.php');
 
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 
 $params = array(
 $params = array(
 	'user:',
 	'user:',
@@ -11,7 +11,7 @@ $params = array(
 
 
 $options = getopt('', $params);
 $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");
 	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'.
 	 * the parent key is 'a.b.c.d'.
 	 */
 	 */
 	private function getParentKey(string $key): string {
 	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)]);
 		$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) {
 			if ($element === $key) {
 				return false;
 				return false;
 			}
 			}

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

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

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

@@ -2,7 +2,7 @@
 <?php
 <?php
 require(__DIR__ . '/_cli.php');
 require(__DIR__ . '/_cli.php');
 
 
-performRequirementCheck(FreshRSS_Context::$system_conf->db['type']);
+performRequirementCheck(FreshRSS_Context::$system_conf->db['type'] ?? '');
 
 
 $params = [
 $params = [
 	'user:',
 	'user:',
@@ -12,7 +12,7 @@ $params = [
 
 
 $options = getopt('', $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');
 	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 ? '*' : '',
 		'enabled' => FreshRSS_Context::$user_conf->enabled ? '*' : '',
 		'last_user_activity' => FreshRSS_UserDAO::mtime($username),
 		'last_user_activity' => FreshRSS_UserDAO::mtime($username),
 		'database_size' => $databaseDAO->size(),
 		'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,
 		'lang' => FreshRSS_Context::$user_conf->language,
 		'mail_login' => FreshRSS_Context::$user_conf->mail_login,
 		'mail_login' => FreshRSS_Context::$user_conf->mail_login,
 	);
 	);

+ 8 - 2
composer.json

@@ -49,7 +49,8 @@
         "ext-phar": "*",
         "ext-phar": "*",
         "ext-tokenizer": "*",
         "ext-tokenizer": "*",
         "ext-xmlwriter": "*",
         "ext-xmlwriter": "*",
-        "phpstan/phpstan": "~1.9.17",
+        "phpstan/phpstan": "~1.10.13",
+        "phpstan/phpstan-phpunit": "^1.3",
         "phpunit/phpunit": "^9",
         "phpunit/phpunit": "^9",
         "squizlabs/php_codesniffer": "^3.7"
         "squizlabs/php_codesniffer": "^3.7"
     },
     },
@@ -59,7 +60,7 @@
         "phpcs": "phpcs . -s",
         "phpcs": "phpcs . -s",
         "phpcbf": "phpcbf . -p -s",
         "phpcbf": "phpcbf . -p -s",
         "phpstan": "phpstan analyse --memory-limit 512M .",
         "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",
         "phpunit": "phpunit --bootstrap ./tests/bootstrap.php --verbose ./tests",
         "translations": "cli/manipulate.translation.php -a format",
         "translations": "cli/manipulate.translation.php -a format",
         "test": [
         "test": [
@@ -74,5 +75,10 @@
             "@translations",
             "@translations",
             "@phpcbf"
             "@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",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "4dcbcd3ba9c1dbed63612651a725dab8",
+    "content-hash": "194c10da954d3f120fef1e0b21c34546",
     "packages": [],
     "packages": [],
     "packages-dev": [
     "packages-dev": [
         {
         {
             "name": "doctrine/instantiator",
             "name": "doctrine/instantiator",
-            "version": "1.5.0",
+            "version": "2.0.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+                "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
-                "php": "^7.1 || ^8.0"
+                "php": "^8.1"
             },
             },
             "require-dev": {
             "require-dev": {
-                "doctrine/coding-standard": "^9 || ^11",
+                "doctrine/coding-standard": "^11",
                 "ext-pdo": "*",
                 "ext-pdo": "*",
                 "ext-phar": "*",
                 "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",
             "type": "library",
             "autoload": {
             "autoload": {
@@ -59,7 +59,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/doctrine/instantiator/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -75,20 +75,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2022-12-30T00:15:36+00:00"
+            "time": "2022-12-30T00:23:10+00:00"
         },
         },
         {
         {
             "name": "myclabs/deep-copy",
             "name": "myclabs/deep-copy",
-            "version": "1.11.0",
+            "version": "1.11.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
+                "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -126,7 +126,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/myclabs/DeepCopy/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -134,20 +134,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2022-03-03T13:19:32+00:00"
+            "time": "2023-03-08T13:26:56+00:00"
         },
         },
         {
         {
             "name": "nikic/php-parser",
             "name": "nikic/php-parser",
-            "version": "v4.15.2",
+            "version": "v4.15.4",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
+                "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -188,9 +188,9 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
                 "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",
             "name": "phar-io/manifest",
@@ -305,16 +305,16 @@
         },
         },
         {
         {
             "name": "phpstan/phpstan",
             "name": "phpstan/phpstan",
-            "version": "1.9.17",
+            "version": "1.10.13",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2"
+                "reference": "f07bf8c6980b81bf9e49d44bd0caf2e737614a70"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -343,8 +343,11 @@
                 "static analysis"
                 "static analysis"
             ],
             ],
             "support": {
             "support": {
+                "docs": "https://phpstan.org/user-guide/getting-started",
+                "forum": "https://github.com/phpstan/phpstan/discussions",
                 "issues": "https://github.com/phpstan/phpstan/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -360,27 +363,79 @@
                     "type": "tidelift"
                     "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",
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.23",
+            "version": "9.2.26",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c"
+                "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "ext-dom": "*",
                 "ext-dom": "*",
                 "ext-libxml": "*",
                 "ext-libxml": "*",
                 "ext-xmlwriter": "*",
                 "ext-xmlwriter": "*",
-                "nikic/php-parser": "^4.14",
+                "nikic/php-parser": "^4.15",
                 "php": ">=7.3",
                 "php": ">=7.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-text-template": "^2.0.2",
                 "phpunit/php-text-template": "^2.0.2",
@@ -395,8 +450,8 @@
                 "phpunit/phpunit": "^9.3"
                 "phpunit/phpunit": "^9.3"
             },
             },
             "suggest": {
             "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",
             "type": "library",
             "extra": {
             "extra": {
@@ -429,7 +484,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -437,7 +492,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-12-28T12:41:10+00:00"
+            "time": "2023-03-06T12:58:08+00:00"
         },
         },
         {
         {
             "name": "phpunit/php-file-iterator",
             "name": "phpunit/php-file-iterator",
@@ -682,20 +737,20 @@
         },
         },
         {
         {
             "name": "phpunit/phpunit",
             "name": "phpunit/phpunit",
-            "version": "9.5.27",
+            "version": "9.6.7",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38"
+                "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
-                "doctrine/instantiator": "^1.3.1",
+                "doctrine/instantiator": "^1.3.1 || ^2",
                 "ext-dom": "*",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-libxml": "*",
@@ -724,8 +779,8 @@
                 "sebastian/version": "^3.0.2"
                 "sebastian/version": "^3.0.2"
             },
             },
             "suggest": {
             "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": [
             "bin": [
                 "phpunit"
                 "phpunit"
@@ -733,7 +788,7 @@
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "branch-alias": {
                 "branch-alias": {
-                    "dev-master": "9.5-dev"
+                    "dev-master": "9.6-dev"
                 }
                 }
             },
             },
             "autoload": {
             "autoload": {
@@ -764,7 +819,8 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -780,7 +836,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2022-12-09T07:31:23+00:00"
+            "time": "2023-04-14T08:58:40+00:00"
         },
         },
         {
         {
             "name": "sebastian/cli-parser",
             "name": "sebastian/cli-parser",
@@ -1148,16 +1204,16 @@
         },
         },
         {
         {
             "name": "sebastian/environment",
             "name": "sebastian/environment",
-            "version": "5.1.4",
+            "version": "5.1.5",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
+                "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1199,7 +1255,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/environment/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -1207,7 +1263,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-04-03T09:37:03+00:00"
+            "time": "2023-02-03T06:03:51+00:00"
         },
         },
         {
         {
             "name": "sebastian/exporter",
             "name": "sebastian/exporter",
@@ -1521,16 +1577,16 @@
         },
         },
         {
         {
             "name": "sebastian/recursion-context",
             "name": "sebastian/recursion-context",
-            "version": "4.0.4",
+            "version": "4.0.5",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+                "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1569,10 +1625,10 @@
                 }
                 }
             ],
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "description": "Provides functionality to recursively process PHP variables",
-            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "homepage": "https://github.com/sebastianbergmann/recursion-context",
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -1580,7 +1636,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2020-10-26T13:17:30+00:00"
+            "time": "2023-02-03T06:07:39+00:00"
         },
         },
         {
         {
             "name": "sebastian/resource-operations",
             "name": "sebastian/resource-operations",
@@ -1639,16 +1695,16 @@
         },
         },
         {
         {
             "name": "sebastian/type",
             "name": "sebastian/type",
-            "version": "3.2.0",
+            "version": "3.2.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/type.git",
                 "url": "https://github.com/sebastianbergmann/type.git",
-                "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e"
+                "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1683,7 +1739,7 @@
             "homepage": "https://github.com/sebastianbergmann/type",
             "homepage": "https://github.com/sebastianbergmann/type",
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/type/issues",
                 "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": [
             "funding": [
                 {
                 {
@@ -1691,7 +1747,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-09-12T14:47:03+00:00"
+            "time": "2023-02-03T06:13:03+00:00"
         },
         },
         {
         {
             "name": "sebastian/version",
             "name": "sebastian/version",
@@ -1748,16 +1804,16 @@
         },
         },
         {
         {
             "name": "squizlabs/php_codesniffer",
             "name": "squizlabs/php_codesniffer",
-            "version": "3.7.1",
+            "version": "3.7.2",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
+                "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1793,14 +1849,15 @@
             "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
             "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
             "keywords": [
             "keywords": [
                 "phpcs",
                 "phpcs",
-                "standards"
+                "standards",
+                "static analysis"
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
                 "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
                 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
                 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
                 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
                 "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",
             "name": "theseer/tokenizer",
@@ -1867,9 +1924,11 @@
         "ext-gmp": "*",
         "ext-gmp": "*",
         "ext-intl": "*",
         "ext-intl": "*",
         "ext-json": "*",
         "ext-json": "*",
+        "ext-libxml": "*",
         "ext-mbstring": "*",
         "ext-mbstring": "*",
         "ext-openssl": "*",
         "ext-openssl": "*",
         "ext-pcre": "*",
         "ext-pcre": "*",
+        "ext-pdo": "*",
         "ext-pdo_sqlite": "*",
         "ext-pdo_sqlite": "*",
         "ext-session": "*",
         "ext-session": "*",
         "ext-simplexml": "*",
         "ext-simplexml": "*",
@@ -1885,5 +1944,5 @@
         "ext-tokenizer": "*",
         "ext-tokenizer": "*",
         "ext-xmlwriter": "*"
         "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.
  * Manage configuration for the application.
  * @property-read string $base_url
  * @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 $disable_update
  * @property-read string $environment
  * @property-read string $environment
  * @property array<string,bool> $extensions_enabled
  * @property array<string,bool> $extensions_enabled
@@ -24,9 +25,10 @@ class Minz_Configuration {
 	 * @param string $namespace the name of the current configuration
 	 * @param string $namespace the name of the current configuration
 	 * @param string $config_filename the filename of the configuration
 	 * @param string $config_filename the filename of the configuration
 	 * @param string $default_filename a filename containing default values for 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(
 		self::$config_list[$namespace] = new static(
 			$namespace, $config_filename, $default_filename, $configuration_setter
 			$namespace, $config_filename, $default_filename, $configuration_setter
 		);
 		);
@@ -92,9 +94,9 @@ class Minz_Configuration {
 
 
 	/**
 	/**
 	 * An object which help to set good values in 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.
 	 * Create a new Minz_Configuration object.
@@ -102,9 +104,10 @@ class Minz_Configuration {
 	 * @param string $namespace the name of the current configuration.
 	 * @param string $namespace the name of the current configuration.
 	 * @param string $config_filename the file containing configuration values.
 	 * @param string $config_filename the file containing configuration values.
 	 * @param string $default_filename the file containing default values, null by default.
 	 * @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->namespace = $namespace;
 		$this->config_filename = $config_filename;
 		$this->config_filename = $config_filename;
 		$this->default_filename = $default_filename;
 		$this->default_filename = $default_filename;
@@ -127,16 +130,15 @@ class Minz_Configuration {
 
 
 	/**
 	/**
 	 * Set a configuration setter for the current 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'))) {
 		if (is_callable(array($configuration_setter, 'handle'))) {
 			$this->configuration_setter = $configuration_setter;
 			$this->configuration_setter = $configuration_setter;
 		}
 		}
 	}
 	}
 
 
-	public function configurationSetter(): object {
+	public function configurationSetter(): ?Minz_ConfigurationSetterInterface {
 		return $this->configuration_setter;
 		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.
 	 * @param mixed $value the value to set. If null, the key is removed from the configuration.
 	 */
 	 */
 	public function _param(string $key, $value = null): void {
 	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);
 			$this->configuration_setter->handle($this->data, $key, $value);
 		} elseif (isset($this->data[$key]) && is_null($value)) {
 		} elseif (isset($this->data[$key]) && is_null($value)) {
 			unset($this->data[$key]);
 			unset($this->data[$key]);
-		} elseif (!is_null($value)) {
+		} elseif ($value !== null) {
 			$this->data[$key] = $value;
 			$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';
 			$controller_name = 'FreshRSS_' . $base_name . '_Controller';
 		}
 		}
 
 
-		if (!class_exists ($controller_name)) {
+		if (!class_exists($controller_name)) {
 			throw new Minz_ControllerNotExistException (
 			throw new Minz_ControllerNotExistException (
 				Minz_Exception::ERROR
 				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 (
 			throw new Minz_ControllerNotActionControllerException (
 				$controller_name,
 				$controller_name,
 				Minz_Exception::ERROR
 				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
 	 * @throws Minz_ActionException if the action cannot be executed on the controller
 	 */
 	 */
 	private function launchAction(string $action_name): void {
 	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 (
 			throw new Minz_ActionException (
-				get_class ($this->controller),
+				get_class($this->controller),
 				$action_name,
 				$action_name,
 				Minz_Exception::ERROR
 				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.
 	 * Return if a controller is registered.
 	 *
 	 *
 	 * @param string $base_name the base name of the controller.
 	 * @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]);
 		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)
 	 * @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)
 	 * @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');
 		$conf = Minz_Configuration::get('system');
 		$env = $conf->environment;
 		$env = $conf->environment;
 		$logs_ok = array ();
 		$logs_ok = array ();

+ 4 - 4
lib/Minz/Extension.php

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

+ 1 - 1
lib/Minz/ExtensionManager.php

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

+ 3 - 3
lib/Minz/FrontController.php

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

+ 2 - 3
lib/Minz/Mailer.php

@@ -44,7 +44,7 @@ class Minz_Mailer {
 	 */
 	 */
 	public function __construct () {
 	public function __construct () {
 		$this->view = new Minz_View();
 		$this->view = new Minz_View();
-		$this->view->_layout(false);
+		$this->view->_layout(null);
 		$this->view->attributeParams();
 		$this->view->attributeParams();
 
 
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
@@ -66,10 +66,9 @@ class Minz_Mailer {
 	 *
 	 *
 	 * @param string $to The recipient of the email
 	 * @param string $to The recipient of the email
 	 * @param string $subject The subject of the email
 	 * @param string $subject The subject of the email
-	 *
 	 * @return bool true on success, false if a SMTP error happens
 	 * @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();
 		ob_start();
 		$this->view->render();
 		$this->view->render();
 		$body = ob_get_contents();
 		$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));
 		$applied_migrations = array_filter(explode("\n", $applied_migrations));
 
 
 		$migration_files = scandir($migrations_path);
 		$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);
 			$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
 			return $file_extension === 'php';
 			return $file_extension === 'php';
 		});
 		});
-		$migration_versions = array_map(function ($filename) {
+		$migration_versions = array_map(static function (string $filename) {
 			return basename($filename, '.php');
 			return basename($filename, '.php');
 		}, $migration_files);
 		}, $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 {
 	public function upToDate(): bool {
 		// Counting versions is enough since we cannot apply a version which
 		// 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
 	 * @param string $filename le nom du fichier à ouvrir contenant un tableau
 	 * Remarque : $array sera obligatoirement un tableau
 	 * Remarque : $array sera obligatoirement un tableau
 	 */
 	 */
-	public function __construct ($filename) {
+	public function __construct(string $filename) {
 		$this->filename = $filename;
 		$this->filename = $filename;
 	}
 	}
 
 

+ 1 - 1
lib/Minz/ModelPdo.php

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

+ 28 - 30
lib/Minz/Paginator.php

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

+ 2 - 2
lib/Minz/Request.php

@@ -298,7 +298,7 @@ class Minz_Request {
 	 * localhost address.
 	 * localhost address.
 	 *
 	 *
 	 * @param string $address the address to test, can be an IP or a URL.
 	 * @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?)
 	 * @todo improve test with a more valid technique (e.g. test with an external server?)
 	 */
 	 */
 	public static function serverIsPublic(string $address): bool {
 	public static function serverIsPublic(string $address): bool {
@@ -360,7 +360,7 @@ class Minz_Request {
 		$requests = Minz_Session::param('requests');
 		$requests = Minz_Session::param('requests');
 		if ($requests) {
 		if ($requests) {
 			//Delete abandoned notifications
 			//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();
 			$requestId = self::requestId();
 			if (!empty($requests[$requestId]['notification'])) {
 			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(
 		$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
 		// 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
 		// If $translates[$top_level] is null it means we have to load the
 		// corresponding files.
 		// 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);
 			$res = self::loadKey($top_level);
 			if (!$res) {
 			if (!$res) {
 				return $key;
 				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)
 	 * @param string $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @return string uri sous la forme ?key=value&key2=value2
 	 * @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 = '';
 		$uri = '';
 		$separator = '?';
 		$separator = '?';
 		$anchor = '';
 		$anchor = '';
@@ -108,23 +108,15 @@ class Minz_Url {
 
 
 	/**
 	/**
 	 * Check that all array elements representing the controller URL are OK
 	 * 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
 	 * @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 */
 	/** @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 {
 	public static function unserialize(string $url = ''): array {
 		try {
 		try {
 			return json_decode(base64_decode($url), true, JSON_THROW_ON_ERROR) ?? [];
 			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
 	 * 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 {
 	public static function build(): array {
 		$url = [
 		$url = [

+ 5 - 6
lib/Minz/View.php

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

+ 2 - 2
lib/lib_date.php

@@ -34,7 +34,7 @@ example('PT6M/');
 example('PT7S/');
 example('PT7S/');
 example('P1DT1H/');
 example('P1DT1H/');
 
 
-function example($dateInterval) {
+function example(string $dateInterval) {
 	$dateIntervalArray = parseDateInterval($dateInterval);
 	$dateIntervalArray = parseDateInterval($dateInterval);
 	echo $dateInterval, "\t=>\t",
 	echo $dateInterval, "\t=>\t",
 		$dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
 		$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,
  * @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.
  *  or null if open interval, or false if error.
  */
  */
-function parseDateInterval(string $dateInterval) {
+function parseDateInterval(string $dateInterval): array {
 	$dateInterval = trim($dateInterval);
 	$dateInterval = trim($dateInterval);
 	$dateInterval = str_replace('--', '/', $dateInterval);
 	$dateInterval = str_replace('--', '/', $dateInterval);
 	$dateInterval = strtoupper($dateInterval);
 	$dateInterval = strtoupper($dateInterval);

+ 8 - 4
lib/lib_install.php

@@ -1,7 +1,7 @@
 <?php
 <?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> */
 /** @return array<string,string> */
 function checkRequirements(string $dbType = ''): array {
 function checkRequirements(string $dbType = ''): array {
@@ -76,7 +76,7 @@ function checkRequirements(string $dbType = ''): array {
 }
 }
 
 
 function generateSalt(): string {
 function generateSalt(): string {
-	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__)));
+	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__) ?: []));
 }
 }
 
 
 function initDb(): string {
 function initDb(): string {
@@ -88,10 +88,14 @@ function initDb(): string {
 	$db['pdo_options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
 	$db['pdo_options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
 	$conf->db = $db;	//TODO: Remove this Minz limitation "Indirect modification of overloaded property"
 	$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
 	//Attempt to auto-create database if it does not already exist
 	if ($db['type'] !== 'sqlite') {
 	if ($db['type'] !== 'sqlite') {
 		Minz_ModelPdo::$usesSharedPdo = false;
 		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:
 		//For first connection, use default database for PostgreSQL, empty database for MySQL / MariaDB:
 		$db['base'] = $db['type'] === 'pgsql' ? 'postgres' : '';
 		$db['base'] = $db['type'] === 'pgsql' ? 'postgres' : '';
 		$conf->db = $db;
 		$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) ?: '';
 	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);
 	$text = htmlspecialchars_decode($text, ENT_QUOTES);
 
 
 	//Problematic characters
 	//Problematic characters
@@ -176,7 +167,8 @@ function escapeToUnicodeAlternative($text, $extended = true) {
 	return trim(str_replace($problem, $replace, $text));
 	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
 	// number_format does not seem to be Unicode-compatible
 	return str_replace(' ', ' ',	// Thin non-breaking space
 	return str_replace(' ', ' ',	// Thin non-breaking space
 		number_format($n, $precision, '.', ' ')
 		number_format($n, $precision, '.', ' ')
@@ -255,7 +247,7 @@ function sensitive_log($log) {
 /**
 /**
  * @param array<string,mixed> $attributes
  * @param array<string,mixed> $attributes
  */
  */
-function customSimplePie($attributes = array()): SimplePie {
+function customSimplePie(array $attributes = array()): SimplePie {
 	if (FreshRSS_Context::$system_conf === null) {
 	if (FreshRSS_Context::$system_conf === null) {
 		throw new FreshRSS_Context_Exception('System configuration not initialised!');
 		throw new FreshRSS_Context_Exception('System configuration not initialised!');
 	}
 	}
@@ -339,10 +331,8 @@ function customSimplePie($attributes = array()): SimplePie {
 	return $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)) {
 	if (!is_string($data) || ($maxLength !== null && $maxLength <= 0)) {
 		return '';
 		return '';
 	}
 	}
@@ -579,7 +569,7 @@ function listUsers(): array {
  * Return if the maximum number of registrations has been reached.
  * Return if the maximum number of registrations has been reached.
  * Note a max_registrations of 0 means there is no limit.
  * 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 {
 function max_registrations_reached(): bool {
 	if (FreshRSS_Context::$system_conf === null) {
 	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.
  * @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.
  * @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)) {
 	if (!FreshRSS_user_Controller::checkUsername($username)) {
 		return null;
 		return null;
 	}
 	}
 	$namespace = 'user_' . $username;
 	$namespace = 'user_' . $username;
 	try {
 	try {
-		Minz_Configuration::register($namespace,
+		FreshRSS_UserConfiguration::register($namespace,
 			USERS_PATH . '/' . $username . '/config.php',
 			USERS_PATH . '/' . $username . '/config.php',
 			FRESHRSS_PATH . '/config-user.default.php');
 			FRESHRSS_PATH . '/config-user.default.php');
 	} catch (Minz_ConfigurationNamespaceException $e) {
 	} catch (Minz_ConfigurationNamespaceException $e) {
@@ -618,10 +608,7 @@ function get_user_configuration(string $username) {
 		return null;
 		return null;
 	}
 	}
 
 
-	/**
-	 * @var FreshRSS_UserConfiguration $user_conf
-	 */
-	$user_conf = Minz_Configuration::get($namespace);
+	$user_conf = FreshRSS_UserConfiguration::get($namespace);
 	return $user_conf;
 	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 $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)
  * @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 {
 function checkCIDR(string $ip, string $range): bool {
 	$binary_ip = ipToBits($ip);
 	$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
  * 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
  * 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 {
 function checkTrustedIP(): bool {
 	if (FreshRSS_Context::$system_conf === null) {
 	if (FreshRSS_Context::$system_conf === null) {
@@ -840,7 +827,7 @@ const SHORTCUT_KEYS = [
 function getNonStandardShortcuts(array $shortcuts): array {
 function getNonStandardShortcuts(array $shortcuts): array {
 	$standard = strtolower(implode(' ', SHORTCUT_KEYS));
 	$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);
 		$shortcut = trim($shortcut);
 		return $shortcut !== '' & stripos($standard, $shortcut) === false;
 		return $shortcut !== '' & stripos($standard, $shortcut) === false;
 	});
 	});

+ 7 - 8
p/api/fever.php

@@ -336,9 +336,8 @@ final class FeverAPI
 		$groups = array();
 		$groups = array();
 
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
-		$categories = $categoryDAO->listCategories(false, false);
+		$categories = $categoryDAO->listCategories(false, false) ?: [];
 
 
-		/** @var FreshRSS_Category $category */
 		foreach ($categories as $category) {
 		foreach ($categories as $category) {
 			$groups[] = array(
 			$groups[] = array(
 				'id' => $category->id(),
 				'id' => $category->id(),
@@ -430,28 +429,28 @@ final class FeverAPI
 	}
 	}
 
 
 	/**
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	 */
 	private function setItemAsRead(string $id) {
 	private function setItemAsRead(string $id) {
 		return $this->entryDAO->markRead($id, true);
 		return $this->entryDAO->markRead($id, true);
 	}
 	}
 
 
 	/**
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	 */
 	private function setItemAsUnread(string $id) {
 	private function setItemAsUnread(string $id) {
 		return $this->entryDAO->markRead($id, false);
 		return $this->entryDAO->markRead($id, false);
 	}
 	}
 
 
 	/**
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	 */
 	private function setItemAsSaved(string $id) {
 	private function setItemAsSaved(string $id) {
 		return $this->entryDAO->markFavorite($id, true);
 		return $this->entryDAO->markFavorite($id, true);
 	}
 	}
 
 
 	/**
 	/**
-	 * @return integer|false
+	 * @return int|false
 	 */
 	 */
 	private function setItemAsUnsaved(string $id) {
 	private function setItemAsUnsaved(string $id) {
 		return $this->entryDAO->markFavorite($id, false);
 		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) {
 	private function setFeedAsRead(int $id, int $before) {
 		$before = $this->convertBeforeToId($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) {
 	private function setGroupAsRead(int $id, int $before) {
 		$before = $this->convertBeforeToId($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');
 require(LIB_PATH . '/http-conditional.php');
 
 
 function show_default_favicon(int $cacheSeconds = 3600): void {
 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)) {
 	if (!httpConditional($default_mtime, $cacheSeconds, 2)) {
 		header('Content-Type: image/x-icon');
 		header('Content-Type: image/x-icon');
 		header('Content-Disposition: inline; filename="default_favicon.ico"');
 		header('Content-Disposition: inline; filename="default_favicon.ico"');
@@ -21,8 +21,8 @@ if (!ctype_xdigit($id)) {
 $txt = FAVICONS_DIR . $id . '.txt';
 $txt = FAVICONS_DIR . $id . '.txt';
 $ico = FAVICONS_DIR . $id . '.ico';
 $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 ($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (mt_rand(15, 20) * 86400))) {
 	if ($txt_mtime == false) {
 	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');
 		require(LIB_PATH . '/http-conditional.php');
 		$currentUser = Minz_User::name();
 		$currentUser = Minz_User::name();
 		$dateLastModification = $currentUser === null ? time() : max(
 		$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)) {
 		if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) {
 			Minz_Session::init('FreshRSS');
 			Minz_Session::init('FreshRSS');

+ 7 - 4
phpstan.neon

@@ -1,6 +1,7 @@
 parameters:
 parameters:
 	# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
 	# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
-	level: 5
+	level: 6
+	treatPhpDocTypesAsCertain: false
 	fileExtensions:
 	fileExtensions:
 		- php
 		- php
 		- phtml
 		- phtml
@@ -9,14 +10,16 @@ parameters:
 	excludePaths:
 	excludePaths:
 		analyse:
 		analyse:
 			- lib/marienfressinaud/*
 			- lib/marienfressinaud/*
+			- lib/phpgt/*
 			- lib/phpmailer/*
 			- lib/phpmailer/*
 			- lib/SimplePie/*
 			- lib/SimplePie/*
+			- vendor/*
 		analyseAndScan:
 		analyseAndScan:
 			- .git/*
 			- .git/*
 			- node_modules/*
 			- node_modules/*
-			# TODO: include tests
-			- tests/*
-			- vendor/*
 	bootstrapFiles:
 	bootstrapFiles:
 		- cli/_cli.php
 		- cli/_cli.php
 		- lib/favicons.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 {
 class CategoryTest extends PHPUnit\Framework\TestCase {
 
 
-	public function test__construct_whenNoParameters_createsObjectWithDefaultValues() {
+	public function test__construct_whenNoParameters_createsObjectWithDefaultValues(): void {
 		$category = new FreshRSS_Category();
 		$category = new FreshRSS_Category();
 		$this->assertEquals(0, $category->id());
 		$this->assertEquals(0, $category->id());
 		$this->assertEquals('', $category->name());
 		$this->assertEquals('', $category->name());
 	}
 	}
 
 
 	/**
 	/**
-	 * @param string $input
-	 * @param string $expected
 	 * @dataProvider provideValidNames
 	 * @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);
 		$category = new FreshRSS_Category($input);
 		$this->assertEquals($expected, $category->name());
 		$this->assertEquals($expected, $category->name());
 	}
 	}
 
 
-	public function provideValidNames() {
+	/** @return array<array{string,string}> */
+	public function provideValidNames(): array {
 		return array(
 		return array(
 			array('', ''),
 			array('', ''),
 			array('this string does not need trimming', 'this string does not need trimming'),
 			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)
 		$feed_1 = $this->getMockBuilder(FreshRSS_Feed::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();

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

@@ -36,7 +36,7 @@ class LogDAOTest extends TestCase {
 
 
 		$this->logDAO::truncate(self::LOG_FILE_TEST);
 		$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 {
 	protected function tearDown(): void {

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

@@ -6,9 +6,8 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideEmptyInput
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals('', $search->getRawInput());
 		$this->assertEquals('', $search->getRawInput());
 		$this->assertNull($search->getIntitle());
 		$this->assertNull($search->getIntitle());
@@ -24,9 +23,9 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 	/**
 	/**
 	 * Return an array of values for the search object.
 	 * Return an array of values for the search object.
 	 * Here is the description of the values
 	 * Here is the description of the values
-	 * @return array
+	 * @return array{array{''},array{null}}
 	 */
 	 */
-	public function provideEmptyInput() {
+	public function provideEmptyInput(): array {
 		return array(
 		return array(
 			array(''),
 			array(''),
 			array(null),
 			array(null),
@@ -35,20 +34,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideIntitleSearch
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($intitle_value, $search->getIntitle());
 		$this->assertEquals($intitle_value, $search->getIntitle());
 		$this->assertEquals($search_value, $search->getSearch());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
 	 */
-	public function provideIntitleSearch() {
+	public function provideIntitleSearch(): array {
 		return array(
 		return array(
 			array('intitle:word1', array('word1'), null),
 			array('intitle:word1', array('word1'), null),
 			array('intitle:word1-word2', array('word1-word2'), null),
 			array('intitle:word1-word2', array('word1-word2'), null),
@@ -73,20 +71,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideAuthorSearch
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($search_value, $search->getSearch());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
 	 */
-	public function provideAuthorSearch() {
+	public function provideAuthorSearch(): array {
 		return array(
 		return array(
 			array('author:word1', array('word1'), null),
 			array('author:word1', array('word1'), null),
 			array('author:word1-word2', array('word1-word2'), null),
 			array('author:word1-word2', array('word1-word2'), null),
@@ -111,20 +108,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideInurlSearch
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($inurl_value, $search->getInurl());
 		$this->assertEquals($inurl_value, $search->getInurl());
 		$this->assertEquals($search_value, $search->getSearch());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
 	 */
-	public function provideInurlSearch() {
+	public function provideInurlSearch(): array {
 		return array(
 		return array(
 			array('inurl:word1', array('word1'), null),
 			array('inurl:word1', array('word1'), null),
 			array('inurl: word1', array(), array('word1')),
 			array('inurl: word1', array(), array('word1')),
@@ -139,72 +135,65 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideDateSearch
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($min_date_value, $search->getMinDate());
 		$this->assertEquals($min_date_value, $search->getMinDate());
 		$this->assertEquals($max_date_value, $search->getMaxDate());
 		$this->assertEquals($max_date_value, $search->getMaxDate());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
 	 */
-	public function provideDateSearch() {
+	public function provideDateSearch(): array {
 		return 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/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
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($min_pubdate_value, $search->getMinPubdate());
 		$this->assertEquals($min_pubdate_value, $search->getMinPubdate());
 		$this->assertEquals($max_pubdate_value, $search->getMaxPubdate());
 		$this->assertEquals($max_pubdate_value, $search->getMaxPubdate());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<mixed>>
 	 */
 	 */
-	public function providePubdateSearch() {
+	public function providePubdateSearch(): array {
 		return 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/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
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($tags_value, $search->getTags());
 		$this->assertEquals($tags_value, $search->getTags());
 		$this->assertEquals($search_value, $search->getSearch());
 		$this->assertEquals($search_value, $search->getSearch());
 	}
 	}
 
 
 	/**
 	/**
-	 * @return array
+	 * @return array<array<string|array<string>|null>>
 	 */
 	 */
-	public function provideTagsSearch() {
+	public function provideTagsSearch(): array {
 		return array(
 		return array(
 			array('#word1', array('word1'), null),
 			array('#word1', array('word1'), null),
 			array('# word1', array(), array('#', 'word1')),
 			array('# word1', array(), array('#', 'word1')),
@@ -219,19 +208,15 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideMultipleSearch
 	 * @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);
 		$search = new FreshRSS_Search($input);
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($author_value, $search->getAuthor());
 		$this->assertEquals($min_date_value, $search->getMinDate());
 		$this->assertEquals($min_date_value, $search->getMinDate());
@@ -245,7 +230,8 @@ class SearchTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($input, $search->getRawInput());
 		$this->assertEquals($input, $search->getRawInput());
 	}
 	}
 
 
-	public function provideMultipleSearch() {
+	/** @return array<array<mixed>> */
+	public function provideMultipleSearch(): array {
 		return array(
 		return array(
 			array(
 			array(
 				'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5',
 				'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
 	 * @dataProvider provideParentheses
 	 * @param array<string> $values
 	 * @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));
 		list($filterValues, $filterSearch) = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
 		$this->assertEquals($sql, $filterSearch);
 		$this->assertEquals($sql, $filterSearch);
 		$this->assertEquals($values, $filterValues);
 		$this->assertEquals($values, $filterValues);
 	}
 	}
 
 
-	public function provideParentheses() {
+	/** @return array<array<mixed>> */
+	public function provideParentheses(): array {
 		return [
 		return [
 			[
 			[
 				'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))',
 				'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 {
 class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 
-	public function test__construct_whenAllQuery_storesAllParameters() {
+	public function test__construct_whenAllQuery_storesAllParameters(): void {
 		$query = array('get' => 'a');
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals('all', $user_query->getGetName());
 		$this->assertEquals('all', $user_query->getGetName());
 		$this->assertEquals('all', $user_query->getGetType());
 		$this->assertEquals('all', $user_query->getGetType());
 	}
 	}
 
 
-	public function test__construct_whenFavoriteQuery_storesFavoriteParameters() {
+	public function test__construct_whenFavoriteQuery_storesFavoriteParameters(): void {
 		$query = array('get' => 's');
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals('favorite', $user_query->getGetName());
 		$this->assertEquals('favorite', $user_query->getGetName());
 		$this->assertEquals('favorite', $user_query->getGetType());
 		$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->expectException(FreshRSS_DAO_Exception::class);
 		$this->expectExceptionMessage('Category DAO is not loaded in UserQuery');
 		$this->expectExceptionMessage('Category DAO is not loaded in UserQuery');
 
 
@@ -27,13 +27,15 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		new FreshRSS_UserQuery($query);
 		new FreshRSS_UserQuery($query);
 	}
 	}
 
 
-	public function test__construct_whenCategoryQuery_storesCategoryParameters() {
+	public function test__construct_whenCategoryQuery_storesCategoryParameters(): void {
 		$category_name = 'some category name';
 		$category_name = 'some category name';
+		/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
 		$cat = $this->createMock('FreshRSS_Category');
 		$cat = $this->createMock('FreshRSS_Category');
 		$cat->expects($this->atLeastOnce())
 		$cat->expects($this->atLeastOnce())
 			->method('name')
 			->method('name')
 			->withAnyParameters()
 			->withAnyParameters()
 			->willReturn($category_name);
 			->willReturn($category_name);
+		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -45,7 +47,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('category', $user_query->getGetType());
 		$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->expectException(FreshRSS_DAO_Exception::class);
 		$this->expectExceptionMessage('Feed DAO is not loaded in UserQuery');
 		$this->expectExceptionMessage('Feed DAO is not loaded in UserQuery');
 
 
@@ -53,13 +55,15 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		new FreshRSS_UserQuery($query);
 		new FreshRSS_UserQuery($query);
 	}
 	}
 
 
-	public function test__construct_whenFeedQuery_storesFeedParameters() {
+	public function test__construct_whenFeedQuery_storesFeedParameters(): void {
 		$feed_name = 'some feed name';
 		$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())
 		$feed->expects($this->atLeastOnce())
 			->method('name')
 			->method('name')
 			->withAnyParameters()
 			->withAnyParameters()
 			->willReturn($feed_name);
 			->willReturn($feed_name);
+		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
 		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -71,48 +75,48 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('feed', $user_query->getGetType());
 		$this->assertEquals('feed', $user_query->getGetType());
 	}
 	}
 
 
-	public function test__construct_whenUnknownQuery_doesStoreParameters() {
+	public function test__construct_whenUnknownQuery_doesStoreParameters(): void {
 		$query = array('get' => 'q');
 		$query = array('get' => 'q');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEmpty($user_query->getGetName());
 		$this->assertEmpty($user_query->getGetName());
 		$this->assertEmpty($user_query->getGetType());
 		$this->assertEmpty($user_query->getGetType());
 	}
 	}
 
 
-	public function test__construct_whenName_storesName() {
+	public function test__construct_whenName_storesName(): void {
 		$name = 'some name';
 		$name = 'some name';
 		$query = array('name' => $name);
 		$query = array('name' => $name);
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($name, $user_query->getName());
 		$this->assertEquals($name, $user_query->getName());
 	}
 	}
 
 
-	public function test__construct_whenOrder_storesOrder() {
+	public function test__construct_whenOrder_storesOrder(): void {
 		$order = 'some order';
 		$order = 'some order';
 		$query = array('order' => $order);
 		$query = array('order' => $order);
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($order, $user_query->getOrder());
 		$this->assertEquals($order, $user_query->getOrder());
 	}
 	}
 
 
-	public function test__construct_whenState_storesState() {
+	public function test__construct_whenState_storesState(): void {
 		$state = FreshRSS_Entry::STATE_ALL;
 		$state = FreshRSS_Entry::STATE_ALL;
 		$query = array('state' => $state);
 		$query = array('state' => $state);
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($state, $user_query->getState());
 		$this->assertEquals($state, $user_query->getState());
 	}
 	}
 
 
-	public function test__construct_whenUrl_storesUrl() {
+	public function test__construct_whenUrl_storesUrl(): void {
 		$url = 'some url';
 		$url = 'some url';
 		$query = array('url' => $url);
 		$query = array('url' => $url);
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertEquals($url, $user_query->getUrl());
 		$this->assertEquals($url, $user_query->getUrl());
 	}
 	}
 
 
-	public function testToArray_whenNoData_returnsEmptyArray() {
+	public function testToArray_whenNoData_returnsEmptyArray(): void {
 		$user_query = new FreshRSS_UserQuery(array());
 		$user_query = new FreshRSS_UserQuery(array());
 		$this->assertIsIterable($user_query->toArray());
 		$this->assertIsIterable($user_query->toArray());
 		$this->assertCount(0, $user_query->toArray());
 		$this->assertCount(0, $user_query->toArray());
 	}
 	}
 
 
-	public function testToArray_whenData_returnsArray() {
+	public function testToArray_whenData_returnsArray(): void {
 		$query = array(
 		$query = array(
 			'get' => 's',
 			'get' => 's',
 			'name' => 'some name',
 			'name' => 'some name',
@@ -127,7 +131,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($query, $user_query->toArray());
 		$this->assertEquals($query, $user_query->toArray());
 	}
 	}
 
 
-	public function testHasSearch_whenSearch_returnsTrue() {
+	public function testHasSearch_whenSearch_returnsTrue(): void {
 		$query = array(
 		$query = array(
 			'search' => 'some search',
 			'search' => 'some search',
 		);
 		);
@@ -135,31 +139,33 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->hasSearch());
 		$this->assertTrue($user_query->hasSearch());
 	}
 	}
 
 
-	public function testHasSearch_whenNoSearch_returnsFalse() {
+	public function testHasSearch_whenNoSearch_returnsFalse(): void {
 		$user_query = new FreshRSS_UserQuery(array());
 		$user_query = new FreshRSS_UserQuery(array());
 		$this->assertFalse($user_query->hasSearch());
 		$this->assertFalse($user_query->hasSearch());
 	}
 	}
 
 
-	public function testHasParameters_whenAllQuery_returnsFalse() {
+	public function testHasParameters_whenAllQuery_returnsFalse(): void {
 		$query = array('get' => 'a');
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->hasParameters());
 		$this->assertFalse($user_query->hasParameters());
 	}
 	}
 
 
-	public function testHasParameters_whenNoParameter_returnsFalse() {
+	public function testHasParameters_whenNoParameter_returnsFalse(): void {
 		$query = array();
 		$query = array();
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->hasParameters());
 		$this->assertFalse($user_query->hasParameters());
 	}
 	}
 
 
-	public function testHasParameters_whenParameter_returnTrue() {
+	public function testHasParameters_whenParameter_returnTrue(): void {
 		$query = array('get' => 's');
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertTrue($user_query->hasParameters());
 		$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');
 		$cat = $this->createMock('FreshRSS_Category');
+		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -170,7 +176,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($user_query->isDeprecated());
 		$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 = $this->createMock('FreshRSS_CategoryDAO');
 		$cat_dao->expects($this->atLeastOnce())
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -181,8 +188,10 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->isDeprecated());
 		$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 = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -193,7 +202,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($user_query->isDeprecated());
 		$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 = $this->createMock('FreshRSS_FeedDAO');
 		$feed_dao->expects($this->atLeastOnce())
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->method('searchById')
@@ -204,19 +214,19 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($user_query->isDeprecated());
 		$this->assertTrue($user_query->isDeprecated());
 	}
 	}
 
 
-	public function testIsDeprecated_whenAllQuery_returnFalse() {
+	public function testIsDeprecated_whenAllQuery_returnFalse(): void {
 		$query = array('get' => 'a');
 		$query = array('get' => 'a');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());
 		$this->assertFalse($user_query->isDeprecated());
 	}
 	}
 
 
-	public function testIsDeprecated_whenFavoriteQuery_returnFalse() {
+	public function testIsDeprecated_whenFavoriteQuery_returnFalse(): void {
 		$query = array('get' => 's');
 		$query = array('get' => 's');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());
 		$this->assertFalse($user_query->isDeprecated());
 	}
 	}
 
 
-	public function testIsDeprecated_whenUnknownQuery_returnFalse() {
+	public function testIsDeprecated_whenUnknownQuery_returnFalse(): void {
 		$query = array('get' => 'q');
 		$query = array('get' => 'q');
 		$user_query = new FreshRSS_UserQuery($query);
 		$user_query = new FreshRSS_UserQuery($query);
 		$this->assertFalse($user_query->isDeprecated());
 		$this->assertFalse($user_query->isDeprecated());

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

@@ -1,7 +1,7 @@
 <?php
 <?php
 
 
 class passwordUtilTest extends PHPUnit\Framework\TestCase {
 class passwordUtilTest extends PHPUnit\Framework\TestCase {
-	public function testCheck() {
+	public function testCheck(): void {
 		$password = '1234567';
 		$password = '1234567';
 
 
 		$ok = FreshRSS_password_Util::check($password);
 		$ok = FreshRSS_password_Util::check($password);
@@ -9,7 +9,7 @@ class passwordUtilTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($ok);
 		$this->assertTrue($ok);
 	}
 	}
 
 
-	public function testCheckReturnsFalseIfEmpty() {
+	public function testCheckReturnsFalseIfEmpty(): void {
 		$password = '';
 		$password = '';
 
 
 		$ok = FreshRSS_password_Util::check($password);
 		$ok = FreshRSS_password_Util::check($password);
@@ -17,7 +17,7 @@ class passwordUtilTest extends PHPUnit\Framework\TestCase {
 		$this->assertFalse($ok);
 		$this->assertFalse($ok);
 	}
 	}
 
 
-	public function testCheckReturnsFalseIfLessThan7Characters() {
+	public function testCheckReturnsFalseIfLessThan7Characters(): void {
 		$password = '123456';
 		$password = '123456';
 
 
 		$ok = FreshRSS_password_Util::check($password);
 		$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';
 require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 
 
 class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
+	/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
 	private $value;
 	private $value;
 
 
 	public function setUp(): void {
 	public function setUp(): void {
@@ -12,7 +13,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 			->getMock();
 			->getMock();
 	}
 	}
 
 
-	public function testDisplayReport() {
+	public function testDisplayReport(): void {
 		$validator = new I18nCompletionValidator([], []);
 		$validator = new I18nCompletionValidator([], []);
 
 
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
@@ -40,13 +41,13 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$validator->displayReport();
 		$validator->displayReport();
 	}
 	}
 
 
-	public function testValidateWhenNoData() {
+	public function testValidateWhenNoData(): void {
 		$validator = new I18nCompletionValidator([], []);
 		$validator = new I18nCompletionValidator([], []);
 		$this->assertTrue($validator->validate());
 		$this->assertTrue($validator->validate());
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenKeyIsMissing() {
+	public function testValidateWhenKeyIsMissing(): void {
 		$validator = new I18nCompletionValidator([
 		$validator = new I18nCompletionValidator([
 			'file1.php' => [
 			'file1.php' => [
 				'file1.l1.l2.k1' => $this->value,
 				'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());
 		$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))
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->method('isIgnore')
 			->willReturn(true);
 			->willReturn(true);
@@ -85,7 +86,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenValueIsEqual() {
+	public function testValidateWhenValueIsEqual(): void {
 		$this->value->expects($this->exactly(2))
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->method('isIgnore')
 			->willReturn(false);
 			->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());
 		$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))
 		$this->value->expects($this->exactly(2))
 			->method('isIgnore')
 			->method('isIgnore')
 			->willReturn(false);
 			->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';
 require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
 
 
 class I18nDataTest extends PHPUnit\Framework\TestCase {
 class I18nDataTest extends PHPUnit\Framework\TestCase {
+	/** @var array<string,array<string,array<string,I18nValue>>> */
 	private $referenceData;
 	private $referenceData;
+	/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
 	private $value;
 	private $value;
 
 
 	public function setUp(): void {
 	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);
 		$data = new I18nData($this->referenceData);
 		$this->assertEquals($this->referenceData, $data->getData());
 		$this->assertEquals($this->referenceData, $data->getData());
 	}
 	}
 
 
-	public function testConstructorWhenLanguageIsMissingFile() {
+	public function testConstructorWhenLanguageIsMissingFile(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 			'fr' => [
 				'file1.php' => [
 				'file1.php' => [
@@ -79,7 +81,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testConstructorWhenLanguageIsMissingKeys() {
+	public function testConstructorWhenLanguageIsMissingKeys(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 			'fr' => [
 				'file1.php' => [
 				'file1.php' => [
@@ -125,7 +127,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testConstructorWhenLanguageHasExtraKeys() {
+	public function testConstructorWhenLanguageHasExtraKeys(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [
 			'fr' => [
 				'file1.php' => [
 				'file1.php' => [
@@ -179,7 +181,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testConstructorWhenValueIsIdenticalAndIsMarkedAsIgnore() {
+	public function testConstructorWhenValueIsIdenticalAndIsMarkedAsIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -204,7 +206,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 		new I18nData($rawData);
 	}
 	}
 
 
-	public function testConstructorWhenValueIsIdenticalAndIsNotMarkedAsIgnore() {
+	public function testConstructorWhenValueIsIdenticalAndIsNotMarkedAsIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -229,7 +231,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 		new I18nData($rawData);
 	}
 	}
 
 
-	public function testConstructorWhenValueIsDifferentAndIsMarkedAsToDo() {
+	public function testConstructorWhenValueIsDifferentAndIsMarkedAsToDo(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -249,7 +251,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 		new I18nData($rawData);
 	}
 	}
 
 
-	public function testConstructorWhenValueIsDifferentAndIsNotMarkedAsTodo() {
+	public function testConstructorWhenValueIsDifferentAndIsNotMarkedAsTodo(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -269,7 +271,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		new I18nData($rawData);
 		new I18nData($rawData);
 	}
 	}
 
 
-	public function testGetAvailableLanguagesWhenTheyAreSorted() {
+	public function testGetAvailableLanguagesWhenTheyAreSorted(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 			'nl' => [],
 			'nl' => [],
@@ -282,7 +284,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getAvailableLanguages());
 		], $data->getAvailableLanguages());
 	}
 	}
 
 
-	public function testGetAvailableLanguagesWhenTheyAreNotSorted() {
+	public function testGetAvailableLanguagesWhenTheyAreNotSorted(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'nl' => [],
 			'nl' => [],
 			'fr' => [],
 			'fr' => [],
@@ -297,14 +299,14 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getAvailableLanguages());
 		], $data->getAvailableLanguages());
 	}
 	}
 
 
-	public function testAddLanguageWhenLanguageExists() {
+	public function testAddLanguageWhenLanguageExists(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected language already exist.');
 		$this->expectExceptionMessage('The selected language already exist.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('en');
 		$data->addLanguage('en');
 	}
 	}
 
 
-	public function testAddLanguageWhenNoReferenceProvided() {
+	public function testAddLanguageWhenNoReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr');
 		$data->addLanguage('fr');
 		$this->assertEquals([
 		$this->assertEquals([
@@ -341,7 +343,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testAddLanguageWhenUnknownReferenceProvided() {
+	public function testAddLanguageWhenUnknownReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr', 'unknown');
 		$data->addLanguage('fr', 'unknown');
 		$this->assertEquals([
 		$this->assertEquals([
@@ -378,7 +380,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testAddLanguageWhenKnownReferenceProvided() {
+	public function testAddLanguageWhenKnownReferenceProvided(): void {
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addLanguage('fr', 'en');
 		$data->addLanguage('fr', 'en');
 		$this->assertEquals([
 		$this->assertEquals([
@@ -415,24 +417,24 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testIsKnownWhenKeyExists() {
+	public function testIsKnownWhenKeyExists(): void {
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$this->assertTrue($data->isKnown('file2.l1.l2.k2'));
 		$this->assertTrue($data->isKnown('file2.l1.l2.k2'));
 	}
 	}
 
 
-	public function testIsKnownWhenKeyDoesNotExist() {
+	public function testIsKnownWhenKeyDoesNotExist(): void {
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$this->assertFalse($data->isKnown('file2.l1.l2.k3'));
 		$this->assertFalse($data->isKnown('file2.l1.l2.k3'));
 	}
 	}
 
 
-	public function testAddKeyWhenKeyExists() {
+	public function testAddKeyWhenKeyExists(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key already exist.');
 		$this->expectExceptionMessage('The selected key already exist.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addKey('file2.l1.l2.k1', 'value');
 		$data->addKey('file2.l1.l2.k1', 'value');
 	}
 	}
 
 
-	public function testAddKeyWhenParentKeyExists() {
+	public function testAddKeyWhenParentKeyExists(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 		]);
 		]);
@@ -447,7 +449,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($data->isKnown('file2.l1.l2.k1.sk1'));
 		$this->assertTrue($data->isKnown('file2.l1.l2.k1.sk1'));
 	}
 	}
 
 
-	public function testAddKeyWhenKeyIsParent() {
+	public function testAddKeyWhenKeyIsParent(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 		]);
 		]);
@@ -462,7 +464,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertTrue($data->isKnown('file1.l1.l2.k2'));
 		$this->assertTrue($data->isKnown('file1.l1.l2.k2'));
 	}
 	}
 
 
-	public function testAddKey() {
+	public function testAddKey(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k3'];
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k3'];
 		};
 		};
@@ -484,21 +486,21 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($frValue, $enValue);
 		$this->assertEquals($frValue, $enValue);
 	}
 	}
 
 
-	public function testAddValueWhenLanguageDoesNotExist() {
+	public function testAddValueWhenLanguageDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected language does not exist.');
 		$this->expectExceptionMessage('The selected language does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addValue('file2.l1.l2.k2', 'new value', 'fr');
 		$data->addValue('file2.l1.l2.k2', 'new value', 'fr');
 	}
 	}
 
 
-	public function testAddValueWhenKeyDoesNotExist() {
+	public function testAddValueWhenKeyDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist for the selected language.');
 		$this->expectExceptionMessage('The selected key does not exist for the selected language.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->addValue('unknown key', 'new value', 'en');
 		$data->addValue('unknown key', 'new value', 'en');
 	}
 	}
 
 
-	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasNotChange() {
+	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasNotChange(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 			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());
 		$this->assertEquals('new value', $afterFrValue->getValue());
 	}
 	}
 
 
-	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange() {
+	public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 		};
 		};
@@ -561,7 +563,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($value, $afterFrValue);
 		$this->assertEquals($value, $afterFrValue);
 	}
 	}
 
 
-	public function testAddValueWhenLanguageIsNotReference() {
+	public function testAddValueWhenLanguageIsNotReference(): void {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 		$getTargetedValue = static function (I18nData $data, string $language) {
 			return $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
 			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());
 		$this->assertEquals('new value', $afterFrValue->getValue());
 	}
 	}
 
 
-	public function testRemoveKeyWhenKeyDoesNotExist() {
+	public function testRemoveKeyWhenKeyDoesNotExist(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->removeKey('Unknown key');
 		$data->removeKey('Unknown key');
 	}
 	}
 
 
-	public function testRemoveKeyWhenKeyHasNoEmptySibling() {
+	public function testRemoveKeyWhenKeyHasNoEmptySibling(): void {
 		$this->expectException(\Exception::class);
 		$this->expectException(\Exception::class);
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$this->expectExceptionMessage('The selected key does not exist.');
 		$data = new I18nData($this->referenceData);
 		$data = new I18nData($this->referenceData);
 		$data->removeKey('file1.l1.l2');
 		$data->removeKey('file1.l1.l2');
 	}
 	}
 
 
-	public function testRemoveKeyWhenKeyIsEmptySibling() {
+	public function testRemoveKeyWhenKeyIsEmptySibling(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 		]);
 		]);
@@ -635,7 +637,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testRemoveKeyWhenKeyIsTheOnlyChild() {
+	public function testRemoveKeyWhenKeyIsTheOnlyChild(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 		]);
 		]);
@@ -673,7 +675,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		], $data->getData());
 		], $data->getData());
 	}
 	}
 
 
-	public function testIgnore() {
+	public function testIgnore(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -695,7 +697,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$data->ignore('file1.l1.l2.k1', 'fr', false);
 		$data->ignore('file1.l1.l2.k1', 'fr', false);
 	}
 	}
 
 
-	public function testIgnoreUnmodified() {
+	public function testIgnoreUnmodified(): void {
 		$value = $this->getMockBuilder(I18nValue::class)
 		$value = $this->getMockBuilder(I18nValue::class)
 			->disableOriginalConstructor()
 			->disableOriginalConstructor()
 			->getMock();
 			->getMock();
@@ -722,7 +724,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$data->ignore_unmodified('fr', false);
 		$data->ignore_unmodified('fr', false);
 	}
 	}
 
 
-	public function testGetLanguage() {
+	public function testGetLanguage(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 			'nl' => [],
 			'nl' => [],
@@ -731,7 +733,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($this->referenceData['en'], $data->getLanguage('en'));
 		$this->assertEquals($this->referenceData['en'], $data->getLanguage('en'));
 	}
 	}
 
 
-	public function testGetReferenceLanguage() {
+	public function testGetReferenceLanguage(): void {
 		$rawData = array_merge($this->referenceData, [
 		$rawData = array_merge($this->referenceData, [
 			'fr' => [],
 			'fr' => [],
 			'nl' => [],
 			'nl' => [],

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

@@ -3,7 +3,7 @@
 require_once __DIR__ . '/../../../cli/i18n/I18nFile.php';
 require_once __DIR__ . '/../../../cli/i18n/I18nFile.php';
 
 
 class I18nFileTest extends PHPUnit\Framework\TestCase {
 class I18nFileTest extends PHPUnit\Framework\TestCase {
-	public function test() {
+	public function test(): void {
 		$before = $this->computeFilesHash();
 		$before = $this->computeFilesHash();
 
 
 		$file = new I18nFile();
 		$file = new I18nFile();
@@ -15,7 +15,8 @@ class I18nFileTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals($before, $after);
 		$this->assertEquals($before, $after);
 	}
 	}
 
 
-	private function computeFilesHash() {
+	/** @return array<string,string> */
+	private function computeFilesHash(): array {
 		$hashes = [];
 		$hashes = [];
 
 
 		$dirs = new DirectoryIterator(I18N_PATH);
 		$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';
 require_once __DIR__ . '/../../../cli/i18n/I18nUsageValidator.php';
 
 
 class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
+	/** @var I18nValue */
 	private $value;
 	private $value;
 
 
 	public function setUp(): void {
 	public function setUp(): void {
@@ -12,7 +13,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 			->getMock();
 			->getMock();
 	}
 	}
 
 
-	public function testDisplayReport() {
+	public function testDisplayReport(): void {
 		$validator = new I18nUsageValidator([], []);
 		$validator = new I18nUsageValidator([], []);
 
 
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
 		$this->assertEquals("There is no data.\n", $validator->displayReport());
@@ -40,13 +41,13 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$validator->displayReport();
 		$validator->displayReport();
 	}
 	}
 
 
-	public function testValidateWhenNoData() {
+	public function testValidateWhenNoData(): void {
 		$validator = new I18nUsageValidator([], []);
 		$validator = new I18nUsageValidator([], []);
 		$this->assertTrue($validator->validate());
 		$this->assertTrue($validator->validate());
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenParentKeyExistsWithoutTransformation() {
+	public function testValidateWhenParentKeyExistsWithoutTransformation(): void {
 		$validator = new I18nUsageValidator([
 		$validator = new I18nUsageValidator([
 			'file1' => [
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
 				'file1.l1.l2._' => $this->value,
@@ -62,7 +63,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenParentKeyExistsWithTransformation() {
+	public function testValidateWhenParentKeyExistsWithTransformation(): void {
 		$validator = new I18nUsageValidator([
 		$validator = new I18nUsageValidator([
 			'file1' => [
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
 				'file1.l1.l2._' => $this->value,
@@ -78,7 +79,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenParentKeyDoesNotExist() {
+	public function testValidateWhenParentKeyDoesNotExist(): void {
 		$validator = new I18nUsageValidator([
 		$validator = new I18nUsageValidator([
 			'file1' => [
 			'file1' => [
 				'file1.l1.l2._' => $this->value,
 				'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());
 		$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([
 		$validator = new I18nUsageValidator([
 			'file1' => [
 			'file1' => [
 				'file1.l1.l2.k1' => $this->value,
 				'file1.l1.l2.k1' => $this->value,
@@ -107,7 +108,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {
 		$this->assertEquals('', $validator->displayResult());
 		$this->assertEquals('', $validator->displayResult());
 	}
 	}
 
 
-	public function testValidateWhenChildKeyDoesNotExist() {
+	public function testValidateWhenChildKeyDoesNotExist(): void {
 		$validator = new I18nUsageValidator([
 		$validator = new I18nUsageValidator([
 			'file1' => [
 			'file1' => [
 				'file1.l1.l2.k1' => $this->value,
 				'file1.l1.l2.k1' => $this->value,

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

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

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

@@ -2,9 +2,9 @@
 
 
 class FreshRSS_Migration_2019_12_22_FooBar {
 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;
 		return true;
 	}
 	}
 }
 }

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

@@ -2,9 +2,9 @@
 
 
 class FreshRSS_Migration_2019_12_23_Baz {
 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;
 		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 {
 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;
 		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 {
 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;
 		return false;
 	}
 	}
 }
 }

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

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