diff --git a/classes/api.php b/classes/api.php
index b17114693dd4b3706b927904d159e7963f7bfdc5..0e873856fe8cddc5854b48426064240aa4f707e6 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -286,7 +286,7 @@ class API extends Handler {
 			$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
 				$field = $set_to $additional_fields
 				WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
-			$sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
+			$sth->execute([...$article_ids, $_SESSION['uid']]);
 
 			$num_updated = $sth->rowCount();
 
diff --git a/classes/article.php b/classes/article.php
index e113ed21919f3b9f5273e1144e27fdc61c86cf9b..17a40ca4fa5e7e350f05ae5a13e6b372d8636937 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -177,7 +177,7 @@ class Article extends Handler_Protected {
 		$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
 			score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
 
-		$sth->execute(array_merge([$score], $ids, [$_SESSION['uid']]));
+		$sth->execute([$score, ...$ids, $_SESSION['uid']]);
 
 		print json_encode(["id" => $ids, "score" => $score]);
 	}
@@ -507,7 +507,7 @@ class Article extends Handler_Protected {
 					WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
 		}
 
-		$sth->execute(array_merge($ids, [$owner_uid]));
+		$sth->execute([...$ids, $owner_uid]);
 	}
 
 	/**
diff --git a/classes/counters.php b/classes/counters.php
index 8756b5acf70402ce3f1fc0b7ab30ca92275bbc60..bc4d2d4a37de3d8045c50cf282bcc35cead84b6b 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -5,13 +5,13 @@ class Counters {
 	 * @return array<int, array<string, int|string>>
 	 */
 	static function get_all(): array {
-		return array_merge(
-			self::get_global(),
-			self::get_virt(),
-			self::get_labels(),
-			self::get_feeds(),
-			self::get_cats()
-		);
+		return [
+			...self::get_global(),
+			...self::get_virt(),
+			...self::get_labels(),
+			...self::get_feeds(),
+			...self::get_cats(),
+		];
 	}
 
 	/**
@@ -20,13 +20,13 @@ class Counters {
 	 * @return array<int, array<string, int|string>>
 	 */
 	static function get_conditional(array $feed_ids = null, array $label_ids = null): array {
-		return array_merge(
-			self::get_global(),
-			self::get_virt(),
-			self::get_labels($label_ids),
-			self::get_feeds($feed_ids),
-			self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
-		);
+		return [
+			...self::get_global(),
+			...self::get_virt(),
+			...self::get_labels($label_ids),
+			...self::get_feeds($feed_ids),
+			...self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
+		];
 	}
 
 	/**
@@ -93,11 +93,7 @@ class Counters {
 					ue.feed_id = f.id AND
 					ue.owner_uid = ?");
 
-			$sth->execute(array_merge(
-				[$_SESSION['uid']],
-				$cat_ids,
-				[$_SESSION['uid']]
-			));
+			$sth->execute([$_SESSION['uid'], ...$cat_ids, $_SESSION['uid']]);
 
 		} else {
 			$sth = $pdo->prepare("SELECT fc.id,
@@ -170,7 +166,7 @@ class Counters {
 				WHERE f.id = ue.feed_id AND ue.owner_uid = ? AND f.id IN ($feed_ids_qmarks)
 				GROUP BY f.id");
 
-			$sth->execute(array_merge([$_SESSION['uid']], $feed_ids));
+			$sth->execute([$_SESSION['uid'], ...$feed_ids]);
 		} else {
 			$sth = $pdo->prepare("SELECT f.id,
 					f.title,
@@ -319,7 +315,7 @@ class Counters {
 						LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = ?
 							WHERE ttrss_labels2.owner_uid = ? AND ttrss_labels2.id IN ($label_ids_qmarks)
 								GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
-			$sth->execute(array_merge([$_SESSION["uid"], $_SESSION["uid"]], $label_ids));
+			$sth->execute([$_SESSION["uid"], $_SESSION["uid"], ...$label_ids]);
 		} else {
 			$sth = $pdo->prepare("SELECT id,
 						caption,
diff --git a/classes/db/migrations.php b/classes/db/migrations.php
index aecd9186cb3499025bf1168453b4faed40d2ed9c..ffce2af5f21adfd6deac53b0167d11e632b94b27 100644
--- a/classes/db/migrations.php
+++ b/classes/db/migrations.php
@@ -1,33 +1,15 @@
 <?php
 class Db_Migrations {
 
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var string */
-	private $base_filename = "schema.sql";
-
-	/** @var string */
-	private $base_path;
-
-	/** @var string */
-	private $migrations_path;
-
-	/** @var string */
-	private $migrations_table;
-
-	/** @var bool */
-	private $base_is_latest;
-
-	/** @var PDO */
-	private $pdo;
-
-	/** @var int */
-	private $cached_version = 0;
-
-	/** @var int */
-	private $cached_max_version = 0;
-
-	/** @var int */
-	private $max_version_override;
+	private string $base_filename = "schema.sql";
+	private string $base_path;
+	private string $migrations_path;
+	private string $migrations_table;
+	private bool $base_is_latest;
+	private PDO $pdo;
+	private int $cached_version = 0;
+	private int $cached_max_version = 0;
+	private int $max_version_override;
 
 	function __construct() {
 		$this->pdo = Db::pdo();
@@ -207,14 +189,11 @@ class Db_Migrations {
 			$filename = "{$this->base_path}/{$this->base_filename}";
 
 		if (file_exists($filename)) {
-			$lines =	array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
-							function ($line) {
-								return strlen(trim($line)) > 0 && strpos($line, "--") !== 0;
-							});
-
-			return array_filter(explode(";", implode("", $lines)), function ($line) {
-				return strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]);
-			});
+			$lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
+				fn($line) => strlen(trim($line)) > 0 && strpos($line, "--") !== 0);
+
+			return array_filter(explode(";", implode("", $lines)),
+				fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
 
 		} else {
 			user_error("Requested schema file ${filename} not found.", E_USER_ERROR);
diff --git a/classes/debug.php b/classes/debug.php
index 4777e8c7434a741562b1b90a7cc54532a018ec96..40fa273776fc32168c0c1d50229c6bedebefcd28 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -12,44 +12,31 @@ class Debug {
 		Debug::LOG_EXTENDED,
 	];
 
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
 	/**
 	 * @deprecated
-	 * @var int
 	*/
-	public static $LOG_DISABLED = self::LOG_DISABLED;
+	public static int $LOG_DISABLED = self::LOG_DISABLED;
 
 	/**
 	 * @deprecated
-	 * @var int
 	*/
-	public static $LOG_NORMAL = self::LOG_NORMAL;
+	public static int $LOG_NORMAL = self::LOG_NORMAL;
 
 	/**
 	 * @deprecated
-	 * @var int
 	*/
-	public static $LOG_VERBOSE = self::LOG_VERBOSE;
+	public static int $LOG_VERBOSE = self::LOG_VERBOSE;
 
 	/**
 	 * @deprecated
-	 * @var int
 	*/
-	public static $LOG_EXTENDED = self::LOG_EXTENDED;
+	public static int $LOG_EXTENDED = self::LOG_EXTENDED;
 
-	/** @var bool */
-	private static $enabled = false;
+	private static bool $enabled = false;
+	private static bool $quiet = false;
+	private static ?string $logfile = null;
 
-	/** @var bool */
-	private static $quiet = false;
-
-	/** @var string|null */
-	private static $logfile = null;
-
-	/**
-	 * @var int Debug::LOG_*
-	 */
-    private static $loglevel = self::LOG_NORMAL;
+	private static int $loglevel = self::LOG_NORMAL;
 
 	public static function set_logfile(string $logfile): void {
         self::$logfile = $logfile;
@@ -70,7 +57,7 @@ class Debug {
 	/**
 	 * @param Debug::LOG_* $level
 	 */
-    public static function set_loglevel($level): void {
+    public static function set_loglevel(int $level): void {
         self::$loglevel = $level;
     }
 
diff --git a/classes/digest.php b/classes/digest.php
index 15203166bef79cb7bd67b30895e79cac72082acb..3e943e6dd7df475491d42d8c7994ae9fd65647a6 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -163,9 +163,7 @@ class Digest
 			$article_labels_formatted = "";
 
 			if (is_array($article_labels) && count($article_labels) > 0) {
-				$article_labels_formatted = implode(", ", array_map(function($a) {
-					return $a[1];
-				}, $article_labels));
+				$article_labels_formatted = implode(", ", array_map(fn($a) => $a[1], $article_labels));
 			}
 
 			$tpl->setVariable('FEED_TITLE', $line["feed_title"]);
diff --git a/classes/diskcache.php b/classes/diskcache.php
index 34bba25f1788e36e2ca6a8310f43b55e2b063635..01c713b9970098c992e287a8378a4340426bbf3e 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -1,15 +1,13 @@
 <?php
 class DiskCache {
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var string */
-	private $dir;
+	private string $dir;
 
 	/**
 	 * https://stackoverflow.com/a/53662733
 	 *
 	 * @var array<string, string>
 	 */
-	private $mimeMap = [
+	private array $mimeMap = [
 		'video/3gpp2'                                                               => '3g2',
 		'video/3gp'                                                                 => '3gp',
 		'video/3gpp'                                                                => '3gp',
diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php
index 6de790ff94a109417b2bf55a450d5f52fe5279d3..59bf403b303645f449e1efb1456369e41ab8af06 100755
--- a/classes/feeditem/atom.php
+++ b/classes/feeditem/atom.php
@@ -201,7 +201,7 @@ class FeedItem_Atom extends FeedItem_Common {
 			}
 		}
 
-		$encs = array_merge($encs, parent::get_enclosures());
+		array_push($encs, ...parent::get_enclosures());
 
 		return $encs;
 	}
diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php
index 6a9be8aca735a5f166400700e7fe2202f0a3cb4e..fde481179a5fade5454e5c738e6afb2639db9c45 100755
--- a/classes/feeditem/common.php
+++ b/classes/feeditem/common.php
@@ -189,7 +189,7 @@ abstract class FeedItem_Common extends FeedItem {
 		$tmp = [];
 
 		foreach ($cats as $rawcat) {
-			$tmp = array_merge($tmp, explode(",", $rawcat));
+			array_push($tmp, ...explode(",", $rawcat));
 		}
 
 		$tmp = array_map(function($srccat) {
diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php
index 7017d04e9b75efe7194ab2c99b54985c1bfc9d5d..132eabff53c26eb08f476acb1b9d2c8cb067138b 100755
--- a/classes/feeditem/rss.php
+++ b/classes/feeditem/rss.php
@@ -153,7 +153,7 @@ class FeedItem_RSS extends FeedItem_Common {
 			array_push($encs, $enc);
 		}
 
-		$encs = array_merge($encs, parent::get_enclosures());
+		array_push($encs, ...parent::get_enclosures());
 
 		return $encs;
 	}
diff --git a/classes/feedparser.php b/classes/feedparser.php
index fc2489e2d78fa1df6798c00faf1dff48a7e0dfbf..4b9c63f565f2803b09560005537c7214507549b1 100644
--- a/classes/feedparser.php
+++ b/classes/feedparser.php
@@ -43,10 +43,8 @@ class FeedParser {
 			foreach (libxml_get_errors() as $error) {
 				if ($error->level == LIBXML_ERR_FATAL) {
 					// currently only the first error is reported
-					if (!isset($this->error)) {
-						$this->error = $this->format_error($error);
-					}
-					$this->libxml_errors[] = $this->format_error($error);
+					$this->error ??= Errors::format_libxml_error($error);
+					$this->libxml_errors[] = Errors::format_libxml_error($error);
 				}
 			}
 		}
@@ -87,9 +85,7 @@ class FeedParser {
 					$this->type = $this::FEED_ATOM;
 					break;
 				default:
-					if (!isset($this->error)) {
-						$this->error = "Unknown/unsupported feed type";
-					}
+					$this->error ??= "Unknown/unsupported feed type";
 					return;
 				}
 			}
@@ -186,9 +182,7 @@ class FeedParser {
 			if ($this->link) $this->link = trim($this->link);
 
 		} else {
-			if (!isset($this->error)) {
-				$this->error = "Unknown/unsupported feed type";
-			}
+			$this->error ??= "Unknown/unsupported feed type";
 			return;
 		}
 	}
diff --git a/classes/feeds.php b/classes/feeds.php
index 8981d6f14032f5fdf16a22d86d6b7c85f08fcaa4..afcc97d8129203f63121a1e3e44725065bf66cd7 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -1938,8 +1938,8 @@ class Feeds extends Handler_Protected {
 		$sth->execute([$cat, $owner_uid]);
 
 		while ($line = $sth->fetch()) {
-			array_push($rv, (int)$line["parent_cat"]);
-			$rv = array_merge($rv, self::_get_parent_cats($line["parent_cat"], $owner_uid));
+			$cat = (int) $line["parent_cat"];
+			array_push($rv, $cat, ...self::_get_parent_cats($cat, $owner_uid));
 		}
 
 		return $rv;
@@ -1958,8 +1958,7 @@ class Feeds extends Handler_Protected {
 		$sth->execute([$cat, $owner_uid]);
 
 		while ($line = $sth->fetch()) {
-			array_push($rv, $line["id"]);
-			$rv = array_merge($rv, self::_get_child_cats($line["id"], $owner_uid));
+			array_push($rv, $line["id"], ...self::_get_child_cats($line["id"], $owner_uid));
 		}
 
 		return $rv;
@@ -1980,16 +1979,18 @@ class Feeds extends Handler_Protected {
 		$sth = $pdo->prepare("SELECT DISTINCT cat_id, fc.parent_cat FROM ttrss_feeds f LEFT JOIN ttrss_feed_categories fc
 				ON (fc.id = f.cat_id)
 				WHERE f.owner_uid = ? AND f.id IN ($feeds_qmarks)");
-		$sth->execute(array_merge([$owner_uid], $feeds));
+		$sth->execute([$owner_uid, ...$feeds]);
 
 		$rv = [];
 
 		if ($row = $sth->fetch()) {
+			$cat_id = (int) $row["cat_id"];
+			$rv[] = $cat_id;
 			array_push($rv, (int)$row["cat_id"]);
 
-			if ($with_parents && $row["parent_cat"])
-				$rv = array_merge($rv,
-							self::_get_parent_cats($row["cat_id"], $owner_uid));
+			if ($with_parents && $row["parent_cat"]) {
+				array_push($rv, ...self::_get_parent_cats($cat_id, $owner_uid));
+			}
 		}
 
 		$rv = array_unique($rv);
diff --git a/classes/handler.php b/classes/handler.php
index 806c9cfbea687ec320587e3c9f409ae67563b3f5..5b54570d8dab1b3a68794fc399ce4585c22982d0 100644
--- a/classes/handler.php
+++ b/classes/handler.php
@@ -1,11 +1,9 @@
 <?php
 class Handler implements IHandler {
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var PDO */
-	protected $pdo;
+	protected PDO $pdo;
 
 	/** @var array<int|string, mixed> */
-	protected $args;
+	protected array $args;
 
 	/**
 	 * @param array<int|string, mixed> $args
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 3db71520de8c229ce75baecf43e2db8edee0baf9..ea0972f6b364912215914bf3d6e828cbcef1be5f 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -417,8 +417,7 @@ class Handler_Public extends Handler {
 				if (session_status() != PHP_SESSION_ACTIVE)
 					session_start();
 
-				if (!isset($_SESSION["login_error_msg"]))
-					$_SESSION["login_error_msg"] = __("Incorrect username or password");
+				$_SESSION["login_error_msg"] ??= __("Incorrect username or password");
 			}
 
 			$return = clean($_REQUEST['return']);
diff --git a/classes/mailer.php b/classes/mailer.php
index 60b1ce4fd97cb57dac2cdaeb589d10fa067666ca..ac5c641eb64a848bcd5f470bec2e06d65887c7b5 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -1,8 +1,6 @@
 <?php
 class Mailer {
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var string */
-	private $last_error = "";
+	private string $last_error = "";
 
 	/**
 	 * @param array<string, mixed> $params
@@ -47,7 +45,7 @@ class Mailer {
 
 		$headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
 
-		$rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
+		$rc = mail($to_combined, $subject, $message, implode("\r\n", [...$headers, ...$additional_headers]));
 
 		if (!$rc) {
 			$this->set_error(error_get_last()['message'] ?? T_sprintf("Unknown error while sending mail. Hooks tried: %d.", $hooks_tried));
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 6ab4ac8067064caf9bdf5127f40a0d1fedb87aeb..4b85bc2162a2374fc94033a164751dedeb67d8ce 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -1,49 +1,42 @@
 <?php
 class PluginHost {
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var PDO|null */
-	private $pdo = null;
+	private ?PDO $pdo = null;
 
 	/**
 	 * separate handle for plugin data so transaction while saving wouldn't clash with possible main
 	 * tt-rss code transactions; only initialized when first needed
-	 *
-	 * @var PDO|null
 	 */
-	private $pdo_data = null;
+	private ?PDO $pdo_data = null;
 
 	/** @var array<string, array<int, array<int, Plugin>>> hook types -> priority levels -> Plugins */
-	private $hooks = [];
+	private array $hooks = [];
 
 	/** @var array<string, Plugin> */
-	private $plugins = [];
+	private array $plugins = [];
 
 	/** @var array<string, array<string, Plugin>> handler type -> method type -> Plugin */
-	private $handlers = [];
+	private array $handlers = [];
 
 	/** @var array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
-	private $commands = [];
+	private array $commands = [];
 
 	/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value  */
-	private $storage = [];
+	private array $storage = [];
 
 	/** @var array<int, array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>> */
-	private $feeds = [];
+	private array $feeds = [];
 
 	/** @var array<string, Plugin> API method name, Plugin sender */
-	private $api_methods = [];
+	private array $api_methods = [];
 
 	/** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */
-	private $plugin_actions = [];
+	private array $plugin_actions = [];
 
-	/** @var int|null */
-	private $owner_uid = null;
+	private ?int $owner_uid = null;
 
-	/** @var bool */
-	private $data_loaded = false;
+	private bool $data_loaded = false;
 
-	/** @var PluginHost|null */
-	private static $instance = null;
+	private static ?PluginHost $instance = null;
 
 	const API_VERSION = 2;
 	const PUBLIC_METHOD_DELIMITER = "--";
@@ -412,7 +405,7 @@ class PluginHost {
 			$tmp = [];
 
 			foreach (array_keys($this->hooks[$type]) as $prio) {
-				$tmp = array_merge($tmp, $this->hooks[$type][$prio]);
+				array_push($tmp, ...$this->hooks[$type][$prio]);
 			}
 
 			return $tmp;
@@ -425,7 +418,7 @@ class PluginHost {
 	 */
 	function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
 
-		$plugins = array_merge(glob("plugins/*"), glob("plugins.local/*"));
+		$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
 		$plugins = array_filter($plugins, "is_dir");
 		$plugins = array_map("basename", $plugins);
 
@@ -542,10 +535,7 @@ class PluginHost {
 		$method = strtolower($method);
 
 		if ($this->is_system($sender)) {
-			if (!isset($this->handlers[$handler])) {
-				$this->handlers[$handler] = [];
-			}
-
+			$this->handlers[$handler] ??= [];
 			$this->handlers[$handler][$method] = $sender;
 		}
 	}
@@ -648,8 +638,7 @@ class PluginHost {
 				owner_uid= ? AND name = ?");
 			$sth->execute([$this->owner_uid, $plugin]);
 
-			if (!isset($this->storage[$plugin]))
-				$this->storage[$plugin] = [];
+			$this->storage[$plugin] ??= [];
 
 			$content = serialize($this->storage[$plugin]);
 
@@ -680,14 +669,8 @@ class PluginHost {
 		if ($profile_id) {
 			$idx = get_class($sender);
 
-			if (!isset($this->storage[$idx])) {
-				$this->storage[$idx] = [];
-			}
-
-			if (!isset($this->storage[$idx][$profile_id])) {
-				$this->storage[$idx][$profile_id] = [];
-			}
-
+			$this->storage[$idx] ??= [];
+			$this->storage[$idx][$profile_id] ??= [];
 			$this->storage[$idx][$profile_id][$name] = $value;
 
 			$this->save_data(get_class($sender));
@@ -702,9 +685,7 @@ class PluginHost {
 	function set(Plugin $sender, string $name, $value): void {
 		$idx = get_class($sender);
 
-		if (!isset($this->storage[$idx]))
-			$this->storage[$idx] = [];
-
+		$this->storage[$idx] ??= [];
 		$this->storage[$idx][$name] = $value;
 
 		$this->save_data(get_class($sender));
@@ -716,8 +697,7 @@ class PluginHost {
 	function set_array(Plugin $sender, array $params): void {
 		$idx = get_class($sender);
 
-		if (!isset($this->storage[$idx]))
-			$this->storage[$idx] = [];
+		$this->storage[$idx] ??= [];
 
 		foreach ($params as $name => $value)
 			$this->storage[$idx][$name] = $value;
@@ -855,11 +835,13 @@ class PluginHost {
 	function add_filter_action(Plugin $sender, string $action_name, string $action_desc): void {
 		$sender_class = get_class($sender);
 
-		if (!isset($this->plugin_actions[$sender_class]))
-			$this->plugin_actions[$sender_class] = [];
+		$this->plugin_actions[$sender_class] ??= [];
 
-		array_push($this->plugin_actions[$sender_class],
-			array("action" => $action_name, "description" => $action_desc, "sender" => $sender));
+		$this->plugin_actions[$sender_class][] = [
+			"action" => $action_name,
+			"description" => $action_desc,
+			"sender" => $sender,
+		];
 	}
 
 	/**
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index bb638f1662f83e53594498772ac31894cfe8361b..6cf979b0aa16274c8a9c3cd82994f15402ab3ff7 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -172,7 +172,7 @@ class Pref_Feeds extends Handler_Protected {
 			if ($enable_cats) {
 				array_push($root['items'], $cat);
 			} else {
-				$root['items'] = array_merge($root['items'], $cat['items']);
+				array_push($root['items'], ...$cat['items']);
 			}
 
 			$sth = $this->pdo->prepare("SELECT * FROM
@@ -202,7 +202,7 @@ class Pref_Feeds extends Handler_Protected {
 				if ($enable_cats) {
 					array_push($root['items'], $cat);
 				} else {
-					$root['items'] = array_merge($root['items'], $cat['items']);
+					array_push($root['items'], ...$cat['items']);
 				}
 			}
 		}
@@ -848,7 +848,7 @@ class Pref_Feeds extends Handler_Protected {
 				if ($qpart) {
 					$sth = $this->pdo->prepare("UPDATE ttrss_feeds SET $qpart WHERE id IN ($feed_ids_qmarks)
 						AND owner_uid = ?");
-					$sth->execute(array_merge($feed_ids, [$_SESSION['uid']]));
+					$sth->execute([...$feed_ids, $_SESSION['uid']]);
 				}
 			}
 
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index e7c7877a7f0ca4865ea6d9b13f0dae80d04b1ff9..19ec8d39e90cdc661b68fd19b3a45d6fc8f8a050 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -538,7 +538,7 @@ class Pref_Filters extends Handler_Protected {
 
 		$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)
 			AND owner_uid = ?");
-		$sth->execute(array_merge($ids, [$_SESSION['uid']]));
+		$sth->execute([...$ids, $_SESSION['uid']]);
 	}
 
 	private function _save_rules_and_actions(int $filter_id): void {
@@ -781,11 +781,11 @@ class Pref_Filters extends Handler_Protected {
 
 			$sth = $this->pdo->prepare("UPDATE ttrss_filters2_rules
 				SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
-			$sth->execute(array_merge([$base_id], $ids));
+			$sth->execute([$base_id, ...$ids]);
 
 			$sth = $this->pdo->prepare("UPDATE ttrss_filters2_actions
 				SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
-			$sth->execute(array_merge([$base_id], $ids));
+			$sth->execute([$base_id, ...$ids]);
 
 			$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)");
 			$sth->execute($ids);
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index 7acd06aa427b871f2843fb8a60930cfeb9dd0fdd..3285ce200fbfb9947cfed2850a84c6b7fcb85108 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -2,18 +2,17 @@
 use chillerlan\QRCode;
 
 class Pref_Prefs extends Handler_Protected {
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
 	/** @var array<Prefs::*, array<int, string>> */
-	private $pref_help = [];
+	private array $pref_help = [];
 
 	/** @var array<string, array<int, string>> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */
-	private $pref_item_map = [];
+	private array $pref_item_map = [];
 
 	/** @var array<string, string> */
-	private $pref_help_bottom = [];
+	private array $pref_help_bottom = [];
 
 	/** @var array<int, string> */
-	private $pref_blacklist = [];
+	private array $pref_blacklist = [];
 
 	private const BLOCK_SEPARATOR = 'BLOCK_SEPARATOR';
 
@@ -26,7 +25,6 @@ class Pref_Prefs extends Handler_Protected {
 	const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND";
 	const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR";
 
-	/** @param string $method */
 	function csrf_ignore(string $method) : bool {
 		$csrf_ignored = array("index", "updateself", "otpqrcode");
 
@@ -187,7 +185,7 @@ class Pref_Prefs extends Handler_Protected {
 		$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
 
 		foreach ($boolean_prefs as $pref) {
-			if (!isset($_POST[$pref])) $_POST[$pref] = 'false';
+			$_POST[$pref] ??= 'false';
 		}
 
 		$need_reload = false;
@@ -633,10 +631,11 @@ class Pref_Prefs extends Handler_Protected {
 
 					} else if ($pref_name == Prefs::USER_CSS_THEME) {
 
-						$theme_files = array_map("basename",
-							array_merge(glob("themes/*.php"),
-								glob("themes/*.css"),
-								glob("themes.local/*.css")));
+						$theme_files = array_map("basename", [
+							...glob("themes/*.php") ?: [],
+							...glob("themes/*.css") ?: [],
+							...glob("themes.local/*.css") ?: [],
+						]);
 
 						asort($theme_files);
 
@@ -869,18 +868,19 @@ class Pref_Prefs extends Handler_Protected {
 
 						$feed_handler_whitelist = [ "Af_Comics" ];
 
-						$feed_handlers = array_merge(
-							PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_FETCHED),
-							PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_PARSED),
-							PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED));
+						$feed_handlers = [
+							...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_FETCHED),
+							...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_PARSED),
+							...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED),
+						];
 
-						$feed_handlers = array_filter($feed_handlers, function($plugin) use ($feed_handler_whitelist) {
-							return in_array(get_class($plugin), $feed_handler_whitelist) === false; });
+						$feed_handlers = array_filter($feed_handlers,
+							fn($plugin) => in_array(get_class($plugin), $feed_handler_whitelist) === false);
 
 						if (count($feed_handlers) > 0) {
 							print_error(
 								T_sprintf("The following plugins use per-feed content hooks. This may cause excessive data usage and origin server load resulting in a ban of your instance: <b>%s</b>" ,
-									implode(", ", array_map(function($plugin) { return get_class($plugin); }, $feed_handlers))
+									implode(", ", array_map(fn($plugin) => get_class($plugin), $feed_handlers))
 								) . " (<a href='https://tt-rss.org/wiki/FeedHandlerPlugins' target='_blank'>".__("More info...")."</a>)"
 							);
 						}
@@ -1069,9 +1069,7 @@ class Pref_Prefs extends Handler_Protected {
 			}
 		}
 
-		$rv = array_values(array_filter($rv, function ($item) {
-			return $item["rv"]["need_update"];
-		}));
+		$rv = array_values(array_filter($rv, fn($item) => $item["rv"]["need_update"]));
 
 		return $rv;
 	}
diff --git a/classes/rpc.php b/classes/rpc.php
index dbb54cec5fd034953d509652a1d3f1bb6d9f3b5e..ef2cdfc490678bde79f4453d2c9c46bc68d09c68 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -77,7 +77,7 @@ class RPC extends Handler_Protected {
 
 		$sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries
 			WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
-		$sth->execute(array_merge($ids, [$_SESSION['uid']]));
+		$sth->execute([...$ids, $_SESSION['uid']]);
 
 		Article::_purge_orphans();
 
@@ -364,7 +364,7 @@ class RPC extends Handler_Protected {
 					WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
 		}
 
-		$sth->execute(array_merge($ids, [$_SESSION['uid']]));
+		$sth->execute([...$ids, $_SESSION['uid']]);
 	}
 
 	/**
@@ -388,7 +388,7 @@ class RPC extends Handler_Protected {
 					WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
 		}
 
-		$sth->execute(array_merge($ids, [$_SESSION['uid']]));
+		$sth->execute([...$ids, $_SESSION['uid']]);
 	}
 
 	function log(): void {
@@ -753,12 +753,11 @@ class RPC extends Handler_Protected {
 	function hotkeyHelp(): void {
 		$info = self::get_hotkeys_info();
 		$imap = self::get_hotkeys_map();
-		$omap = array();
+		$omap = [];
 
 		foreach ($imap[1] as $sequence => $action) {
-			if (!isset($omap[$action])) $omap[$action] = array();
-
-			array_push($omap[$action], $sequence);
+			$omap[$action] ??= [];
+			$omap[$action][] = $sequence;
 		}
 
 		?>
diff --git a/classes/rssutils.php b/classes/rssutils.php
index e039284f2d5de3d493a860d7e6e4cac650539b39..fe295417a5b1571725a041adc34653530d99c463 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -39,7 +39,7 @@ class RSSUtils {
 
 		// check icon files once every Config::get(Config::CACHE_MAX_DAYS) days
 		$icon_files = array_filter(glob(Config::get(Config::ICONS_DIR) . "/*.ico"),
-			function($f) { return filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS); });
+			fn(string $f) => filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS));
 
 		foreach ($icon_files as $icon) {
 			$feed_id = basename($icon, ".ico");
@@ -733,7 +733,7 @@ class RSSUtils {
 					$article_labels = Article::_get_labels($base_entry_id, $feed_obj->owner_uid);
 
 					$existing_tags = Article::_get_tags($base_entry_id, $feed_obj->owner_uid);
-					$entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
+					$entry_tags = array_unique([...$entry_tags, ...$existing_tags]);
 				} else {
 					$base_entry_id = false;
 					$entry_stored_hash = "";
@@ -865,7 +865,7 @@ class RSSUtils {
 				$pluginhost->run_hooks(PluginHost::HOOK_FILTER_TRIGGERED,
 					$feed, $feed_obj->owner_uid, $article, $matched_filters, $matched_rules, $article_filters);
 
-				$matched_filter_ids = array_map(function($f) { return $f['id']; }, $matched_filters);
+				$matched_filter_ids = array_map(fn(array $f) => $f['id'], $matched_filters);
 
 				if (count($matched_filter_ids) > 0) {
 					$filter_objs = ORM::for_table('ttrss_filters2')
@@ -1190,8 +1190,7 @@ class RSSUtils {
 				// check for manual tags (we have to do it here since they're loaded from filters)
 				foreach ($article_filters as $f) {
 					if ($f["type"] == "tag") {
-						$entry_tags = array_merge($entry_tags,
-							FeedItem_Common::normalize_categories(explode(",", $f["param"])));
+						$entry_tags = [...$entry_tags, ...FeedItem_Common::normalize_categories(explode(",", $f["param"]))];
 					}
 				}
 
@@ -1796,12 +1795,10 @@ class RSSUtils {
 				owner_uid = ? AND enabled = true ORDER BY order_id, title");
 		$sth->execute([$owner_uid]);
 
-		$check_cats = array_merge(
-			Feeds::_get_parent_cats($cat_id, $owner_uid),
-			[$cat_id]);
+		$check_cats = [...Feeds::_get_parent_cats($cat_id, $owner_uid), $cat_id];
 
 		$check_cats_str = join(",", $check_cats);
-		$check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
+		$check_cats_fullids = array_map(fn(int $a) => "CAT:$a", $check_cats);
 
 		while ($line = $sth->fetch()) {
 			$filter_id = $line["id"];
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
index 8dcb94b731110a938a56c3ef9cad3d3b9d1f9807..dc47f5ad8bee5d0346ea9037a1710e02271e6835 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -10,31 +10,14 @@ class UrlHelper {
 		"application/x-bittorrent" => [ "magnet" ],
 	];
 
-	// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
-	/** @var string */
-	static $fetch_last_error;
-
-	/** @var int */
-	static $fetch_last_error_code;
-
-	/** @var string */
-	static $fetch_last_error_content;
-
-	/** @var string */
-	static $fetch_last_content_type;
-
-	/** @var string */
-	static $fetch_last_modified;
-
-
-	/** @var string */
-	static $fetch_effective_url;
-
-	/** @var string */
-	static $fetch_effective_ip_addr;
-
-	/** @var bool */
-	static $fetch_curl_used;
+	static string $fetch_last_error;
+	static int $fetch_last_error_code;
+	static string $fetch_last_error_content;
+	static string $fetch_last_content_type;
+	static string $fetch_last_modified;
+	static string $fetch_effective_url;
+	static string $fetch_effective_ip_addr;
+	static bool $fetch_curl_used;
 
 	/**
 	 * @param array<string, string|int> $parts
@@ -207,32 +190,26 @@ class UrlHelper {
 		if ($nest > 10)
 			return false;
 
-		if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
-			$context_options = array(
-				'http' => array(
-					 'header' => array(
-						 'Connection: close'
-					 ),
-					 'method' => 'HEAD',
-					 'timeout' => $timeout,
-					 'protocol_version'=> 1.1)
-				);
-
-			if (Config::get(Config::HTTP_PROXY)) {
-				$context_options['http']['request_fulluri'] = true;
-				$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
-			}
+		$context_options = array(
+			'http' => array(
+					'header' => array(
+						'Connection: close'
+					),
+					'method' => 'HEAD',
+					'timeout' => $timeout,
+					'protocol_version'=> 1.1)
+			);
+
+		if (Config::get(Config::HTTP_PROXY)) {
+			$context_options['http']['request_fulluri'] = true;
+			$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
+		}
 
-			$context = stream_context_create($context_options);
+		$context = stream_context_create($context_options);
 
-			// PHP 8 changed the second param from int to bool, but we still support PHP >= 7.1.0
-			// @phpstan-ignore-next-line
-			$headers = get_headers($url, 0, $context);
-		} else {
-			// PHP 8 changed the second param from int to bool, but we still support PHP >= 7.1.0
-			// @phpstan-ignore-next-line
-			$headers = get_headers($url, 0);
-		}
+		// PHP 8 changed the second param from int to bool, but we still support PHP >= 7.4.0
+		// @phpstan-ignore-next-line
+		$headers = get_headers($url, 0, $context);
 
 		if (is_array($headers)) {
 			$headers = array_reverse($headers); // last one is the correct one
diff --git a/include/functions.php b/include/functions.php
index 82e08ddb3ada590e06f6fc545fda8e1b3c68a698..a7b15c1655cbae931f27ffa5daa66d7ea4b5c20b 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -108,8 +108,7 @@
 				$valid_langs[$lang] = $t;
 
 				$lang = substr($lang, 0, 2);
-				if (!isset($valid_langs[$lang]))
-					$valid_langs[$lang] = $t;
+				$valid_langs[$lang] ??= $t;
 			}
 
 			// break up string into pieces (languages and q factors)
diff --git a/plugins/af_comics/init.php b/plugins/af_comics/init.php
index a9a8f3faaa1c280b1c275a6fd0aaca74f044062b..0649cf92c29086a4865797c62bc84b319a4d0e65 100755
--- a/plugins/af_comics/init.php
+++ b/plugins/af_comics/init.php
@@ -19,7 +19,10 @@ class Af_Comics extends Plugin {
 
 		require_once __DIR__ . "/filter_base.php";
 
-		$filters = array_merge(glob(__DIR__ . "/filters.local/*.php"), glob(__DIR__ . "/filters/*.php"));
+		$filters = [
+			...(glob(__DIR__ . "/filters.local/*.php") ?: []),
+			...(glob(__DIR__ . "/filters/*.php") ?: []),
+		];
 		$names = [];
 
 		foreach ($filters as $file) {
diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php
index 8a0d7d99e5a81c6440a294d97bd80c3a7147b08e..898f0b49cbd9824281fab52ae8b227b800589297 100755
--- a/plugins/af_redditimgur/init.php
+++ b/plugins/af_redditimgur/init.php
@@ -376,11 +376,11 @@ class Af_RedditImgur extends Plugin {
 		}
 
 		if ($post_is_nsfw && count($apply_nsfw_tags) > 0) {
-			$article["tags"] = array_merge($article["tags"], $apply_nsfw_tags);
+			array_push($article["tags"], ...$apply_nsfw_tags);
 		}
 
 		if (count($link_flairs) > 0) {
-			$article["tags"] = array_merge($article["tags"], FeedItem_Common::normalize_categories($link_flairs));
+			array_push($article["tags"], ...FeedItem_Common::normalize_categories($link_flairs));
 		}
 
 		$article["num_comments"] = $num_comments;
@@ -903,7 +903,7 @@ class Af_RedditImgur extends Plugin {
 
 			// do not try to embed posts linking back to other reddit posts
 			// readability.php requires PHP 5.6
-			if ($url &&	strpos($url, "reddit.com") === false && version_compare(PHP_VERSION, '5.6.0', '>=')) {
+			if ($url &&	strpos($url, "reddit.com") === false) {
 
 				/* link may lead to a huge video file or whatever, we need to check content type before trying to
 				parse it which p much requires curl */
@@ -937,7 +937,7 @@ class Af_RedditImgur extends Plugin {
 		$src_domain = parse_url($src, PHP_URL_HOST);
 
 		if ($src_domain)
-			foreach (array_merge($this->domain_blacklist, $also_blacklist) as $domain) {
+		foreach ([...$this->domain_blacklist, ...$also_blacklist] as $domain) {
 				if (strstr($src_domain, $domain) !== false) {
 					return true;
 				}
diff --git a/plugins/cache_starred_images/init.php b/plugins/cache_starred_images/init.php
index feec81d62f785a28a17244d9843eaed0bb1a0ccc..eed5264c609a70596665402f7af0b3d6531285ea 100755
--- a/plugins/cache_starred_images/init.php
+++ b/plugins/cache_starred_images/init.php
@@ -84,10 +84,10 @@ class Cache_Starred_Images extends Plugin {
 
 		Debug::log("expiring {$this->cache->get_dir()} and {$this->cache_status->get_dir()}...");
 
-		$files = array_merge(
-			glob($this->cache->get_dir() . "/*-*"),
-			glob($this->cache_status->get_dir() . "/*.status")
-		);
+		$files = [
+			...(glob($this->cache->get_dir() . "/*-*") ?: []),
+			...(glob($this->cache_status->get_dir() . "/*.status") ?: []),
+		];
 
 		asort($files);