From 6b74a9e98ac89d1ebd6b7f8a69525e5ddfa1d220 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Fri, 14 May 2021 17:59:35 +0000
Subject: [PATCH] refactor: update code base to php 8 and set phpstan lvl to 6

---
 .gitlab-ci.yml                                |   2 +-
 .prettierrc.json                              |   2 +-
 Dockerfile                                    |   6 +-
 INSTALL.md                                    |   6 +-
 app/Authorization/FlatAuthorization.php       |   4 +-
 app/Config/ActivityPub.php                    |  20 +-
 app/Config/Analytics.php                      |   1 +
 app/Config/Database.php                       |  16 +-
 app/Config/Exceptions.php                     |   2 +-
 app/Config/Filters.php                        |   8 +-
 app/Config/Kint.php                           |  15 +-
 app/Config/Logger.php                         |  16 +-
 app/Config/Routes.php                         |   6 +-
 app/Config/Services.php                       |  29 +-
 app/Config/View.php                           |   4 +-
 app/Controllers/ActorController.php           |   5 +-
 app/Controllers/Admin/BaseController.php      |   2 +-
 .../Admin/ContributorController.php           |  25 +-
 app/Controllers/Admin/EpisodeController.php   | 100 +-
 .../Admin/EpisodePersonController.php         |  29 +-
 app/Controllers/Admin/FediverseController.php |   6 +-
 app/Controllers/Admin/HomeController.php      |   3 +-
 app/Controllers/Admin/MyAccountController.php |   7 +-
 app/Controllers/Admin/PageController.php      |  17 +-
 app/Controllers/Admin/PersonController.php    |  23 +-
 app/Controllers/Admin/PodcastController.php   |  13 +-
 .../Admin/PodcastImportController.php         | 259 +++--
 .../Admin/PodcastPersonController.php         |  22 +-
 .../Admin/PodcastPlatformController.php       |  23 +-
 app/Controllers/Admin/UserController.php      |  23 +-
 app/Controllers/AuthController.php            |  16 +-
 app/Controllers/BaseController.php            |   2 +-
 app/Controllers/EpisodeController.php         |  48 +-
 app/Controllers/HomeController.php            |   8 +-
 app/Controllers/InstallController.php         |  52 +-
 app/Controllers/NoteController.php            |  52 +-
 app/Controllers/PageController.php            |   6 +-
 app/Controllers/PodcastController.php         |  61 +-
 .../Seeds/FakePodcastsAnalyticsSeeder.php     |   2 +-
 app/Entities/Actor.php                        |  11 +-
 app/Entities/Category.php                     |   5 +-
 app/Entities/Credit.php                       |  37 +-
 app/Entities/Episode.php                      | 166 +---
 app/Entities/EpisodePerson.php                |  48 -
 app/Entities/Image.php                        |  31 +-
 app/Entities/Note.php                         |  14 +-
 app/Entities/Page.php                         |  13 +-
 app/Entities/Person.php                       |  17 +-
 app/Entities/Podcast.php                      | 102 +-
 app/Entities/PodcastPerson.php                |  46 -
 app/Filters/PermissionFilter.php              |   8 +-
 app/Helpers/auth_helper.php                   |   6 +-
 app/Helpers/breadcrumb_helper.php             |   5 +-
 app/Helpers/components_helper.php             |  25 +-
 app/Helpers/form_helper.php                   |  19 +-
 app/Helpers/id3_helper.php                    |   2 +-
 app/Helpers/location_helper.php               |   4 +
 app/Helpers/media_helper.php                  |  10 +-
 app/Helpers/misc_helper.php                   |  16 +-
 app/Helpers/persons_helper.php                |  56 --
 app/Helpers/rss_helper.php                    |  59 +-
 app/Helpers/url_helper.php                    |   4 +-
 .../Activities/AnnounceActivity.php           |   3 +-
 app/Libraries/ActivityPub/ActivityRequest.php |   4 +-
 .../ActivityPub/Config/ActivityPub.php        |   6 +-
 .../Controllers/ActorController.php           |   9 +-
 .../Controllers/BlockController.php           |   9 +-
 .../Controllers/NoteController.php            |  10 +-
 .../Controllers/WebFingerController.php       |   2 +-
 .../ActivityPub/Core/AbstractObject.php       |   7 +-
 app/Libraries/ActivityPub/Core/ObjectType.php |   6 +-
 app/Libraries/ActivityPub/Entities/Note.php   |   2 +-
 .../ActivityPub/Filters/ActivityPubFilter.php |   6 +-
 .../Helpers/activitypub_helper.php            |  25 +-
 app/Libraries/ActivityPub/HttpSignature.php   |   8 +-
 .../ActivityPub/Models/ActivityModel.php      |  12 +-
 .../ActivityPub/Models/ActorModel.php         |  15 +-
 .../ActivityPub/Models/BlockedDomainModel.php |  19 +-
 .../ActivityPub/Models/FavouriteModel.php     |   6 +-
 .../ActivityPub/Models/FollowModel.php        |   4 +-
 .../ActivityPub/Models/NoteModel.php          |  56 +-
 .../ActivityPub/Models/PreviewCardModel.php   |   9 +-
 .../ActivityPub/Objects/ActorObject.php       |   6 +-
 .../ActivityPub/Objects/NoteObject.php        |   5 +-
 .../Objects/OrderedCollectionObject.php       |  11 +-
 app/Libraries/ActivityPub/WebFinger.php       |  22 +-
 app/Libraries/Analytics/AnalyticsTrait.php    |   1 +
 app/Libraries/Analytics/Config/Analytics.php  |   2 +-
 .../Controllers/AnalyticsController.php       |   6 +-
 .../EpisodeAnalyticsController.php            |   2 +-
 .../Controllers/EpisodeController.php         | 200 ++++
 .../UnknownUserAgentsController.php           |   4 +-
 .../Entities/AnalyticsPodcastsByCountry.php   |   2 +-
 .../Entities/AnalyticsPodcastsByRegion.php    |   2 +-
 .../Entities/AnalyticsPodcastsByService.php   |   2 +-
 ...ents.php => AnalyticsUnknownUserAgent.php} |   9 +-
 .../Analytics/Helpers/analytics_helper.php    |  21 +-
 .../AnalyticsUnknownUseragentsModel.php       |  18 +-
 .../Models/UnknownUserAgentsModel.php         |  26 -
 app/Libraries/Breadcrumb.php                  |  11 +-
 app/Libraries/Negotiate.php                   |   3 +
 app/Libraries/Router.php                      |  14 +-
 app/Models/ActorModel.php                     |   3 +
 app/Models/CategoryModel.php                  |  18 +-
 app/Models/EpisodeModel.php                   |  14 +-
 app/Models/EpisodePersonModel.php             | 159 ----
 app/Models/LanguageModel.php                  |   8 +-
 app/Models/NoteModel.php                      |  13 +-
 app/Models/PageModel.php                      |   2 +
 app/Models/PersonModel.php                    | 211 +++-
 app/Models/PlatformModel.php                  |  52 +-
 app/Models/PodcastModel.php                   | 118 ++-
 app/Models/PodcastPersonModel.php             | 159 ----
 app/Models/SoundbiteModel.php                 |   6 +-
 app/Models/UserModel.php                      |  11 +-
 app/Validation/FileRules.php                  |   2 +-
 app/Views/errors/cli/error_exception.php      |   4 +-
 composer.json                                 |  12 +-
 composer.lock                                 | 901 ++++++++----------
 phpstan.neon                                  |  10 +-
 public/index.php                              |   6 +-
 rector.php                                    |  21 +-
 spark                                         |   2 +-
 tests/unit/HealthTest.php                     |   5 +-
 124 files changed, 1810 insertions(+), 2157 deletions(-)
 delete mode 100644 app/Entities/EpisodePerson.php
 delete mode 100644 app/Entities/PodcastPerson.php
 delete mode 100644 app/Helpers/persons_helper.php
 create mode 100644 app/Libraries/Analytics/Controllers/EpisodeController.php
 rename app/Libraries/Analytics/Entities/{AnalyticsUnknownUseragents.php => AnalyticsUnknownUserAgent.php} (74%)
 delete mode 100644 app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
 delete mode 100644 app/Models/EpisodePersonModel.php
 delete mode 100644 app/Models/PodcastPersonModel.php

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0ce56bb888..62cc6540dc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: php:7.3-fpm
+image: php:8.0-fpm
 
 stages:
   - quality
diff --git a/.prettierrc.json b/.prettierrc.json
index a766ac8d82..4f59557607 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -4,7 +4,7 @@
     {
       "files": "*.php",
       "options": {
-        "phpVersion": "7.3",
+        "phpVersion": "7.4",
         "singleQuote": true
       }
     },
diff --git a/Dockerfile b/Dockerfile
index fce76183d6..44e3f9a5c2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@
 # should be used only for development purposes
 ####################################################
 
-FROM php:7.3-fpm
+FROM php:8.0-fpm
 
 LABEL maintainer="Yassine Doghri<yassine@podlibre.org>"
 
@@ -37,8 +37,8 @@ RUN apt-get update && apt-get install -y \
     zlib1g-dev \
     && docker-php-ext-install intl
 
-RUN docker-php-ext-configure gd --with-jpeg-dir=/usr/include/ \
-    && docker-php-ext-install gd
+RUN docker-php-ext-configure gd --with-jpeg \
+    && docker-php-ext-install gd    
 
 RUN pecl install -o -f redis \
     &&  rm -rf /tmp/pear \
diff --git a/INSTALL.md b/INSTALL.md
index 28bb59b148..295c4e740f 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -10,7 +10,7 @@ or shared hosting, you can install it on most PHP-MySQL compatible web servers.
   - [1. Install Wizard](#1-install-wizard)
   - [1-alt Manual configuration](#1-alt-manual-configuration)
 - [Web Server Requirements](#web-server-requirements)
-  - [PHP v7.3 or higher](#php-v73-or-higher)
+  - [PHP v8.0 or higher](#php-v73-or-higher)
   - [MySQL compatible database](#mysql-compatible-database)
     - [Privileges](#privileges)
   - [(Optional) Other recommendations](#optional-other-recommendations)
@@ -59,9 +59,9 @@ through the install wizard, you can create and update the `.env` file yourself:
 
 ## Web Server Requirements
 
-### PHP v7.3 or higher
+### PHP v8.0 or higher
 
-PHP version 7.3 or higher is required, with the following extensions installed:
+PHP version 8.0 or higher is required, with the following extensions installed:
 
 - [intl](https://php.net/manual/en/intl.requirements.php)
 - [libcurl](https://php.net/manual/en/curl.requirements.php)
diff --git a/app/Authorization/FlatAuthorization.php b/app/Authorization/FlatAuthorization.php
index b5af66d5b0..f7015aa543 100644
--- a/app/Authorization/FlatAuthorization.php
+++ b/app/Authorization/FlatAuthorization.php
@@ -17,10 +17,8 @@ class FlatAuthorization extends MythAuthFlatAuthorization
 
     /**
      * Checks a group to see if they have the specified permission.
-     *
-     * @param int|string $permission
      */
-    public function groupHasPermission($permission, int $groupId): bool
+    public function groupHasPermission(int|string $permission, int $groupId): bool
     {
         // Get the Permission ID
         $permissionId = $this->getPermissionID($permission);
diff --git a/app/Config/ActivityPub.php b/app/Config/ActivityPub.php
index 062603dbc1..976064542c 100644
--- a/app/Config/ActivityPub.php
+++ b/app/Config/ActivityPub.php
@@ -1,5 +1,7 @@
 <?php namespace Config;
 
+use App\Libraries\PodcastActor;
+use App\Libraries\NoteObject;
 use ActivityPub\Config\ActivityPub as ActivityPubBase;
 
 class ActivityPub extends ActivityPubBase
@@ -8,18 +10,32 @@ class ActivityPub extends ActivityPubBase
      * --------------------------------------------------------------------
      * ActivityPub Objects
      * --------------------------------------------------------------------
+     * @var string
      */
-    public $actorObject = 'App\Libraries\PodcastActor';
-    public $noteObject = 'App\Libraries\NoteObject';
+    public $actorObject = PodcastActor::class;
+    /**
+     * @var string
+     */
+    public $noteObject = NoteObject::class;
 
     /**
      * --------------------------------------------------------------------
      * Default avatar and cover images
      * --------------------------------------------------------------------
+     * @var string
      */
     public $defaultAvatarImagePath = 'assets/images/castopod-avatar-default.jpg';
+    /**
+     * @var string
+     */
     public $defaultAvatarImageMimetype = 'image/jpeg';
 
+    /**
+     * @var string
+     */
     public $defaultCoverImagePath = 'assets/images/castopod-cover-default.jpg';
+    /**
+     * @var string
+     */
     public $defaultCoverImageMimetype = 'image/jpeg';
 }
diff --git a/app/Config/Analytics.php b/app/Config/Analytics.php
index df73c9a955..66fe694573 100644
--- a/app/Config/Analytics.php
+++ b/app/Config/Analytics.php
@@ -10,6 +10,7 @@ class Analytics extends AnalyticsBase
      * --------------------------------------------------------------------
      * Route filters options
      * --------------------------------------------------------------------
+     * @var array<string, string>
      */
     public $routeFilters = [
         'analytics-full-data' => 'permission:podcasts-view,podcast-view',
diff --git a/app/Config/Database.php b/app/Config/Database.php
index 4ee26d9513..a854688a5a 100644
--- a/app/Config/Database.php
+++ b/app/Config/Database.php
@@ -12,25 +12,21 @@ class Database extends Config
     /**
      * The directory that holds the Migrations
      * and Seeds directories.
-     *
-     * @var string
      */
-    public $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
+    public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
 
     /**
      * Lets you choose which connection group to
      * use if no other is specified.
-     *
-     * @var string
      */
-    public $defaultGroup = 'default';
+    public string $defaultGroup = 'default';
 
     /**
      * The default database connection.
      *
-     * @var array
+     * @var array<string, string|bool|int|array>
      */
-    public $default = [
+    public array $default = [
         'DSN' => '',
         'hostname' => 'localhost',
         'username' => '',
@@ -54,9 +50,9 @@ class Database extends Config
      * This database connection is used when
      * running PHPUnit database tests.
      *
-     * @var array
+     * @var array<string, string|bool|int|array>
      */
-    public $tests = [
+    public array $tests = [
         'DSN' => '',
         'hostname' => '127.0.0.1',
         'username' => '',
diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php
index b3bef25c53..47b8575762 100644
--- a/app/Config/Exceptions.php
+++ b/app/Config/Exceptions.php
@@ -29,7 +29,7 @@ class Exceptions extends BaseConfig
      * Any status codes here will NOT be logged if logging is turned on.
      * By default, only 404 (Page Not Found) exceptions are ignored.
      *
-     * @var array
+     * @var int[]
      */
     public $ignoreCodes = [404];
 
diff --git a/app/Config/Filters.php b/app/Config/Filters.php
index 6a70932800..573fc1ee23 100644
--- a/app/Config/Filters.php
+++ b/app/Config/Filters.php
@@ -17,7 +17,7 @@ class Filters extends BaseConfig
      * Configures aliases for Filter classes to
      * make reading things nicer and simpler.
      *
-     * @var array
+     * @var array<string, string>
      */
     public $aliases = [
         'csrf' => CSRF::class,
@@ -33,7 +33,7 @@ class Filters extends BaseConfig
      * List of filter aliases that are always
      * applied before and after every request.
      *
-     * @var array
+     * @var array<string, string[]>
      */
     public $globals = [
         'before' => [
@@ -53,7 +53,7 @@ class Filters extends BaseConfig
      * Example:
      * 'post' => ['csrf', 'throttle']
      *
-     * @var array
+     * @var array<string, string[]>
      */
     public $methods = [];
 
@@ -64,7 +64,7 @@ class Filters extends BaseConfig
      * Example:
      * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
      *
-     * @var array
+     * @var array<string, array<string, string[]>>
      */
     public $filters = [];
 
diff --git a/app/Config/Kint.php b/app/Config/Kint.php
index 8d5ed9e0ce..ce2ff28a8f 100644
--- a/app/Config/Kint.php
+++ b/app/Config/Kint.php
@@ -23,7 +23,10 @@ class Kint extends BaseConfig
 	|--------------------------------------------------------------------------
 	*/
 
-    public $plugins;
+    /**
+     * @var string[]
+     */
+    public $plugins = [];
 
     /**
      * @var int
@@ -60,9 +63,15 @@ class Kint extends BaseConfig
      */
     public $richSort = Renderer::SORT_FULL;
 
-    public $richObjectPlugins;
+    /**
+     * @var string[]
+     */
+    public $richObjectPlugins = [];
 
-    public $richTabPlugins;
+    /**
+     * @var string[]
+     */
+    public $richTabPlugins = [];
 
     /*
     |--------------------------------------------------------------------------
diff --git a/app/Config/Logger.php b/app/Config/Logger.php
index 5607dcfb32..cfee275721 100644
--- a/app/Config/Logger.php
+++ b/app/Config/Logger.php
@@ -36,9 +36,9 @@ class Logger extends BaseConfig
      * For a live site you'll usually enable Critical or higher (3) to be logged otherwise
      * your log files will fill up very fast.
      *
-     * @var integer|array
+     * @var integer|int[]
      */
-    public $threshold = 4;
+    public int|array $threshold = 4;
 
     /**
      * --------------------------------------------------------------------------
@@ -50,7 +50,7 @@ class Logger extends BaseConfig
      *
      * @var string
      */
-    public $dateFormat = 'Y-m-d H:i:s';
+    public string $dateFormat = 'Y-m-d H:i:s';
 
     /**
      * --------------------------------------------------------------------------
@@ -75,9 +75,9 @@ class Logger extends BaseConfig
      * Handlers are executed in the order defined in this array, starting with
      * the handler on top and continuing down.
      *
-     * @var array
+     * @var array<string, string|int|array<string, string>>
      */
-    public $handlers = [
+    public array $handlers = [
         /*
          * --------------------------------------------------------------------
          * File Handler
@@ -125,9 +125,9 @@ class Logger extends BaseConfig
         ],
 
         /**
-         * The ChromeLoggerHandler requires the use of the Chrome web browser
-         * and the ChromeLogger extension. Uncomment this block to use it.
-         */
+     * The ChromeLoggerHandler requires the use of the Chrome web browser
+     * and the ChromeLogger extension. Uncomment this block to use it.
+     */
         //      'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
         //          /*
         //           * The log levels that this handler will handle.
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 4539867c1e..a279b940ff 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -164,13 +164,13 @@ $routes->group(
                 ]);
 
                 $routes->group('persons', function ($routes): void {
-                    $routes->get('/', 'PodcastPersonController/$1', [
+                    $routes->get('/', 'PodcastPodcastController/$1', [
                         'as' => 'podcast-person-manage',
                         'filter' => 'permission:podcast-edit',
                     ]);
                     $routes->post(
                         '/',
-                        'PodcastPersonController::attemptAdd/$1',
+                        'PodcastPodcastController::attemptAdd/$1',
                         [
                             'filter' => 'permission:podcast-edit',
                         ],
@@ -178,7 +178,7 @@ $routes->group(
 
                     $routes->get(
                         '(:num)/remove',
-                        'PodcastPersonController::remove/$1/$2',
+                        'PodcastPodcastController::remove/$1/$2',
                         [
                             'as' => 'podcast-person-remove',
                             'filter' => 'permission:podcast-edit',
diff --git a/app/Config/Services.php b/app/Config/Services.php
index 7ea9a3dd60..5228dd391a 100644
--- a/app/Config/Services.php
+++ b/app/Config/Services.php
@@ -34,18 +34,12 @@ class Services extends BaseService
     /**
      * The Router class uses a RouteCollection's array of routes, and determines
      * the correct Controller and Method to execute.
-     *
-     * @param RouteCollectionInterface|null $routes
-     * @param Request|null                  $request
-     * @param boolean                       $getShared
-     *
-     * @return Router
      */
     public static function router(
-        RouteCollectionInterface $routes = null,
-        Request $request = null,
+        ?RouteCollectionInterface $routes = null,
+        ?Request $request = null,
         bool $getShared = true
-    ) {
+    ): Router {
         if ($getShared) {
             return static::getSharedInstance('router', $routes, $request);
         }
@@ -60,16 +54,11 @@ class Services extends BaseService
      * The Negotiate class provides the content negotiation features for
      * working the request to determine correct language, encoding, charset,
      * and more.
-     *
-     * @param RequestInterface|null $request
-     * @param boolean               $getShared
-     *
-     * @return Negotiate
      */
     public static function negotiator(
-        RequestInterface $request = null,
+        ?RequestInterface $request = null,
         bool $getShared = true
-    ) {
+    ): Negotiate {
         if ($getShared) {
             return static::getSharedInstance('negotiator', $request);
         }
@@ -79,6 +68,9 @@ class Services extends BaseService
         return new Negotiate($request);
     }
 
+    /**
+     * @return mixed
+     */
     public static function authentication(
         string $lib = 'local',
         Model $userModel = null,
@@ -112,6 +104,9 @@ class Services extends BaseService
         return $instance->setUserModel($userModel)->setLoginModel($loginModel);
     }
 
+    /**
+     * @return mixed
+     */
     public static function authorization(
         Model $groupModel = null,
         Model $permissionModel = null,
@@ -144,7 +139,7 @@ class Services extends BaseService
         return $instance->setUserModel($userModel);
     }
 
-    public static function breadcrumb(bool $getShared = true)
+    public static function breadcrumb(bool $getShared = true): Breadcrumb
     {
         if ($getShared) {
             return self::getSharedInstance('breadcrumb');
diff --git a/app/Config/View.php b/app/Config/View.php
index 7697cfc927..5014fdf157 100644
--- a/app/Config/View.php
+++ b/app/Config/View.php
@@ -29,7 +29,7 @@ class View extends BaseView
      *  { title|esc(js) }
      *  { created_on|date(Y-m-d)|esc(attr) }
      *
-     * @var array
+     * @var string[]
      */
     public $filters = [];
 
@@ -38,7 +38,7 @@ class View extends BaseView
      * by the core Parser by creating aliases that will be replaced with
      * any callable. Can be single or tag pair.
      *
-     * @var array
+     * @var string[]
      */
     public $plugins = [];
 }
diff --git a/app/Controllers/ActorController.php b/app/Controllers/ActorController.php
index cbc27c1c95..15cd0fae48 100644
--- a/app/Controllers/ActorController.php
+++ b/app/Controllers/ActorController.php
@@ -15,9 +15,12 @@ class ActorController extends ActivityPubActorController
 {
     use AnalyticsTrait;
 
+    /**
+     * @var string[]
+     */
     protected $helpers = ['auth', 'svg', 'components', 'misc'];
 
-    public function follow()
+    public function follow(): string
     {
         // Prevent analytics hit when authenticated
         if (!can_user_interact()) {
diff --git a/app/Controllers/Admin/BaseController.php b/app/Controllers/Admin/BaseController.php
index 43deabd6cc..3150118664 100644
--- a/app/Controllers/Admin/BaseController.php
+++ b/app/Controllers/Admin/BaseController.php
@@ -25,7 +25,7 @@ class BaseController extends Controller
      * class instantiation. These helpers will be available
      * to all other controllers that extend BaseController.
      *
-     * @var array
+     * @var string[]
      */
     protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc'];
 
diff --git a/app/Controllers/Admin/ContributorController.php b/app/Controllers/Admin/ContributorController.php
index 2726abb1f5..bcdc22145e 100644
--- a/app/Controllers/Admin/ContributorController.php
+++ b/app/Controllers/Admin/ContributorController.php
@@ -15,6 +15,7 @@ use Exception;
 use App\Authorization\GroupModel;
 use App\Models\PodcastModel;
 use App\Models\UserModel;
+use CodeIgniter\HTTP\RedirectResponse;
 
 class ContributorController extends BaseController
 {
@@ -28,9 +29,9 @@ class ContributorController extends BaseController
      */
     protected $user;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
-        $this->podcast = (new PodcastModel())->getPodcastById($params[0]);
+        $this->podcast = (new PodcastModel())->getPodcastById((int) $params[0]);
 
         if (count($params) <= 1) {
             return $this->$method();
@@ -38,8 +39,8 @@ class ContributorController extends BaseController
 
         if (
             $this->user = (new UserModel())->getPodcastContributor(
-                $params[1],
-                $params[0],
+                (int) $params[1],
+                (int) $params[0],
             )
         ) {
             return $this->$method();
@@ -48,7 +49,7 @@ class ContributorController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function list()
+    public function list(): string
     {
         $data = [
             'podcast' => $this->podcast,
@@ -58,7 +59,7 @@ class ContributorController extends BaseController
         return view('admin/contributor/list', $data);
     }
 
-    public function view()
+    public function view(): string
     {
         $data = [
             'contributor' => (new UserModel())->getPodcastContributor(
@@ -74,7 +75,7 @@ class ContributorController extends BaseController
         return view('admin/contributor/view', $data);
     }
 
-    public function add()
+    public function add(): string
     {
         helper('form');
 
@@ -108,7 +109,7 @@ class ContributorController extends BaseController
         return view('admin/contributor/add', $data);
     }
 
-    public function attemptAdd()
+    public function attemptAdd(): RedirectResponse
     {
         try {
             (new PodcastModel())->addPodcastContributor(
@@ -116,7 +117,7 @@ class ContributorController extends BaseController
                 $this->podcast->id,
                 $this->request->getPost('role'),
             );
-        } catch (Exception $exception) {
+        } catch (Exception) {
             return redirect()
                 ->back()
                 ->withInput()
@@ -128,7 +129,7 @@ class ContributorController extends BaseController
         return redirect()->route('contributor-list', [$this->podcast->id]);
     }
 
-    public function edit()
+    public function edit(): string
     {
         helper('form');
 
@@ -159,7 +160,7 @@ class ContributorController extends BaseController
         return view('admin/contributor/edit', $data);
     }
 
-    public function attemptEdit()
+    public function attemptEdit(): RedirectResponse
     {
         (new PodcastModel())->updatePodcastContributor(
             $this->user->id,
@@ -170,7 +171,7 @@ class ContributorController extends BaseController
         return redirect()->route('contributor-list', [$this->podcast->id]);
     }
 
-    public function remove()
+    public function remove(): RedirectResponse
     {
         if ($this->podcast->created_by === $this->user->id) {
             return redirect()
diff --git a/app/Controllers/Admin/EpisodeController.php b/app/Controllers/Admin/EpisodeController.php
index ed81106e9a..423d1b3ba9 100644
--- a/app/Controllers/Admin/EpisodeController.php
+++ b/app/Controllers/Admin/EpisodeController.php
@@ -8,6 +8,9 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\HTTP\RedirectResponse;
+use Config\Database;
 use App\Entities\Episode;
 use App\Entities\Note;
 use App\Entities\Podcast;
@@ -29,12 +32,14 @@ class EpisodeController extends BaseController
      */
     protected $episode;
 
-    public function _remap(string $method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (
-            !($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
+            ($this->podcast = (new PodcastModel())->getPodcastById(
+                (int) $params[0],
+            )) === null
         ) {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            throw PageNotFoundException::forPageNotFound();
         }
 
         if (count($params) > 1) {
@@ -46,7 +51,7 @@ class EpisodeController extends BaseController
                     ])
                     ->first())
             ) {
-                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+                throw PageNotFoundException::forPageNotFound();
             }
 
             unset($params[1]);
@@ -56,7 +61,7 @@ class EpisodeController extends BaseController
         return $this->$method(...$params);
     }
 
-    public function list()
+    public function list(): string
     {
         $episodes = (new EpisodeModel())
             ->where('podcast_id', $this->podcast->id)
@@ -74,7 +79,7 @@ class EpisodeController extends BaseController
         return view('admin/episode/list', $data);
     }
 
-    public function view()
+    public function view(): string
     {
         $data = [
             'podcast' => $this->podcast,
@@ -88,7 +93,7 @@ class EpisodeController extends BaseController
         return view('admin/episode/view', $data);
     }
 
-    public function create()
+    public function create(): string
     {
         helper(['form']);
 
@@ -102,7 +107,7 @@ class EpisodeController extends BaseController
         return view('admin/episode/create', $data);
     }
 
-    public function attemptCreate()
+    public function attemptCreate(): RedirectResponse
     {
         $rules = [
             'audio_file' => 'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]',
@@ -204,7 +209,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function edit()
+    public function edit(): string
     {
         helper(['form']);
 
@@ -220,7 +225,7 @@ class EpisodeController extends BaseController
         return view('admin/episode/edit', $data);
     }
 
-    public function attemptEdit()
+    public function attemptEdit(): RedirectResponse
     {
         $rules = [
             'audio_file' =>
@@ -282,17 +287,14 @@ class EpisodeController extends BaseController
             }
         } elseif ($transcriptChoice === 'remote-url') {
             if (
-                $transcriptFileRemoteUrl = $this->request->getPost(
+                ($transcriptFileRemoteUrl = $this->request->getPost(
                     'transcript_file_remote_url',
-                )
+                )) &&
+                (($transcriptFile = $this->episode->transcript_file) &&
+                    $transcriptFile !== null)
             ) {
-                if (
-                    ($transcriptFile = $this->episode->transcript_file) &&
-                    $transcriptFile !== null
-                ) {
-                    unlink($transcriptFile);
-                    $this->episode->transcript_file_path = null;
-                }
+                unlink($transcriptFile);
+                $this->episode->transcript_file_path = null;
             }
             $this->episode->transcript_file_remote_url = $transcriptFileRemoteUrl;
         }
@@ -306,17 +308,14 @@ class EpisodeController extends BaseController
             }
         } elseif ($chaptersChoice === 'remote-url') {
             if (
-                $chaptersFileRemoteUrl = $this->request->getPost(
+                ($chaptersFileRemoteUrl = $this->request->getPost(
                     'chapters_file_remote_url',
-                )
+                )) &&
+                (($chaptersFile = $this->episode->chapters_file) &&
+                    $chaptersFile !== null)
             ) {
-                if (
-                    ($chaptersFile = $this->episode->chapters_file) &&
-                    $chaptersFile !== null
-                ) {
-                    unlink($chaptersFile);
-                    $this->episode->chapters_file_path = null;
-                }
+                unlink($chaptersFile);
+                $this->episode->chapters_file_path = null;
             }
             $this->episode->chapters_file_remote_url = $chaptersFileRemoteUrl;
         }
@@ -351,7 +350,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function transcriptDelete()
+    public function transcriptDelete(): RedirectResponse
     {
         unlink($this->episode->transcript_file);
         $this->episode->transcript_file_path = null;
@@ -368,7 +367,7 @@ class EpisodeController extends BaseController
         return redirect()->back();
     }
 
-    public function chaptersDelete()
+    public function chaptersDelete(): RedirectResponse
     {
         unlink($this->episode->chapters_file);
         $this->episode->chapters_file_path = null;
@@ -385,7 +384,7 @@ class EpisodeController extends BaseController
         return redirect()->back();
     }
 
-    public function publish()
+    public function publish(): string
     {
         if ($this->episode->publication_status === 'not_published') {
             helper(['form']);
@@ -400,12 +399,12 @@ class EpisodeController extends BaseController
                 1 => $this->episode->title,
             ]);
             return view('admin/episode/publish', $data);
-        } else {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
         }
+
+        throw PageNotFoundException::forPageNotFound();
     }
 
-    public function attemptPublish()
+    public function attemptPublish(): RedirectResponse
     {
         $rules = [
             'publication_method' => 'required',
@@ -420,7 +419,7 @@ class EpisodeController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
-        $db = \Config\Database::connect();
+        $db = Database::connect();
         $db->transStart();
 
         $newNote = new Note([
@@ -482,7 +481,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function publishEdit()
+    public function publishEdit(): string
     {
         if ($this->episode->publication_status === 'scheduled') {
             helper(['form']);
@@ -503,12 +502,11 @@ class EpisodeController extends BaseController
                 1 => $this->episode->title,
             ]);
             return view('admin/episode/publish_edit', $data);
-        } else {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
         }
+        throw PageNotFoundException::forPageNotFound();
     }
 
-    public function attemptPublishEdit()
+    public function attemptPublishEdit(): RedirectResponse
     {
         $rules = [
             'note_id' => 'required',
@@ -524,7 +522,7 @@ class EpisodeController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
-        $db = \Config\Database::connect();
+        $db = Database::connect();
         $db->transStart();
 
         $note = (new NoteModel())->getNoteById(
@@ -584,7 +582,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function unpublish()
+    public function unpublish(): string
     {
         if ($this->episode->publication_status === 'published') {
             helper(['form']);
@@ -599,12 +597,12 @@ class EpisodeController extends BaseController
                 1 => $this->episode->title,
             ]);
             return view('admin/episode/unpublish', $data);
-        } else {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
         }
+
+        throw PageNotFoundException::forPageNotFound();
     }
 
-    public function attemptUnpublish()
+    public function attemptUnpublish(): RedirectResponse
     {
         $rules = [
             'understand' => 'required',
@@ -617,7 +615,7 @@ class EpisodeController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
-        $db = \Config\Database::connect();
+        $db = Database::connect();
 
         $db->transStart();
 
@@ -650,14 +648,14 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function delete()
+    public function delete(): RedirectResponse
     {
         (new EpisodeModel())->delete($this->episode->id);
 
         return redirect()->route('episode-list', [$this->podcast->id]);
     }
 
-    public function soundbitesEdit()
+    public function soundbitesEdit(): string
     {
         helper(['form']);
 
@@ -673,7 +671,7 @@ class EpisodeController extends BaseController
         return view('admin/episode/soundbites', $data);
     }
 
-    public function soundbitesAttemptEdit()
+    public function soundbitesAttemptEdit(): RedirectResponse
     {
         $soundbites_array = $this->request->getPost('soundbites_array');
         $rules = [
@@ -682,7 +680,7 @@ class EpisodeController extends BaseController
             'soundbites_array.0.duration' =>
                 'permit_empty|required_with[soundbites_array.0.start_time]|decimal|greater_than_equal_to[0]',
         ];
-        foreach ($soundbites_array as $soundbite_id => $soundbite) {
+        foreach (array_keys($soundbites_array) as $soundbite_id) {
             $rules += [
                 "soundbites_array.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
                 "soundbites_array.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]',
@@ -728,7 +726,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function soundbiteDelete($soundbiteId)
+    public function soundbiteDelete(int $soundbiteId): RedirectResponse
     {
         (new SoundbiteModel())->deleteSoundbite(
             $this->podcast->id,
@@ -742,7 +740,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function embeddablePlayer()
+    public function embeddablePlayer(): string
     {
         helper(['form']);
 
diff --git a/app/Controllers/Admin/EpisodePersonController.php b/app/Controllers/Admin/EpisodePersonController.php
index 7c882513f8..c38c571c42 100644
--- a/app/Controllers/Admin/EpisodePersonController.php
+++ b/app/Controllers/Admin/EpisodePersonController.php
@@ -8,27 +8,20 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Podcast;
 use App\Entities\Episode;
 use CodeIgniter\Exceptions\PageNotFoundException;
-use App\Models\EpisodePersonModel;
 use App\Models\PodcastModel;
 use App\Models\EpisodeModel;
 use App\Models\PersonModel;
 
 class EpisodePersonController extends BaseController
 {
-    /**
-     * @var Podcast
-     */
-    protected $podcast;
+    protected Podcast $podcast;
+    protected Episode $episode;
 
-    /**
-     * @var Episode
-     */
-    protected $episode;
-
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) <= 2) {
             throw PageNotFoundException::forPageNotFound();
@@ -36,7 +29,7 @@ class EpisodePersonController extends BaseController
 
         if (
             ($this->podcast = (new PodcastModel())->getPodcastById(
-                $params[0],
+                (int) $params[0],
             )) &&
             ($this->episode = (new EpisodeModel())
                 ->where([
@@ -54,14 +47,14 @@ class EpisodePersonController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         helper('form');
 
         $data = [
             'episode' => $this->episode,
             'podcast' => $this->podcast,
-            'episodePersons' => (new EpisodePersonModel())->getEpisodePersons(
+            'episodePersons' => (new PersonModel())->getEpisodePersons(
                 $this->podcast->id,
                 $this->episode->id,
             ),
@@ -75,7 +68,7 @@ class EpisodePersonController extends BaseController
         return view('admin/episode/person', $data);
     }
 
-    public function attemptAdd()
+    public function attemptAdd(): RedirectResponse
     {
         $rules = [
             'person' => 'required',
@@ -88,7 +81,7 @@ class EpisodePersonController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
-        (new EpisodePersonModel())->addEpisodePersons(
+        (new PersonModel())->addEpisodePersons(
             $this->podcast->id,
             $this->episode->id,
             $this->request->getPost('person'),
@@ -98,9 +91,9 @@ class EpisodePersonController extends BaseController
         return redirect()->back();
     }
 
-    public function remove($episodePersonId)
+    public function remove(int $episodePersonId): RedirectResponse
     {
-        (new EpisodePersonModel())->removeEpisodePersons(
+        (new PersonModel())->removeEpisodePersons(
             $this->podcast->id,
             $this->episode->id,
             $episodePersonId,
diff --git a/app/Controllers/Admin/FediverseController.php b/app/Controllers/Admin/FediverseController.php
index fd54d103e2..c709dd835d 100644
--- a/app/Controllers/Admin/FediverseController.php
+++ b/app/Controllers/Admin/FediverseController.php
@@ -10,12 +10,12 @@ namespace App\Controllers\Admin;
 
 class FediverseController extends BaseController
 {
-    public function dashboard()
+    public function dashboard(): string
     {
         return view('admin/fediverse/dashboard');
     }
 
-    public function blockedActors()
+    public function blockedActors(): string
     {
         helper(['form']);
 
@@ -26,7 +26,7 @@ class FediverseController extends BaseController
         ]);
     }
 
-    public function blockedDomains()
+    public function blockedDomains(): string
     {
         helper(['form']);
 
diff --git a/app/Controllers/Admin/HomeController.php b/app/Controllers/Admin/HomeController.php
index d2b973f9d7..1b515a6bc1 100644
--- a/app/Controllers/Admin/HomeController.php
+++ b/app/Controllers/Admin/HomeController.php
@@ -8,9 +8,10 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 class HomeController extends BaseController
 {
-    public function index()
+    public function index(): RedirectResponse
     {
         session()->keepFlashdata('message');
         return redirect()->route('podcast-list');
diff --git a/app/Controllers/Admin/MyAccountController.php b/app/Controllers/Admin/MyAccountController.php
index e482e21404..074e0785db 100644
--- a/app/Controllers/Admin/MyAccountController.php
+++ b/app/Controllers/Admin/MyAccountController.php
@@ -8,24 +8,25 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use Config\Services;
 use App\Models\UserModel;
 
 class MyAccountController extends BaseController
 {
-    public function index()
+    public function index(): string
     {
         return view('admin/my_account/view');
     }
 
-    public function changePassword()
+    public function changePassword(): string
     {
         helper('form');
 
         return view('admin/my_account/change_password');
     }
 
-    public function attemptChange()
+    public function attemptChange(): RedirectResponse
     {
         $auth = Services::authentication();
         $userModel = new UserModel();
diff --git a/app/Controllers/Admin/PageController.php b/app/Controllers/Admin/PageController.php
index 520ce08d43..2308f3db8c 100644
--- a/app/Controllers/Admin/PageController.php
+++ b/app/Controllers/Admin/PageController.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Page;
 use CodeIgniter\Exceptions\PageNotFoundException;
 use App\Models\PageModel;
@@ -19,7 +20,7 @@ class PageController extends BaseController
      */
     protected $page;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
@@ -32,7 +33,7 @@ class PageController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    function list()
+    function list(): string
     {
         $data = [
             'pages' => (new PageModel())->findAll(),
@@ -41,19 +42,19 @@ class PageController extends BaseController
         return view('admin/page/list', $data);
     }
 
-    function view()
+    function view(): string
     {
         return view('admin/page/view', ['page' => $this->page]);
     }
 
-    function create()
+    function create(): string
     {
         helper('form');
 
         return view('admin/page/create');
     }
 
-    function attemptCreate()
+    function attemptCreate(): RedirectResponse
     {
         $page = new Page([
             'title' => $this->request->getPost('title'),
@@ -80,7 +81,7 @@ class PageController extends BaseController
             );
     }
 
-    function edit()
+    function edit(): string
     {
         helper('form');
 
@@ -88,7 +89,7 @@ class PageController extends BaseController
         return view('admin/page/edit', ['page' => $this->page]);
     }
 
-    function attemptEdit()
+    function attemptEdit(): RedirectResponse
     {
         $this->page->title = $this->request->getPost('title');
         $this->page->slug = $this->request->getPost('slug');
@@ -106,7 +107,7 @@ class PageController extends BaseController
         return redirect()->route('page-list');
     }
 
-    public function delete()
+    public function delete(): RedirectResponse
     {
         (new PageModel())->delete($this->page->id);
 
diff --git a/app/Controllers/Admin/PersonController.php b/app/Controllers/Admin/PersonController.php
index 513d521d78..7d32264b30 100644
--- a/app/Controllers/Admin/PersonController.php
+++ b/app/Controllers/Admin/PersonController.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Person;
 use CodeIgniter\Exceptions\PageNotFoundException;
 use App\Models\PersonModel;
@@ -19,27 +20,31 @@ class PersonController extends BaseController
      */
     protected $person;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
         }
 
-        if ($this->person = (new PersonModel())->getPersonById($params[0])) {
+        if (
+            ($this->person = (new PersonModel())->getPersonById(
+                (int) $params[0],
+            )) !== null
+        ) {
             return $this->$method();
         }
 
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         $data = ['persons' => (new PersonModel())->findAll()];
 
         return view('admin/person/list', $data);
     }
 
-    public function view()
+    public function view(): string
     {
         $data = ['person' => $this->person];
 
@@ -47,14 +52,14 @@ class PersonController extends BaseController
         return view('admin/person/view', $data);
     }
 
-    public function create()
+    public function create(): string
     {
         helper(['form']);
 
         return view('admin/person/create');
     }
 
-    public function attemptCreate()
+    public function attemptCreate(): RedirectResponse
     {
         $rules = [
             'image' =>
@@ -89,7 +94,7 @@ class PersonController extends BaseController
         return redirect()->route('person-list');
     }
 
-    public function edit()
+    public function edit(): string
     {
         helper('form');
 
@@ -101,7 +106,7 @@ class PersonController extends BaseController
         return view('admin/person/edit', $data);
     }
 
-    public function attemptEdit()
+    public function attemptEdit(): RedirectResponse
     {
         $rules = [
             'image' =>
@@ -138,7 +143,7 @@ class PersonController extends BaseController
         return redirect()->route('person-view', [$this->person->id]);
     }
 
-    public function delete()
+    public function delete(): RedirectResponse
     {
         (new PersonModel())->delete($this->person->id);
 
diff --git a/app/Controllers/Admin/PodcastController.php b/app/Controllers/Admin/PodcastController.php
index 2fdcdc1b6a..47172c908f 100644
--- a/app/Controllers/Admin/PodcastController.php
+++ b/app/Controllers/Admin/PodcastController.php
@@ -26,18 +26,17 @@ class PodcastController extends BaseController
      */
     protected $podcast;
 
-    /**
-     *
-     * @param array<string> $params
-     * @return static|string
-     */
-    public function _remap(string $method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
         }
 
-        if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
+        if (
+            ($this->podcast = (new PodcastModel())->getPodcastById(
+                (int) $params[0],
+            )) !== null
+        ) {
             return $this->$method();
         }
 
diff --git a/app/Controllers/Admin/PodcastImportController.php b/app/Controllers/Admin/PodcastImportController.php
index 82356daede..75ec683294 100644
--- a/app/Controllers/Admin/PodcastImportController.php
+++ b/app/Controllers/Admin/PodcastImportController.php
@@ -8,22 +8,21 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Podcast;
 use CodeIgniter\Exceptions\PageNotFoundException;
 use ErrorException;
 use Config\Database;
 use Podlibre\PodcastNamespace\ReversedTaxonomy;
-use App\Entities\PodcastPerson;
 use App\Entities\Episode;
 use App\Entities\Image;
+use App\Entities\Person;
 use App\Models\CategoryModel;
 use App\Models\LanguageModel;
 use App\Models\PodcastModel;
 use App\Models\EpisodeModel;
 use App\Models\PlatformModel;
 use App\Models\PersonModel;
-use App\Models\PodcastPersonModel;
-use App\Models\EpisodePersonModel;
 use Config\Services;
 use League\HTMLToMarkdown\HtmlConverter;
 
@@ -34,20 +33,20 @@ class PodcastImportController extends BaseController
      */
     protected $podcast;
 
-    public function _remap(string $method, string ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
         }
 
-        if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
+        if (($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null) {
             return $this->$method();
         }
 
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         helper(['form', 'misc']);
 
@@ -65,7 +64,7 @@ class PodcastImportController extends BaseController
         return view('admin/podcast/import', $data);
     }
 
-    public function attemptImport()
+    public function attemptImport(): RedirectResponse
     {
         helper(['media', 'misc']);
 
@@ -92,11 +91,11 @@ class PodcastImportController extends BaseController
                 ->withInput()
                 ->with('errors', [
                     $errorException->getMessage() .
-                    ': <a href="' .
-                    $this->request->getPost('imported_feed_url') .
-                    '" rel="noreferrer noopener" target="_blank">' .
-                    $this->request->getPost('imported_feed_url') .
-                    ' ⎋</a>',
+                        ': <a href="' .
+                        $this->request->getPost('imported_feed_url') .
+                        '" rel="noreferrer noopener" target="_blank">' .
+                        $this->request->getPost('imported_feed_url') .
+                        ' ⎋</a>',
                 ]);
         }
         $nsItunes = $feed->channel[0]->children(
@@ -151,40 +150,40 @@ class PodcastImportController extends BaseController
                 'language_code' => $this->request->getPost('language'),
                 'category_id' => $this->request->getPost('category'),
                 'parental_advisory' =>
-                    $nsItunes->explicit === null
-                        ? null
-                        : (in_array($nsItunes->explicit, ['yes', 'true'])
-                            ? 'explicit'
-                            : (in_array($nsItunes->explicit, ['no', 'false'])
-                                ? 'clean'
-                                : null)),
+                $nsItunes->explicit === null
+                    ? null
+                    : (in_array($nsItunes->explicit, ['yes', 'true'])
+                        ? 'explicit'
+                        : (in_array($nsItunes->explicit, ['no', 'false'])
+                            ? 'clean'
+                            : null)),
                 'owner_name' => (string) $nsItunes->owner->name,
                 'owner_email' => (string) $nsItunes->owner->email,
                 'publisher' => (string) $nsItunes->author,
                 'type' =>
-                    $nsItunes->type === null ? 'episodic' : $nsItunes->type,
+                $nsItunes->type === null ? 'episodic' : $nsItunes->type,
                 'copyright' => (string) $feed->channel[0]->copyright,
                 'is_blocked' =>
-                    $nsItunes->block === null
-                        ? false
-                        : $nsItunes->block === 'yes',
+                $nsItunes->block === null
+                    ? false
+                    : $nsItunes->block === 'yes',
                 'is_completed' =>
-                    $nsItunes->complete === null
-                        ? false
-                        : $nsItunes->complete === 'yes',
+                $nsItunes->complete === null
+                    ? false
+                    : $nsItunes->complete === 'yes',
                 'location_name' => $nsPodcast->location
                     ? (string) $nsPodcast->location
                     : null,
                 'location_geo' =>
-                    !$nsPodcast->location ||
+                !$nsPodcast->location ||
                     $nsPodcast->location->attributes()['geo'] === null
-                        ? null
-                        : (string) $nsPodcast->location->attributes()['geo'],
+                    ? null
+                    : (string) $nsPodcast->location->attributes()['geo'],
                 'location_osm_id' =>
-                    !$nsPodcast->location ||
+                !$nsPodcast->location ||
                     $nsPodcast->location->attributes()['osm'] === null
-                        ? null
-                        : (string) $nsPodcast->location->attributes()['osm'],
+                    ? null
+                    : (string) $nsPodcast->location->attributes()['osm'],
                 'created_by' => user_id(),
                 'updated_by' => user_id(),
             ]);
@@ -194,11 +193,11 @@ class PodcastImportController extends BaseController
                 ->withInput()
                 ->with('errors', [
                     $ex->getMessage() .
-                    ': <a href="' .
-                    $this->request->getPost('imported_feed_url') .
-                    '" rel="noreferrer noopener" target="_blank">' .
-                    $this->request->getPost('imported_feed_url') .
-                    ' ⎋</a>',
+                        ': <a href="' .
+                        $this->request->getPost('imported_feed_url') .
+                        '" rel="noreferrer noopener" target="_blank">' .
+                        $this->request->getPost('imported_feed_url') .
+                        ' ⎋</a>',
                 ]);
         }
 
@@ -235,7 +234,7 @@ class PodcastImportController extends BaseController
             foreach ($platformType['elements'] as $platform) {
                 $platformLabel = $platform->attributes()['platform'];
                 $platformSlug = slugify($platformLabel);
-                if ($platformModel->getPlatform($platformSlug)) {
+                if ($platformModel->getPlatform($platformSlug) !== null) {
                     $podcastsPlatformsData[] = [
                         'platform_slug' => $platformSlug,
                         'podcast_id' => $newPodcastId,
@@ -255,45 +254,41 @@ class PodcastImportController extends BaseController
         }
 
         foreach ($nsPodcast->person as $podcastPerson) {
+            $fullName = (string) $podcastPerson;
             $personModel = new PersonModel();
             $newPersonId = null;
-            if ($newPerson = $personModel->getPerson($podcastPerson)) {
+            if (($newPerson = $personModel->getPerson($fullName)) !== null) {
                 $newPersonId = $newPerson->id;
-            } elseif (
-                !($newPersonId = $personModel->createPerson(
-                    $podcastPerson,
-                    $podcastPerson->attributes()['href'],
-                    $podcastPerson->attributes()['img'],
-                ))
-            ) {
-                return redirect()
-                    ->back()
-                    ->withInput()
-                    ->with('errors', $personModel->errors());
+            } else {
+                $newPodcastPerson = new Person([
+                    'full_name' => $fullName,
+                    'unique_name' => slugify($fullName),
+                    'information_url' => $podcastPerson->attributes()['href'],
+                    'image' => new Image(download_file($podcastPerson->attributes()['img'])),
+                    'created_by' => user_id(),
+                    'updated_by' => user_id(),
+                ]);
+
+                if (!$newPersonId = $personModel->insert($newPodcastPerson)) {
+                    return redirect()
+                        ->back()
+                        ->withInput()
+                        ->with('errors', $personModel->errors());
+                }
             }
 
             $personGroup =
-                $podcastPerson->attributes()['group'] === null
-                    ? ['slug' => '']
-                    : ReversedTaxonomy::$taxonomy[
-                        (string) $podcastPerson->attributes()['group']
-                    ];
+                isset($podcastPerson->attributes()['group'])
+                ? ['slug' => '']
+                : ReversedTaxonomy::$taxonomy[(string) $podcastPerson->attributes()['group']];
             $personRole =
-                $podcastPerson->attributes()['role'] === null ||
+                isset($podcastPerson->attributes()['role']) ||
                 $personGroup === null
-                    ? ['slug' => '']
-                    : $personGroup['roles'][
-                        strval($podcastPerson->attributes()['role'])
-                    ];
-            $newPodcastPerson = new PodcastPerson([
-                'podcast_id' => $newPodcastId,
-                'person_id' => $newPersonId,
-                'person_group' => $personGroup['slug'],
-                'person_role' => $personRole['slug'],
-            ]);
-            $podcastPersonModel = new PodcastPersonModel();
+                ? ['slug' => '']
+                : $personGroup['roles'][strval($podcastPerson->attributes()['role'])];
 
-            if (!$podcastPersonModel->insert($newPodcastPerson)) {
+            $podcastPersonModel = new PersonModel();
+            if (!$podcastPersonModel->addPodcastPerson($newPodcastId, $newPersonId, $personGroup['slug'], $personRole['slug'])) {
                 return redirect()
                     ->back()
                     ->withInput()
@@ -305,8 +300,8 @@ class PodcastImportController extends BaseController
         $lastItem =
             $this->request->getPost('max_episodes') !== null &&
             $this->request->getPost('max_episodes') < $numberItems
-                ? $this->request->getPost('max_episodes')
-                : $numberItems;
+            ? $this->request->getPost('max_episodes')
+            : $numberItems;
 
         $slugs = [];
 
@@ -338,22 +333,12 @@ class PodcastImportController extends BaseController
                 $slug = $slug . '-' . $slugNumber;
             }
             $slugs[] = $slug;
-
-            $itemDescriptionHtml = null;
-            switch ($this->request->getPost('description_field')) {
-                case 'content':
-                    $itemDescriptionHtml = $nsContent->encoded;
-                    break;
-                case 'summary':
-                    $itemDescriptionHtml = $nsItunes->summary;
-                    break;
-                case 'subtitle_summary':
-                    $itemDescriptionHtml =
-                        $nsItunes->subtitle . '<br/>' . $nsItunes->summary;
-                    break;
-                default:
-                    $itemDescriptionHtml = $item->description;
-            }
+            $itemDescriptionHtml = match ($this->request->getPost('description_field')) {
+                'content' => $nsContent->encoded,
+                'summary' => $nsItunes->summary,
+                'subtitle_summary' => $nsItunes->subtitle . '<br/>' . $nsItunes->summary,
+                default => $item->description,
+            };
 
             if (
                 $nsItunes->image !== null &&
@@ -382,42 +367,42 @@ class PodcastImportController extends BaseController
                 'description_html' => $itemDescriptionHtml,
                 'image' => $episodeImage,
                 'parental_advisory' =>
-                    $nsItunes->explicit === null
-                        ? null
-                        : (in_array($nsItunes->explicit, ['yes', 'true'])
-                            ? 'explicit'
-                            : (in_array($nsItunes->explicit, ['no', 'false'])
-                                ? 'clean'
-                                : null)),
+                $nsItunes->explicit === null
+                    ? null
+                    : (in_array($nsItunes->explicit, ['yes', 'true'])
+                        ? 'explicit'
+                        : (in_array($nsItunes->explicit, ['no', 'false'])
+                            ? 'clean'
+                            : null)),
                 'number' =>
-                    $this->request->getPost('force_renumber') === 'yes'
-                        ? $itemNumber
-                        : $nsItunes->episode,
+                $this->request->getPost('force_renumber') === 'yes'
+                    ? $itemNumber
+                    : $nsItunes->episode,
                 'season_number' =>
-                    $this->request->getPost('season_number') === null
-                        ? $nsItunes->season
-                        : $this->request->getPost('season_number'),
+                $this->request->getPost('season_number') === null
+                    ? $nsItunes->season
+                    : $this->request->getPost('season_number'),
                 'type' =>
-                    $nsItunes->episodeType === null
-                        ? 'full'
-                        : $nsItunes->episodeType,
+                $nsItunes->episodeType === null
+                    ? 'full'
+                    : $nsItunes->episodeType,
                 'is_blocked' =>
-                    $nsItunes->block === null
-                        ? false
-                        : $nsItunes->block === 'yes',
+                $nsItunes->block === null
+                    ? false
+                    : $nsItunes->block === 'yes',
                 'location_name' => $nsPodcast->location
                     ? $nsPodcast->location
                     : null,
                 'location_geo' =>
-                    !$nsPodcast->location ||
+                !$nsPodcast->location ||
                     $nsPodcast->location->attributes()['geo'] === null
-                        ? null
-                        : $nsPodcast->location->attributes()['geo'],
+                    ? null
+                    : $nsPodcast->location->attributes()['geo'],
                 'location_osm_id' =>
-                    !$nsPodcast->location ||
+                !$nsPodcast->location ||
                     $nsPodcast->location->attributes()['osm'] === null
-                        ? null
-                        : $nsPodcast->location->attributes()['osm'],
+                    ? null
+                    : $nsPodcast->location->attributes()['osm'],
                 'created_by' => user_id(),
                 'updated_by' => user_id(),
                 'published_at' => strtotime($item->pubDate),
@@ -434,46 +419,40 @@ class PodcastImportController extends BaseController
             }
 
             foreach ($nsPodcast->person as $episodePerson) {
+                $fullName = (string) $episodePerson;
                 $personModel = new PersonModel();
                 $newPersonId = null;
-                if ($newPerson = $personModel->getPerson($episodePerson)) {
+                if (($newPerson = $personModel->getPerson($fullName)) !== null) {
                     $newPersonId = $newPerson->id;
-                } elseif (
-                    !($newPersonId = $personModel->createPerson(
-                        $episodePerson,
-                        $episodePerson->attributes()['href'],
-                        $episodePerson->attributes()['img'],
-                    ))
-                ) {
-                    return redirect()
-                        ->back()
-                        ->withInput()
-                        ->with('errors', $personModel->errors());
+                } else {
+                    $newEpisodePerson = new Person([
+                        'full_name' => $fullName,
+                        'slug' => slugify($fullName),
+                        'information_url' => $episodePerson->attributes()['href'],
+                        'image' => new Image(download_file($episodePerson->attributes()['img']))
+                    ]);
+
+                    if (!($newPersonId = $personModel->insert($newEpisodePerson))) {
+                        return redirect()
+                            ->back()
+                            ->withInput()
+                            ->with('errors', $personModel->errors());
+                    }
                 }
 
                 $personGroup =
                     $episodePerson->attributes()['group'] === null
-                        ? ['slug' => '']
-                        : ReversedTaxonomy::$taxonomy[
-                            strval($episodePerson->attributes()['group'])
-                        ];
+                    ? ['slug' => '']
+                    : ReversedTaxonomy::$taxonomy[strval($episodePerson->attributes()['group'])];
                 $personRole =
                     $episodePerson->attributes()['role'] === null ||
                     $personGroup === null
-                        ? ['slug' => '']
-                        : $personGroup['roles'][
-                            strval($episodePerson->attributes()['role'])
-                        ];
-                $newEpisodePerson = new PodcastPerson([
-                    'podcast_id' => $newPodcastId,
-                    'episode_id' => $newEpisodeId,
-                    'person_id' => $newPersonId,
-                    'person_group' => $personGroup['slug'],
-                    'person_role' => $personRole['slug'],
-                ]);
+                    ? ['slug' => '']
+                    : $personGroup['roles'][strval($episodePerson->attributes()['role'])];
+
 
-                $episodePersonModel = new EpisodePersonModel();
-                if (!$episodePersonModel->insert($newEpisodePerson)) {
+                $episodePersonModel = new PersonModel();
+                if (!$episodePersonModel->addEpisodePerson($newPodcastId, $newEpisodeId, $newPersonId, $personGroup['slug'], $personRole['slug'])) {
                     return redirect()
                         ->back()
                         ->withInput()
diff --git a/app/Controllers/Admin/PodcastPersonController.php b/app/Controllers/Admin/PodcastPersonController.php
index 894555c2e8..623088a95a 100644
--- a/app/Controllers/Admin/PodcastPersonController.php
+++ b/app/Controllers/Admin/PodcastPersonController.php
@@ -8,9 +8,9 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Podcast;
 use CodeIgniter\Exceptions\PageNotFoundException;
-use App\Models\PodcastPersonModel;
 use App\Models\PodcastModel;
 use App\Models\PersonModel;
 
@@ -21,13 +21,17 @@ class PodcastPersonController extends BaseController
      */
     protected $podcast;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             throw PageNotFoundException::forPageNotFound();
         }
 
-        if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
+        if (
+            ($this->podcast = (new PodcastModel())->getPodcastById(
+                (int) $params[0],
+            )) !== null
+        ) {
             unset($params[0]);
             return $this->$method(...$params);
         }
@@ -35,13 +39,13 @@ class PodcastPersonController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         helper('form');
 
         $data = [
             'podcast' => $this->podcast,
-            'podcastPersons' => (new PodcastPersonModel())->getPodcastPersons(
+            'podcastPersons' => (new PersonModel())->getPodcastPersons(
                 $this->podcast->id,
             ),
             'personOptions' => (new PersonModel())->getPersonOptions(),
@@ -53,7 +57,7 @@ class PodcastPersonController extends BaseController
         return view('admin/podcast/person', $data);
     }
 
-    public function attemptAdd()
+    public function attemptAdd(): RedirectResponse
     {
         $rules = [
             'person' => 'required',
@@ -66,7 +70,7 @@ class PodcastPersonController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
-        (new PodcastPersonModel())->addPodcastPersons(
+        (new PersonModel())->addPodcastPersons(
             $this->podcast->id,
             $this->request->getPost('person'),
             $this->request->getPost('person_group_role'),
@@ -75,9 +79,9 @@ class PodcastPersonController extends BaseController
         return redirect()->back();
     }
 
-    public function remove($podcastPersonId)
+    public function remove(int $podcastPersonId): RedirectResponse
     {
-        (new PodcastPersonModel())->removePodcastPersons(
+        (new PersonModel())->removePodcastPersons(
             $this->podcast->id,
             $podcastPersonId,
         );
diff --git a/app/Controllers/Admin/PodcastPlatformController.php b/app/Controllers/Admin/PodcastPlatformController.php
index 27b1deabb9..c68d2a179d 100644
--- a/app/Controllers/Admin/PodcastPlatformController.php
+++ b/app/Controllers/Admin/PodcastPlatformController.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers\Admin;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use App\Entities\Podcast;
 use CodeIgniter\Exceptions\PageNotFoundException;
 use App\Models\PlatformModel;
@@ -21,13 +22,17 @@ class PodcastPlatformController extends BaseController
      */
     protected $podcast;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
         }
 
-        if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
+        if (
+            ($this->podcast = (new PodcastModel())->getPodcastById(
+                (int) $params[0],
+            )) !== null
+        ) {
             unset($params[0]);
             return $this->$method(...$params);
         }
@@ -35,12 +40,12 @@ class PodcastPlatformController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         return view('admin/podcast/platforms/dashboard');
     }
 
-    public function platforms($platformType)
+    public function platforms(string $platformType): string
     {
         helper('form');
 
@@ -57,8 +62,9 @@ class PodcastPlatformController extends BaseController
         return view('admin/podcast/platforms', $data);
     }
 
-    public function attemptPlatformsUpdate($platformType)
-    {
+    public function attemptPlatformsUpdate(
+        string $platformType
+    ): RedirectResponse {
         $platformModel = new PlatformModel();
         $validation = Services::validation();
 
@@ -105,8 +111,9 @@ class PodcastPlatformController extends BaseController
             ->with('message', lang('Platforms.messages.updateSuccess'));
     }
 
-    public function removePodcastPlatform($platformSlug)
-    {
+    public function removePodcastPlatform(
+        string $platformSlug
+    ): RedirectResponse {
         (new PlatformModel())->removePodcastPlatform(
             $this->podcast->id,
             $platformSlug,
diff --git a/app/Controllers/Admin/UserController.php b/app/Controllers/Admin/UserController.php
index 3446e28f4f..04b105700c 100644
--- a/app/Controllers/Admin/UserController.php
+++ b/app/Controllers/Admin/UserController.php
@@ -12,6 +12,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
 use App\Authorization\GroupModel;
 use App\Entities\User;
 use App\Models\UserModel;
+use CodeIgniter\HTTP\RedirectResponse;
 use Config\Services;
 
 class UserController extends BaseController
@@ -21,7 +22,7 @@ class UserController extends BaseController
      */
     protected $user;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
@@ -34,14 +35,14 @@ class UserController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function list()
+    public function list(): string
     {
         $data = ['users' => (new UserModel())->findAll()];
 
         return view('admin/user/list', $data);
     }
 
-    public function view()
+    public function view(): string
     {
         $data = ['user' => $this->user];
 
@@ -49,7 +50,7 @@ class UserController extends BaseController
         return view('admin/user/view', $data);
     }
 
-    public function create()
+    public function create(): string
     {
         helper('form');
 
@@ -60,7 +61,7 @@ class UserController extends BaseController
         return view('admin/user/create', $data);
     }
 
-    public function attemptCreate()
+    public function attemptCreate(): RedirectResponse
     {
         $userModel = new UserModel();
 
@@ -108,7 +109,7 @@ class UserController extends BaseController
             );
     }
 
-    public function edit()
+    public function edit(): string
     {
         helper('form');
 
@@ -131,7 +132,7 @@ class UserController extends BaseController
         return view('admin/user/edit', $data);
     }
 
-    public function attemptEdit()
+    public function attemptEdit(): RedirectResponse
     {
         $authorize = Services::authorization();
 
@@ -149,7 +150,7 @@ class UserController extends BaseController
             );
     }
 
-    public function forcePassReset()
+    public function forcePassReset(): RedirectResponse
     {
         $userModel = new UserModel();
         $this->user->forcePasswordReset();
@@ -171,7 +172,7 @@ class UserController extends BaseController
             );
     }
 
-    public function ban()
+    public function ban(): RedirectResponse
     {
         $authorize = Services::authorization();
         if ($authorize->inGroup('superadmin', $this->user->id)) {
@@ -204,7 +205,7 @@ class UserController extends BaseController
             );
     }
 
-    public function unBan()
+    public function unBan(): RedirectResponse
     {
         $userModel = new UserModel();
         $this->user->unBan();
@@ -225,7 +226,7 @@ class UserController extends BaseController
             );
     }
 
-    public function delete()
+    public function delete(): RedirectResponse
     {
         $authorize = Services::authorization();
         if ($authorize->inGroup('superadmin', $this->user->id)) {
diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php
index 20129e8760..ef5905cddc 100644
--- a/app/Controllers/AuthController.php
+++ b/app/Controllers/AuthController.php
@@ -18,14 +18,14 @@ class AuthController extends MythAuthController
      * An array of helpers to be automatically loaded
      * upon class instantiation.
      *
-     * @var array
+     * @var string[]
      */
     protected $helpers = ['components'];
 
     /**
      * Attempt to register a new user.
      */
-    public function attemptRegister()
+    public function attemptRegister(): RedirectResponse
     {
         // Check if registration is allowed
         if (!$this->config->allowRegistration) {
@@ -61,9 +61,9 @@ class AuthController extends MythAuthController
         );
         $user = new User($this->request->getPost($allowedPostFields));
 
-        $this->config->requireActivation !== false
-            ? $user->generateActivateHash()
-            : $user->activate();
+        $this->config->requireActivation === null
+            ? $user->activate()
+            : $user->generateActivateHash();
 
         // Ensure default group gets assigned if set
         if ($this->config->defaultUserGroup !== null) {
@@ -77,7 +77,7 @@ class AuthController extends MythAuthController
                 ->with('errors', $users->errors());
         }
 
-        if ($this->config->requireActivation !== false) {
+        if ($this->config->requireActivation !== null) {
             $activator = service('activator');
             $sent = $activator->send($user);
 
@@ -109,7 +109,7 @@ class AuthController extends MythAuthController
      */
     public function attemptReset(): RedirectResponse
     {
-        if ($this->config->activeResetter === false) {
+        if ($this->config->activeResetter === null) {
             return redirect()
                 ->route('login')
                 ->with('error', lang('Auth.forgotDisabled'));
@@ -173,7 +173,7 @@ class AuthController extends MythAuthController
             ->with('message', lang('Auth.resetSuccess'));
     }
 
-    public function attemptInteractAsActor()
+    public function attemptInteractAsActor(): RedirectResponse
     {
         $rules = [
             'actor_id' => 'required|numeric',
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 7bfe25bca2..5c8909e9ee 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -24,7 +24,7 @@ class BaseController extends Controller
      * class instantiation. These helpers will be available
      * to all other controllers that extend BaseController.
      *
-     * @var array
+     * @var string[]
      */
     protected $helpers = ['auth', 'svg', 'components', 'misc'];
 
diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php
index 55404eae93..ec531e16c0 100644
--- a/app/Controllers/EpisodeController.php
+++ b/app/Controllers/EpisodeController.php
@@ -8,6 +8,8 @@
 
 namespace App\Controllers;
 
+use CodeIgniter\HTTP\ResponseInterface;
+use Config\Services;
 use Analytics\AnalyticsTrait;
 use App\Entities\Episode;
 use App\Entities\Podcast;
@@ -30,25 +32,25 @@ class EpisodeController extends BaseController
      */
     protected $episode;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) < 2) {
             throw PageNotFoundException::forPageNotFound();
         }
 
         if (
-            !($this->podcast = (new PodcastModel())->getPodcastByName(
+            ($this->podcast = (new PodcastModel())->getPodcastByName(
                 $params[0],
-            ))
+            )) === null
         ) {
             throw PageNotFoundException::forPageNotFound();
         }
 
         if (
-            $this->episode = (new EpisodeModel())->getEpisodeBySlug(
+            ($this->episode = (new EpisodeModel())->getEpisodeBySlug(
                 $this->podcast->id,
                 $params[1],
-            )
+            )) !== null
         ) {
             unset($params[1]);
             unset($params[0]);
@@ -58,7 +60,7 @@ class EpisodeController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         // Prevent analytics hit when authenticated
         if (!can_user_interact()) {
@@ -71,17 +73,9 @@ class EpisodeController extends BaseController
             (can_user_interact() ? '_authenticated' : '');
 
         if (!($cachedView = cache($cacheName))) {
-            helper('persons');
-            $episodePersons = [];
-            construct_person_array($this->episode->persons, $episodePersons);
-            $podcastPersons = [];
-            construct_person_array($this->podcast->persons, $podcastPersons);
-
             $data = [
                 'podcast' => $this->podcast,
                 'episode' => $this->episode,
-                'episodePersons' => $episodePersons,
-                'persons' => $podcastPersons,
             ];
 
             $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
@@ -91,22 +85,22 @@ class EpisodeController extends BaseController
             if (can_user_interact()) {
                 helper('form');
                 return view('podcast/episode_authenticated', $data);
-            } else {
-                // The page cache is set to a decade so it is deleted manually upon podcast update
-                return view('podcast/episode', $data, [
-                    'cache' => $secondsToNextUnpublishedEpisode
-                        ? $secondsToNextUnpublishedEpisode
-                        : DECADE,
-                    'cache_name' => $cacheName,
-                ]);
             }
+            // The page cache is set to a decade so it is deleted manually upon podcast update
+            return view('podcast/episode', $data, [
+                'cache' => $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+                'cache_name' => $cacheName,
+            ]);
         }
 
         return $cachedView;
     }
 
-    public function embeddablePlayer($theme = 'light-transparent')
-    {
+    public function embeddablePlayer(
+        string $theme = 'light-transparent'
+    ): string {
         header('Content-Security-Policy: frame-ancestors https://* http://*');
 
         // Prevent analytics hit when authenticated
@@ -114,7 +108,7 @@ class EpisodeController extends BaseController
             $this->registerPodcastWebpageHit($this->episode->podcast_id);
         }
 
-        $session = \Config\Services::session();
+        $session = Services::session();
         $session->start();
         if (isset($_SERVER['HTTP_REFERER'])) {
             $session->set(
@@ -152,7 +146,7 @@ class EpisodeController extends BaseController
         return $cachedView;
     }
 
-    public function oembedJSON()
+    public function oembedJSON(): ResponseInterface
     {
         return $this->response->setJSON([
             'type' => 'rich',
@@ -174,7 +168,7 @@ class EpisodeController extends BaseController
         ]);
     }
 
-    public function oembedXML()
+    public function oembedXML(): ResponseInterface
     {
         $oembed = new SimpleXMLElement(
             "<?xml version='1.0' encoding='utf-8' standalone='yes'?><oembed></oembed>",
diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php
index f59a5b7fb9..dd8b6d36a5 100644
--- a/app/Controllers/HomeController.php
+++ b/app/Controllers/HomeController.php
@@ -9,20 +9,18 @@
 namespace App\Controllers;
 
 use App\Models\PodcastModel;
+use CodeIgniter\HTTP\RedirectResponse;
 
 class HomeController extends BaseController
 {
-    /**
-     * @return RedirectResponse|string
-     */
-    public function index()
+    public function index(): RedirectResponse|string
     {
         $model = new PodcastModel();
 
         $allPodcasts = $model->findAll();
 
         // check if there's only one podcast to redirect user to it
-        if (count($allPodcasts) == 1) {
+        if (count($allPodcasts) === 1) {
             return redirect()->route('podcast-activity', [
                 $allPodcasts[0]->name,
             ]);
diff --git a/app/Controllers/InstallController.php b/app/Controllers/InstallController.php
index 6fb02e0109..4aa8f2b7cf 100644
--- a/app/Controllers/InstallController.php
+++ b/app/Controllers/InstallController.php
@@ -19,6 +19,7 @@ use Config\Database;
 use App\Entities\User;
 use App\Models\UserModel;
 use CodeIgniter\Controller;
+use CodeIgniter\HTTP\RedirectResponse;
 use Config\Services;
 use Dotenv\Dotenv;
 
@@ -51,7 +52,14 @@ class InstallController extends Controller
     public function index(): string
     {
         if (!file_exists(ROOTPATH . '.env')) {
-            $this->createEnv();
+            // create empty .env file
+            try {
+                $envFile = fopen(ROOTPATH . '.env', 'w');
+                fclose($envFile);
+            } catch (Throwable) {
+                // Could not create the .env file, redirect to a view with instructions on how to add it manually
+                return view('install/manual_config');
+            }
         }
 
         // Check if .env has all required fields
@@ -85,7 +93,7 @@ class InstallController extends Controller
 
             try {
                 $dotenv->required('cache.handler');
-            } catch (ValidationException $validationException) {
+            } catch (ValidationException) {
                 return $this->cacheConfig();
             }
         } else {
@@ -101,7 +109,7 @@ class InstallController extends Controller
                     'database.default.DBPrefix',
                     'cache.handler',
                 ]);
-            } catch (ValidationException $e) {
+            } catch (ValidationException) {
                 return view('install/manual_config');
             }
         }
@@ -117,7 +125,7 @@ class InstallController extends Controller
                 // if so, show a 404 page
                 throw PageNotFoundException::forPageNotFound();
             }
-        } catch (DatabaseException $databaseException) {
+        } catch (DatabaseException) {
             // Could not connect to the database
             // show database config view to fix value
             session()->setFlashdata(
@@ -137,28 +145,12 @@ class InstallController extends Controller
         return $this->createSuperAdmin();
     }
 
-    /**
-     * Returns the form to generate the .env config file for the instance.
-     * @return mixed|void
-     */
-    public function createEnv()
-    {
-        // create empty .env file
-        try {
-            $envFile = fopen(ROOTPATH . '.env', 'w');
-            fclose($envFile);
-        } catch (Throwable $throwable) {
-            // Could not create the .env file, redirect to a view with manual instructions on how to add it
-            return view('install/manual_config');
-        }
-    }
-
-    public function instanceConfig()
+    public function instanceConfig(): string
     {
         return view('install/instance_config');
     }
 
-    public function attemptInstanceConfig()
+    public function attemptInstanceConfig(): RedirectResponse
     {
         $rules = [
             'hostname' => 'required|validate_url',
@@ -198,12 +190,12 @@ class InstallController extends Controller
         );
     }
 
-    public function databaseConfig()
+    public function databaseConfig(): string
     {
         return view('install/database_config');
     }
 
-    public function attemptDatabaseConfig()
+    public function attemptDatabaseConfig(): RedirectResponse
     {
         $rules = [
             'db_hostname' => 'required',
@@ -236,12 +228,12 @@ class InstallController extends Controller
         return redirect()->back();
     }
 
-    public function cacheConfig()
+    public function cacheConfig(): string
     {
         return view('install/cache_config');
     }
 
-    public function attemptCacheConfig()
+    public function attemptCacheConfig(): RedirectResponse
     {
         $rules = [
             'cache_handler' => 'required',
@@ -288,7 +280,7 @@ class InstallController extends Controller
     /**
      * Returns the form to create a the first superadmin user for the instance.
      */
-    public function createSuperAdmin()
+    public function createSuperAdmin(): string
     {
         return view('install/create_superadmin');
     }
@@ -298,7 +290,7 @@ class InstallController extends Controller
      *
      * After creation, user is redirected to login page to input its credentials.
      */
-    public function attemptCreateSuperAdmin()
+    public function attemptCreateSuperAdmin(): RedirectResponse
     {
         $userModel = new UserModel();
 
@@ -356,7 +348,7 @@ class InstallController extends Controller
      * writes config values in .env file
      * overwrites any existing key and appends new ones
      *
-     * @param array $configData key/value config pairs
+     * @param array<string, string> $configData key/value config pairs
      */
     public static function writeEnv(array $configData): void
     {
@@ -370,7 +362,7 @@ class InstallController extends Controller
                 $keyVal,
                 &$replaced
             ) {
-                if (strpos($line, (string) $key) === 0) {
+                if (str_starts_with($line, (string) $key)) {
                     $replaced = true;
                     return $keyVal;
                 }
diff --git a/app/Controllers/NoteController.php b/app/Controllers/NoteController.php
index 41e94ae14f..bcf8f09453 100644
--- a/app/Controllers/NoteController.php
+++ b/app/Controllers/NoteController.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers;
 
+use CodeIgniter\Exceptions\PageNotFoundException;
 use ActivityPub\Controllers\NoteController as ActivityPubNoteController;
 use ActivityPub\Entities\Note as ActivityPubNote;
 use Analytics\AnalyticsTrait;
@@ -34,24 +35,28 @@ class NoteController extends ActivityPubNoteController
      */
     protected $actor;
 
+    /**
+     * @var string[]
+     */
     protected $helpers = ['auth', 'activitypub', 'svg', 'components', 'misc'];
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (
-            !($this->podcast = (new PodcastModel())->getPodcastByName(
+            ($this->podcast = (new PodcastModel())->getPodcastByName(
                 $params[0],
-            ))
+            )) === null
         ) {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            throw PageNotFoundException::forPageNotFound();
         }
 
         $this->actor = $this->podcast->actor;
 
-        if (count($params) > 1) {
-            if (!($this->note = model('NoteModel')->getNoteById($params[1]))) {
-                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
-            }
+        if (
+            count($params) > 1 &&
+            !($this->note = model('NoteModel')->getNoteById($params[1]))
+        ) {
+            throw PageNotFoundException::forPageNotFound();
         }
         unset($params[0]);
         unset($params[1]);
@@ -77,27 +82,21 @@ class NoteController extends ActivityPubNoteController
         );
 
         if (!($cachedView = cache($cacheName))) {
-            helper('persons');
-            $persons = [];
-            construct_person_array($this->podcast->persons, $persons);
-
             $data = [
                 'podcast' => $this->podcast,
                 'actor' => $this->actor,
                 'note' => $this->note,
-                'persons' => $persons,
             ];
 
             // if user is logged in then send to the authenticated activity view
             if (can_user_interact()) {
                 helper('form');
                 return view('podcast/note_authenticated', $data);
-            } else {
-                return view('podcast/note', $data, [
-                    'cache' => DECADE,
-                    'cache_name' => $cacheName,
-                ]);
             }
+            return view('podcast/note', $data, [
+                'cache' => DECADE,
+                'cache_name' => $cacheName,
+            ]);
         }
 
         return $cachedView;
@@ -129,16 +128,13 @@ class NoteController extends ActivityPubNoteController
         $episodeUri = $this->request->getPost('episode_url');
         if (
             $episodeUri &&
-            ($params = extract_params_from_episode_uri(new URI($episodeUri)))
+            ($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
+            ($episode = (new EpisodeModel())->getEpisodeBySlug(
+                $params['podcastName'],
+                $params['episodeSlug'],
+            ))
         ) {
-            if (
-                $episode = (new EpisodeModel())->getEpisodeBySlug(
-                    $params['podcastName'],
-                    $params['episodeSlug'],
-                )
-            ) {
-                $newNote->episode_id = $episode->id;
-            }
+            $newNote->episode_id = $episode->id;
         }
 
         $newNote->message = $message;
@@ -146,7 +142,7 @@ class NoteController extends ActivityPubNoteController
         if (
             !model('NoteModel')->addNote(
                 $newNote,
-                $newNote->episode_id ? false : true,
+                !(bool) $newNote->episode_id,
                 true,
             )
         ) {
diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php
index c2bbd741c7..97d55c6652 100644
--- a/app/Controllers/PageController.php
+++ b/app/Controllers/PageController.php
@@ -21,7 +21,7 @@ class PageController extends BaseController
      */
     protected $page;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
             return $this->$method();
@@ -36,7 +36,7 @@ class PageController extends BaseController
         throw PageNotFoundException::forPageNotFound();
     }
 
-    public function index()
+    public function index(): string
     {
         $cacheName = "page-{$this->page->slug}";
         if (!($found = cache($cacheName))) {
@@ -53,7 +53,7 @@ class PageController extends BaseController
         return $found;
     }
 
-    public function credits()
+    public function credits(): string
     {
         $locale = service('request')->getLocale();
         $allPodcasts = (new PodcastModel())->findAll();
diff --git a/app/Controllers/PodcastController.php b/app/Controllers/PodcastController.php
index 4057218a14..f4ae6663eb 100644
--- a/app/Controllers/PodcastController.php
+++ b/app/Controllers/PodcastController.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers;
 
+use CodeIgniter\Exceptions\PageNotFoundException;
 use Analytics\AnalyticsTrait;
 use App\Entities\Podcast;
 use App\Models\EpisodeModel;
@@ -23,23 +24,25 @@ class PodcastController extends BaseController
      */
     protected $podcast;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (count($params) === 0) {
-            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            throw PageNotFoundException::forPageNotFound();
         }
 
         if (
-            $this->podcast = (new PodcastModel())->getPodcastByName($params[0])
+            ($this->podcast = (new PodcastModel())->getPodcastByName(
+                $params[0],
+            )) !== null
         ) {
             unset($params[0]);
             return $this->$method(...$params);
         }
 
-        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+        throw PageNotFoundException::forPageNotFound();
     }
 
-    public function activity()
+    public function activity(): string
     {
         // Prevent analytics hit when authenticated
         if (!can_user_interact()) {
@@ -58,34 +61,28 @@ class PodcastController extends BaseController
         );
 
         if (!($cachedView = cache($cacheName))) {
-            helper('persons');
-            $persons = [];
-            construct_person_array($this->podcast->persons, $persons);
-
             $data = [
                 'podcast' => $this->podcast,
                 'notes' => (new NoteModel())->getActorPublishedNotes(
                     $this->podcast->actor_id,
                 ),
-                'persons' => $persons,
             ];
 
             // if user is logged in then send to the authenticated activity view
             if (can_user_interact()) {
                 helper('form');
                 return view('podcast/activity_authenticated', $data);
-            } else {
-                return view('podcast/activity', $data, [
-                    'cache' => DECADE,
-                    'cache_name' => $cacheName,
-                ]);
             }
+            return view('podcast/activity', $data, [
+                'cache' => DECADE,
+                'cache_name' => $cacheName,
+            ]);
         }
 
         return $cachedView;
     }
 
-    public function episodes()
+    public function episodes(): string
     {
         // Prevent analytics hit when authenticated
         if (!can_user_interact()) {
@@ -95,7 +92,7 @@ class PodcastController extends BaseController
         $yearQuery = $this->request->getGet('year');
         $seasonQuery = $this->request->getGet('season');
 
-        if (!$yearQuery and !$seasonQuery) {
+        if (!$yearQuery && !$seasonQuery) {
             $defaultQuery = (new PodcastModel())->getDefaultQuery(
                 $this->podcast->id,
             );
@@ -130,7 +127,7 @@ class PodcastController extends BaseController
             $episodesNavigation = [];
             $activeQuery = null;
             foreach ($years as $year) {
-                $isActive = $yearQuery == $year['year'];
+                $isActive = $yearQuery === $year['year'];
                 if ($isActive) {
                     $activeQuery = [
                         'type' => 'year',
@@ -140,7 +137,7 @@ class PodcastController extends BaseController
                     ];
                 }
 
-                array_push($episodesNavigation, [
+                $episodesNavigation[] = [
                     'label' => $year['year'],
                     'number_of_episodes' => $year['number_of_episodes'],
                     'route' =>
@@ -148,11 +145,11 @@ class PodcastController extends BaseController
                         '?year=' .
                         $year['year'],
                     'is_active' => $isActive,
-                ]);
+                ];
             }
 
             foreach ($seasons as $season) {
-                $isActive = $seasonQuery == $season['season_number'];
+                $isActive = $seasonQuery === $season['season_number'];
                 if ($isActive) {
                     $activeQuery = [
                         'type' => 'season',
@@ -164,7 +161,7 @@ class PodcastController extends BaseController
                     ];
                 }
 
-                array_push($episodesNavigation, [
+                $episodesNavigation[] = [
                     'label' => lang('Podcast.season', [
                         'seasonNumber' => $season['season_number'],
                     ]),
@@ -174,13 +171,9 @@ class PodcastController extends BaseController
                         '?season=' .
                         $season['season_number'],
                     'is_active' => $isActive,
-                ]);
+                ];
             }
 
-            helper('persons');
-            $persons = [];
-            construct_person_array($this->podcast->persons, $persons);
-
             $data = [
                 'podcast' => $this->podcast,
                 'episodesNav' => $episodesNavigation,
@@ -191,7 +184,6 @@ class PodcastController extends BaseController
                     $yearQuery,
                     $seasonQuery,
                 ),
-                'persons' => $persons,
             ];
 
             $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
@@ -201,14 +193,13 @@ class PodcastController extends BaseController
             // if user is logged in then send to the authenticated episodes view
             if (can_user_interact()) {
                 return view('podcast/episodes_authenticated', $data);
-            } else {
-                return view('podcast/episodes', $data, [
-                    'cache' => $secondsToNextUnpublishedEpisode
-                        ? $secondsToNextUnpublishedEpisode
-                        : DECADE,
-                    'cache_name' => $cacheName,
-                ]);
             }
+            return view('podcast/episodes', $data, [
+                'cache' => $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+                'cache_name' => $cacheName,
+            ]);
         }
 
         return $cachedView;
diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
index 376877e568..b80ebd16e8 100644
--- a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
+++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
@@ -123,7 +123,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
                                 : $city->subdivisions[0]->isoCode;
                             $latitude = round($city->location->latitude, 3);
                             $longitude = round($city->location->longitude, 3);
-                        } catch (AddressNotFoundException $addressNotFoundException) {
+                        } catch (AddressNotFoundException) {
                             //Bad luck, bad IP, nothing to do.
                         }
 
diff --git a/app/Entities/Actor.php b/app/Entities/Actor.php
index e19892ddcc..34054745fe 100644
--- a/app/Entities/Actor.php
+++ b/app/Entities/Actor.php
@@ -18,15 +18,8 @@ use RuntimeException;
  */
 class Actor extends ActivityPubActor
 {
-    /**
-     * @var Podcast|null
-     */
-    protected $podcast;
-
-    /**
-     * @var boolean
-     */
-    protected $is_podcast;
+    protected ?Podcast $podcast;
+    protected bool $is_podcast;
 
     public function getIsPodcast(): bool
     {
diff --git a/app/Entities/Category.php b/app/Entities/Category.php
index 208a6962f9..a650d65156 100644
--- a/app/Entities/Category.php
+++ b/app/Entities/Category.php
@@ -21,10 +21,7 @@ use CodeIgniter\Entity\Entity;
  */
 class Category extends Entity
 {
-    /**
-     * @var Category|null
-     */
-    protected $parent;
+    protected ?Category $parent;
 
     /**
      * @var array<string, string>
diff --git a/app/Entities/Credit.php b/app/Entities/Credit.php
index 44e0738244..5c6ea62896 100644
--- a/app/Entities/Credit.php
+++ b/app/Entities/Credit.php
@@ -16,7 +16,7 @@ use CodeIgniter\Entity\Entity;
 
 /**
  * @property int $podcast_id
- * @property Podcast $podcast
+ * @property Podcast|null $podcast
  * @property int|null $episode_id
  * @property Episode|null $episode
  * @property string $full_name
@@ -25,34 +25,15 @@ use CodeIgniter\Entity\Entity;
  * @property string $person_role
  * @property string $role_label
  * @property int $person_id
- * @property Person $person
+ * @property Person|null $person
  */
 class Credit extends Entity
 {
-    /**
-     * @var Person
-     */
-    protected $person;
-
-    /**
-     * @var Podcast
-     */
-    protected $podcast;
-
-    /**
-     * @var Episode|null
-     */
-    protected $episode;
-
-    /**
-     * @var string
-     */
-    protected $group_label;
-
-    /**
-     * @var string
-     */
-    protected $role_label;
+    protected ?Person $person;
+    protected ?Podcast $podcast;
+    protected ?Episode $episode;
+    protected string $group_label;
+    protected string $role_label;
 
     /**
      * @var array<string, string>
@@ -66,7 +47,7 @@ class Credit extends Entity
         'person_role' => 'string',
     ];
 
-    public function getPerson(): Person
+    public function getPerson(): ?Person
     {
         if ($this->person_id === null) {
             throw new RuntimeException(
@@ -83,7 +64,7 @@ class Credit extends Entity
         return $this->person;
     }
 
-    public function getPodcast(): Podcast
+    public function getPodcast(): ?Podcast
     {
         if ($this->podcast_id === null) {
             throw new RuntimeException(
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 5e60ff82c6..178baac572 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -12,8 +12,8 @@ use App\Entities\Location;
 use App\Libraries\SimpleRSSElement;
 use App\Models\PodcastModel;
 use App\Models\SoundbiteModel;
-use App\Models\EpisodePersonModel;
 use App\Models\NoteModel;
+use App\Models\PersonModel;
 use CodeIgniter\Entity\Entity;
 use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
@@ -39,7 +39,7 @@ use RuntimeException;
  * @property string $audio_file_mimetype
  * @property int $audio_file_size
  * @property int $audio_file_header_size
- * @property string $description Holds text only description, striped of any markdown or html special characters
+ * @property string|null $description Holds text only description, striped of any markdown or html special characters
  * @property string $description_markdown
  * @property string $description_html
  * @property Image $image
@@ -75,101 +75,43 @@ use RuntimeException;
  * @property Time $updated_at;
  * @property Time|null $deleted_at;
  *
- * @property EpisodePerson[] $persons;
+ * @property Person[] $persons;
  * @property Soundbite[] $soundbites;
  * @property string $embeddable_player_url;
  */
 class Episode extends Entity
 {
-    /**
-     * @var Podcast
-     */
-    protected $podcast;
-
-    /**
-     * @var string
-     */
-    protected $link;
-
-    /**
-     * @var File
-     */
-    protected $audio_file;
-
-    /**
-     * @var string
-     */
-    protected $audio_file_url;
-
-    /**
-     * @var string
-     */
-    protected $audio_file_analytics_url;
+    protected Podcast $podcast;
+    protected string $link;
+    protected File $audio_file;
+    protected string $audio_file_url;
+    protected string $audio_file_analytics_url;
+    protected string $audio_file_web_url;
+    protected string $audio_file_opengraph_url;
+    protected string $embeddable_player_url;
+    protected Image $image;
+    protected ?string $description;
+    protected File $transcript_file;
+    protected File $chapters_file;
 
     /**
-     * @var string
+     * @var Person[]
      */
-    protected $audio_file_web_url;
-
-    /**
-     * @var string
-     */
-    protected $audio_file_opengraph_url;
-
-    /**
-     * @var string
-     */
-    protected $embeddable_player_url;
-
-    /**
-     * @var Image
-     */
-    protected $image;
-
-    /**
-     * @var string
-     */
-    protected $description;
-
-    /**
-     * @var File
-     */
-    protected $transcript_file;
-
-    /**
-     * @var File
-     */
-    protected $chapters_file;
-
-    /**
-     * @var EpisodePerson[]
-     */
-    protected $persons;
+    protected $persons = [];
 
     /**
      * @var Soundbite[]
      */
-    protected $soundbites;
+    protected $soundbites = [];
 
     /**
      * @var Note[]
      */
-    protected $notes;
+    protected $notes = [];
 
-    /**
-     * @var Location|null
-     */
-    protected $location;
-
-    /**
-     * @var string
-     */
-    protected $custom_rss_string;
-
-    /**
-     * @var string
-     */
-    protected $publication_status;
+    protected ?Location $location;
+    protected string $custom_rss_string;
+    protected string $publication_status;
 
     /**
      * @var string[]
@@ -221,10 +163,8 @@ class Episode extends Entity
 
     /**
      * Saves an episode image
-     *
-     * @param Image|null $image
      */
-    public function setImage($image = null): self
+    public function setImage(?Image $image = null): static
     {
         if ($image === null) {
             return $this;
@@ -257,10 +197,8 @@ class Episode extends Entity
 
     /**
      * Saves an audio file
-     *
-     * @param UploadedFile|File $audioFile
      */
-    public function setAudioFile($audioFile)
+    public function setAudioFile(UploadedFile|File $audioFile): static
     {
         helper(['media', 'id3']);
 
@@ -283,10 +221,8 @@ class Episode extends Entity
 
     /**
      * Saves an episode transcript file
-     *
-     * @param UploadedFile|File $transcriptFile
      */
-    public function setTranscriptFile($transcriptFile)
+    public function setTranscriptFile(UploadedFile|File $transcriptFile): static
     {
         helper('media');
 
@@ -301,10 +237,8 @@ class Episode extends Entity
 
     /**
      * Saves an episode chapters file
-     *
-     * @param UploadedFile|File $chaptersFile
      */
-    public function setChaptersFile($chaptersFile)
+    public function setChaptersFile(UploadedFile|File $chaptersFile): static
     {
         helper('media');
 
@@ -390,9 +324,8 @@ class Episode extends Entity
     {
         if ($this->attributes['transcript_file_path']) {
             return media_base_url($this->attributes['transcript_file_path']);
-        } else {
-            return $this->attributes['transcript_file_remote_url'];
         }
+        return $this->attributes['transcript_file_remote_url'];
     }
 
     /**
@@ -411,7 +344,7 @@ class Episode extends Entity
     /**
      * Returns the episode's persons
      *
-     * @return EpisodePerson[]
+     * @return Person[]
      */
     public function getPersons(): array
     {
@@ -422,7 +355,7 @@ class Episode extends Entity
         }
 
         if (empty($this->persons)) {
-            $this->persons = (new EpisodePersonModel())->getEpisodePersons(
+            $this->persons = (new PersonModel())->getEpisodePersons(
                 $this->podcast_id,
                 $this->id,
             );
@@ -483,7 +416,7 @@ class Episode extends Entity
         );
     }
 
-    public function getEmbeddablePlayerUrl($theme = null): string
+    public function getEmbeddablePlayerUrl(string $theme = null): string
     {
         return base_url(
             $theme
@@ -501,25 +434,21 @@ class Episode extends Entity
         );
     }
 
-    public function setGuid(?string $guid = null)
+    public function setGuid(?string $guid = null): static
     {
-        if ($guid === null) {
-            $this->attributes['guid'] = $this->getLink();
-        } else {
-            $this->attributes['guid'] = $guid;
-        }
+        $this->attributes['guid'] = $guid === null ? $this->getLink() : $guid;
 
         return $this;
     }
 
-    public function getPodcast(): Podcast
+    public function getPodcast(): ?Podcast
     {
         return (new PodcastModel())->getPodcastById(
             $this->attributes['podcast_id'],
         );
     }
 
-    public function setDescriptionMarkdown(string $descriptionMarkdown)
+    public function setDescriptionMarkdown(string $descriptionMarkdown): static
     {
         $converter = new CommonMarkConverter([
             'html_input' => 'strip',
@@ -563,7 +492,7 @@ class Episode extends Entity
         if ($this->description === null) {
             $this->description = trim(
                 preg_replace(
-                    '/\s+/',
+                    '~\s+~',
                     ' ',
                     strip_tags($this->attributes['description_html']),
                 ),
@@ -575,11 +504,11 @@ class Episode extends Entity
 
     public function getPublicationStatus(): string
     {
-        if ($this->publication_status) {
+        if ($this->publication_status !== '') {
             return $this->publication_status;
         }
 
-        if (!$this->published_at) {
+        if ($this->published_at === null) {
             return 'not_published';
         }
 
@@ -594,7 +523,7 @@ class Episode extends Entity
     /**
      * Saves the location name and fetches OpenStreetMap info
      */
-    public function setLocation(?string $newLocationName = null)
+    public function setLocation(?string $newLocationName = null): static
     {
         if ($newLocationName === null) {
             $this->attributes['location_name'] = null;
@@ -667,7 +596,7 @@ class Episode extends Entity
     /**
      * Saves custom rss tag into json
      */
-    function setCustomRssString(?string $customRssString = null)
+    function setCustomRssString(?string $customRssString = null): static
     {
         if ($customRssString === null) {
             return $this;
@@ -709,19 +638,16 @@ class Episode extends Entity
         return $partnerLink;
     }
 
-    function getPartnerImageUrl($serviceSlug = null): string
+    function getPartnerImageUrl(string $serviceSlug = null): string
     {
-        $partnerImageUrl =
-            rtrim($this->getPodcast()->partner_image_url, '/') .
-            '?pid=' .
-            $this->getPodcast()->partner_id .
-            '&guid=' .
-            urlencode($this->attributes['guid']);
-
         if ($serviceSlug !== null) {
-            $partnerImageUrl = '&_from=' . $serviceSlug;
+            return '&_from=' . $serviceSlug;
         }
 
-        return $partnerImageUrl;
+        return rtrim($this->getPodcast()->partner_image_url, '/') .
+        '?pid=' .
+        $this->getPodcast()->partner_id .
+        '&guid=' .
+        urlencode($this->attributes['guid']);
     }
 }
diff --git a/app/Entities/EpisodePerson.php b/app/Entities/EpisodePerson.php
deleted file mode 100644
index 9113d353c3..0000000000
--- a/app/Entities/EpisodePerson.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-/**
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Entities;
-
-use CodeIgniter\Entity\Entity;
-use App\Models\PersonModel;
-
-/**
- * @property int $id
- * @property int $podcast_id
- * @property int $episode_id
- * @property int $person_id
- * @property Person $person
- * @property string|null $person_group
- * @property string|null $person_role
- */
-class EpisodePerson extends Entity
-{
-    /**
-     * @var Person
-     */
-    protected $person;
-
-    /**
-     * @var array<string, string>
-     */
-    protected $casts = [
-        'id' => 'integer',
-        'podcast_id' => 'integer',
-        'episode_id' => 'integer',
-        'person_id' => 'integer',
-        'person_group' => '?string',
-        'person_role' => '?string',
-    ];
-
-    public function getPerson(): Person
-    {
-        return (new PersonModel())->getPersonById(
-            $this->attributes['person_id'],
-        );
-    }
-}
diff --git a/app/Entities/Image.php b/app/Entities/Image.php
index 5a9fa82642..74196cad7a 100644
--- a/app/Entities/Image.php
+++ b/app/Entities/Image.php
@@ -10,7 +10,7 @@ namespace App\Entities;
 
 use CodeIgniter\Entity\Entity;
 use CodeIgniter\Files\File;
-use Config\Images as ImagesConfig;
+use Config\Images;
 use Config\Services;
 use RuntimeException;
 
@@ -35,30 +35,11 @@ use RuntimeException;
  */
 class Image extends Entity
 {
-    /**
-     * @var ImagesConfig
-     */
-    protected $config;
-
-    /**
-     * @var null|File
-     */
-    protected $file;
-
-    /**
-     * @var string
-     */
-    protected $dirname;
-
-    /**
-     * @var string
-     */
-    protected $filename;
-
-    /**
-     * @var string
-     */
-    protected $extension;
+    protected Images $config;
+    protected ?File $file;
+    protected string $dirname;
+    protected string $filename;
+    protected string $extension;
 
     public function __construct(
         ?File $file,
diff --git a/app/Entities/Note.php b/app/Entities/Note.php
index c6069c6e8b..eeec4e3fdd 100644
--- a/app/Entities/Note.php
+++ b/app/Entities/Note.php
@@ -9,21 +9,23 @@
 namespace App\Entities;
 
 use ActivityPub\Entities\Note as ActivityPubNote;
-use App\Models\ActorModel;
 use App\Models\EpisodeModel;
 use RuntimeException;
 
 /**
  * @property int|null $episode_id
  * @property Episode|null $episode
+ * @property Actor $actor
+ * @property Note $reblog_of_note
+ * @property Note $reply_to_note
  */
 class Note extends ActivityPubNote
 {
+    protected ?Episode $episode;
+
     /**
-     * @var Episode|null
+     * @var array<string, string>
      */
-    protected $episode;
-
     protected $casts = [
         'id' => 'string',
         'uri' => 'string',
@@ -41,10 +43,8 @@ class Note extends ActivityPubNote
 
     /**
      * Returns the note's attached episode
-     *
-     * @return \App\Entities\Episode
      */
-    public function getEpisode()
+    public function getEpisode(): ?Episode
     {
         if ($this->episode_id === null) {
             throw new RuntimeException(
diff --git a/app/Entities/Page.php b/app/Entities/Page.php
index 500e5bc76b..11c3734e19 100644
--- a/app/Entities/Page.php
+++ b/app/Entities/Page.php
@@ -25,15 +25,8 @@ use League\CommonMark\CommonMarkConverter;
  */
 class Page extends Entity
 {
-    /**
-     * @var string
-     */
-    protected $link;
-
-    /**
-     * @var string
-     */
-    protected $content_html;
+    protected string $link;
+    protected string $content_html;
 
     /**
      * @var array<string, string>
@@ -51,7 +44,7 @@ class Page extends Entity
         return url_to('page', $this->attributes['slug']);
     }
 
-    public function setContentMarkdown(string $contentMarkdown): self
+    public function setContentMarkdown(string $contentMarkdown): static
     {
         $converter = new CommonMarkConverter([
             'html_input' => 'strip',
diff --git a/app/Entities/Person.php b/app/Entities/Person.php
index 5946d95eb7..7bfd0b163f 100644
--- a/app/Entities/Person.php
+++ b/app/Entities/Person.php
@@ -20,13 +20,16 @@ use CodeIgniter\Entity\Entity;
  * @property string $image_mimetype
  * @property int $created_by
  * @property int $updated_by
+ * @property string|null $group
+ * @property string|null $role
+ * @property Podcast|null $podcast
+ * @property Episode|null $episode
  */
 class Person extends Entity
 {
-    /**
-     * @var Image
-     */
-    protected $image;
+    protected Image $image;
+    protected ?Podcast $podcast;
+    protected ?Episode $episode;
 
     /**
      * @var array<string, string>
@@ -38,6 +41,10 @@ class Person extends Entity
         'information_url' => '?string',
         'image_path' => 'string',
         'image_mimetype' => 'string',
+        'podcast_id' => '?integer',
+        'episode_id' => '?integer',
+        'group' => '?string',
+        'role' => '?string',
         'created_by' => 'integer',
         'updated_by' => 'integer',
     ];
@@ -45,7 +52,7 @@ class Person extends Entity
     /**
      * Saves a picture in `public/media/persons/`
      */
-    public function setImage(Image $image): self
+    public function setImage(Image $image): static
     {
         helper('media');
 
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 7476e20d81..7b84d3df9f 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -11,8 +11,8 @@ namespace App\Entities;
 use App\Libraries\SimpleRSSElement;
 use App\Models\CategoryModel;
 use App\Models\EpisodeModel;
+use App\Models\PersonModel;
 use App\Models\PlatformModel;
-use App\Models\PodcastPersonModel;
 use CodeIgniter\Entity\Entity;
 use App\Models\UserModel;
 use CodeIgniter\I18n\Time;
@@ -22,7 +22,7 @@ use RuntimeException;
 /**
  * @property int $id
  * @property int $actor_id
- * @property Actor $actor
+ * @property Actor|null $actor
  * @property string $name
  * @property string $link
  * @property string $feed_url
@@ -35,7 +35,7 @@ use RuntimeException;
  * @property string $image_mimetype
  * @property string $language_code
  * @property int $category_id
- * @property Category $category
+ * @property Category|null $category
  * @property int[] $other_categories_ids
  * @property Category[] $other_categories
  * @property string|null $parental_advisory
@@ -68,7 +68,7 @@ use RuntimeException;
  * @property Time|null $deleted_at;
  *
  * @property Episode[] $episodes
- * @property PodcastPerson[] $persons
+ * @property Person[] $persons
  * @property User[] $contributors
  * @property Platform[] $podcasting_platforms
  * @property Platform[] $social_platforms
@@ -77,80 +77,54 @@ use RuntimeException;
  */
 class Podcast extends Entity
 {
-    /**
-     * @var string
-     */
-    protected $link;
-
-    /**
-     * @var Actor
-     */
-    protected $actor;
-
-    /**
-     * @var Image
-     */
-    protected $image;
-
-    /**
-     * @var string
-     */
-    protected $description;
-
-    /**
-     * @var Category
-     */
-    protected $category;
+    protected string $link;
+    protected ?Actor $actor;
+    protected Image $image;
+    protected string $description;
+    protected ?Category $category;
 
     /**
      * @var Category[]
      */
-    protected $other_categories;
+    protected $other_categories = [];
 
     /**
      * @var string[]
      */
-    protected $other_categories_ids;
+    protected $other_categories_ids = [];
 
     /**
      * @var Episode[]
      */
-    protected $episodes;
+    protected $episodes = [];
 
     /**
-     * @var PodcastPerson[]
+     * @var Person[]
      */
-    protected $persons;
+    protected $persons = [];
 
     /**
      * @var User[]
      */
-    protected $contributors;
+    protected $contributors = [];
 
     /**
      * @var Platform[]
      */
-    protected $podcasting_platforms;
+    protected $podcasting_platforms = [];
 
     /**
      * @var Platform[]
      */
-    protected $social_platforms;
+    protected $social_platforms = [];
 
     /**
      * @var Platform[]
      */
-    protected $funding_platforms;
+    protected $funding_platforms = [];
 
-    /**
-     * @var Location|null
-     */
-    protected $location;
-
-    /**
-     * @var string
-     */
-    protected $custom_rss_string;
+    protected ?Location $location;
+    protected string $custom_rss_string;
 
     /**
      * @var array<string, string>
@@ -193,7 +167,7 @@ class Podcast extends Entity
 
     public function getActor(): Actor
     {
-        if (!$this->actor_id) {
+        if ($this->actor_id === 0) {
             throw new RuntimeException(
                 'Podcast must have an actor_id before getting actor.',
             );
@@ -208,10 +182,8 @@ class Podcast extends Entity
 
     /**
      * Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
-     *
-     * @param Image $image
      */
-    public function setImage($image): self
+    public function setImage(Image $image): static
     {
         // Save image
         $image->saveImage('podcasts/' . $this->attributes['name'], 'cover');
@@ -263,7 +235,7 @@ class Podcast extends Entity
     /**
      * Returns the podcast's persons
      *
-     * @return PodcastPerson[]
+     * @return Person[]
      */
     public function getPersons(): array
     {
@@ -274,9 +246,7 @@ class Podcast extends Entity
         }
 
         if (empty($this->persons)) {
-            $this->persons = (new PodcastPersonModel())->getPodcastPersons(
-                $this->id,
-            );
+            $this->persons = (new PersonModel())->getPodcastPersons($this->id);
         }
 
         return $this->persons;
@@ -284,18 +254,16 @@ class Podcast extends Entity
 
     /**
      * Returns the podcast category entity
-     *
-     * @return Category
      */
-    public function getCategory(): Category
+    public function getCategory(): ?Category
     {
-        if (empty($this->id)) {
+        if ($this->id === null) {
             throw new RuntimeException(
                 'Podcast must be created before getting category.',
             );
         }
 
-        if (empty($this->category)) {
+        if ($this->category === null) {
             $this->category = (new CategoryModel())->getCategoryById(
                 $this->category_id,
             );
@@ -326,7 +294,7 @@ class Podcast extends Entity
         return $this->contributors;
     }
 
-    public function setDescriptionMarkdown(string $descriptionMarkdown): self
+    public function setDescriptionMarkdown(string $descriptionMarkdown): static
     {
         $converter = new CommonMarkConverter([
             'html_input' => 'strip',
@@ -343,7 +311,7 @@ class Podcast extends Entity
 
     public function setEpisodeDescriptionFooterMarkdown(
         ?string $episodeDescriptionFooterMarkdown = null
-    ): self {
+    ): static {
         if ($episodeDescriptionFooterMarkdown) {
             $converter = new CommonMarkConverter([
                 'html_input' => 'strip',
@@ -363,13 +331,13 @@ class Podcast extends Entity
 
     public function getDescription(): string
     {
-        if ($this->description) {
+        if ($this->description !== '') {
             return $this->description;
         }
 
         return trim(
             preg_replace(
-                '/\s+/',
+                '~\s+~',
                 ' ',
                 strip_tags($this->attributes['description_html']),
             ),
@@ -483,7 +451,7 @@ class Podcast extends Entity
     /**
      * Saves the location name and fetches OpenStreetMap info
      */
-    public function setLocation(?string $newLocationName = null)
+    public function setLocation(?string $newLocationName = null): static
     {
         if ($newLocationName === null) {
             $this->attributes['location_name'] = null;
@@ -529,8 +497,6 @@ class Podcast extends Entity
 
     /**
      * Get custom rss tag as XML String
-     *
-     * @return string
      */
     function getCustomRssString(): string
     {
@@ -555,10 +521,8 @@ class Podcast extends Entity
 
     /**
      * Saves custom rss tag into json
-     *
-     * @param string $customRssString
      */
-    function setCustomRssString($customRssString): self
+    function setCustomRssString(string $customRssString): static
     {
         if (empty($customRssString)) {
             return $this;
diff --git a/app/Entities/PodcastPerson.php b/app/Entities/PodcastPerson.php
deleted file mode 100644
index a3f06aecd3..0000000000
--- a/app/Entities/PodcastPerson.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Entities;
-
-use CodeIgniter\Entity\Entity;
-use App\Models\PersonModel;
-
-/**
- * @property int $id
- * @property int $podcast_id
- * @property int $person_id
- * @property Person $person
- * @property string|null $person_group
- * @property string|null $person_role
- */
-class PodcastPerson extends Entity
-{
-    /**
-     * @var Person
-     */
-    protected $person;
-
-    /**
-     * @var array<string, string>
-     */
-    protected $casts = [
-        'id' => 'integer',
-        'podcast_id' => 'integer',
-        'person_id' => 'integer',
-        'person_group' => '?string',
-        'person_role' => '?string',
-    ];
-
-    public function getPerson(): ?Person
-    {
-        return (new PersonModel())->getPersonById(
-            $this->attributes['person_id'],
-        );
-    }
-}
diff --git a/app/Filters/PermissionFilter.php b/app/Filters/PermissionFilter.php
index f4142387f8..84495dc38c 100644
--- a/app/Filters/PermissionFilter.php
+++ b/app/Filters/PermissionFilter.php
@@ -21,7 +21,7 @@ class PermissionFilter implements FilterInterface
      * sent back to the client, allowing for error pages,
      * redirects, etc.
      *
-     * @param array|null                         $params
+     * @param string[]|null                         $params
      * @return void|mixed
      */
     public function before(RequestInterface $request, $params = null)
@@ -50,8 +50,8 @@ class PermissionFilter implements FilterInterface
         foreach ($params as $permission) {
             // check if permission is for a specific podcast
             if (
-                (startsWith($permission, 'podcast-') ||
-                    startsWith($permission, 'podcast_episodes-')) &&
+                (str_starts_with($permission, 'podcast-') ||
+                    str_starts_with($permission, 'podcast_episodes-')) &&
                 count($routerParams) > 0
             ) {
                 if (
@@ -91,7 +91,7 @@ class PermissionFilter implements FilterInterface
      * to stop execution of other after filters, short of
      * throwing an Exception or Error.
      *
-     * @param array|null                          $arguments
+     * @param string[]|null                          $arguments
      */
     public function after(
         RequestInterface $request,
diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php
index b390d51626..4202b3df1a 100644
--- a/app/Helpers/auth_helper.php
+++ b/app/Helpers/auth_helper.php
@@ -27,7 +27,7 @@ if (!function_exists('set_interact_as_actor')) {
     /**
      * Sets the actor id of which the user is acting as
      */
-    function set_interact_as_actor($actorId): void
+    function set_interact_as_actor(int $actorId): void
     {
         $authenticate = Services::authentication();
         $authenticate->check();
@@ -65,10 +65,8 @@ if (!function_exists('interact_as_actor_id')) {
 if (!function_exists('interact_as_actor')) {
     /**
      * Get the actor the user is currently interacting as
-     *
-     * @return Actor|false
      */
-    function interact_as_actor()
+    function interact_as_actor(): Actor|false
     {
         $authenticate = Services::authentication();
         $authenticate->check();
diff --git a/app/Helpers/breadcrumb_helper.php b/app/Helpers/breadcrumb_helper.php
index 503ee07711..5be34556f1 100644
--- a/app/Helpers/breadcrumb_helper.php
+++ b/app/Helpers/breadcrumb_helper.php
@@ -23,7 +23,10 @@ if (!function_exists('render_breadcrumb')) {
 }
 
 if (!function_exists('replace_breadcrumb_params')) {
-    function replace_breadcrumb_params($newParams): void
+    /**
+     * @param string[] $newParams
+     */
+    function replace_breadcrumb_params(array $newParams): void
     {
         $breadcrumb = Services::breadcrumb();
         $breadcrumb->replaceParams($newParams);
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index a156d3fdab..647bfe2031 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -16,10 +16,8 @@ if (!function_exists('button')) {
      *
      * Creates a stylized button or button like anchor tag if the URL is defined.
      *
-     * @param array  $customOptions button options: variant, size, iconLeft, iconRight
-     * @param array  $customAttributes Additional attributes
-     *
-     * @return string
+     * @param array<string, string|null|bool> $customOptions button options: variant, size, iconLeft, iconRight
+     * @param array<string, string> $customAttributes Additional attributes
      */
     function button(
         string $label = '',
@@ -130,10 +128,8 @@ if (!function_exists('icon_button')) {
      *
      * @param string $icon The button icon
      * @param string $title The button label
-     * @param array  $customOptions button options: variant, size, iconLeft, iconRight
-     * @param array  $customAttributes Additional attributes
-     *
-     * @return string
+     * @param array<string, string|null|bool>  $customOptions button options: variant, size, iconLeft, iconRight
+     * @param array<string, string>  $customAttributes Additional attributes
      */
     function icon_button(
         string $icon,
@@ -167,8 +163,6 @@ if (!function_exists('hint_tooltip')) {
      * Used to produce tooltip with a question mark icon for hint texts
      *
      * @param string $hintText The hint text
-     *
-     * @return string
      */
     function hint_tooltip(string $hintText = '', string $class = ''): string
     {
@@ -193,11 +187,9 @@ if (!function_exists('data_table')) {
      *
      * Creates a stylized table.
      *
-     * @param array     $columns array of associate arrays with `header` and `cell` keys where `cell` is a function with a row of $data as parameter
-     * @param array     $data data to loop through and display in rows
-     * @param array     ...$rest Any other argument to pass to the `cell` function
-     *
-     * @return string
+     * @param array<array<string, mixed>> $columns array of associate arrays with `header` and `cell` keys where `cell` is a function with a row of $data as parameter
+     * @param mixed[] $data data to loop through and display in rows
+     * @param mixed ...$rest Any other argument to pass to the `cell` function
      */
     function data_table(array $columns, array $data = [], ...$rest): string
     {
@@ -252,8 +244,6 @@ if (!function_exists('publication_pill')) {
      * Publication pill component
      *
      * Shows the stylized publication datetime in regards to current datetime.
-     *
-     * @return string
      */
     function publication_pill(
         ?Time $publicationDate,
@@ -303,7 +293,6 @@ if (!function_exists('publication_button')) {
      * Displays the appropriate publication button depending on the publication status.
      *
      * @param boolean   $publicationStatus the episode's publication status     *
-     * @return string
      */
     function publication_button(
         int $podcastId,
diff --git a/app/Helpers/form_helper.php b/app/Helpers/form_helper.php
index afa928f283..180efd5e43 100644
--- a/app/Helpers/form_helper.php
+++ b/app/Helpers/form_helper.php
@@ -15,9 +15,7 @@ if (!function_exists('form_section')) {
      *
      * @param string $title The section title
      * @param string $subtitle The section subtitle
-     * @param array  $attributes  Additional attributes
-     *
-     * @return string
+     * @param array<string, string>  $attributes  Additional attributes
      */
     function form_section(
         string $title = '',
@@ -54,9 +52,7 @@ if (!function_exists('form_section_close')) {
     /**
      * Form Section close Tag
      *
-     * @param string $extra
      *
-     * @return string
      */
     function form_section_close(string $extra = ''): string
     {
@@ -72,10 +68,11 @@ if (!function_exists('form_switch')) {
      *
      * Abstracts form_label to stylize it as a switch toggle
      *
-     * @return string
+     * @param mixed[] $data
+     * @param mixed[] $extra
      */
     function form_switch(
-        $label = '',
+        string $label = '',
         array $data = [],
         string $value = '',
         bool $checked = false,
@@ -104,11 +101,9 @@ if (!function_exists('form_label')) {
      *
      * @param string $label_text The text to appear onscreen
      * @param string $id         The id the label applies to
-     * @param array  $attributes Additional attributes
+     * @param array<string, string>  $attributes Additional attributes
      * @param string  $hintText Hint text to add next to the label
      * @param boolean  $isOptional adds an optional text if true
-     *
-     * @return string
      */
     function form_label(
         string $label_text = '',
@@ -151,7 +146,9 @@ if (!function_exists('form_multiselect')) {
     /**
      * Multi-select menu
      *
-     * @return string
+     * @param array<string, string> $options
+     * @param string[] $selected
+     * @param array<string, string> $customExtra
      */
     function form_multiselect(
         string $name = '',
diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php
index 174dea022c..b0739c6c73 100644
--- a/app/Helpers/id3_helper.php
+++ b/app/Helpers/id3_helper.php
@@ -68,7 +68,7 @@ if (!function_exists('write_audio_file_tags')) {
             ],
             'album' => [$episode->podcast->title],
             'year' => [
-                $episode->published_at
+                $episode->published_at !== null
                     ? $episode->published_at->format('Y')
                     : '',
             ],
diff --git a/app/Helpers/location_helper.php b/app/Helpers/location_helper.php
index 1c3701d9b3..9a91e8a1d5 100644
--- a/app/Helpers/location_helper.php
+++ b/app/Helpers/location_helper.php
@@ -11,6 +11,10 @@ use Config\Services;
 if (!function_exists('fetch_osm_location')) {
     /**
      * Fetches places from Nominatim OpenStreetMap
+     *
+     * TODO: move this to Location object?
+     *
+     * @return array<string, string>|null
      */
     function fetch_osm_location(string $locationName): ?array
     {
diff --git a/app/Helpers/media_helper.php b/app/Helpers/media_helper.php
index 8491a9642c..c644827a7a 100644
--- a/app/Helpers/media_helper.php
+++ b/app/Helpers/media_helper.php
@@ -20,7 +20,7 @@ if (!function_exists('save_media')) {
     function save_media(
         File $file,
         string $folder = '',
-        string $filename
+        string $filename = ''
     ): string {
         if (($extension = $file->getExtension()) !== '') {
             $filename = $filename . '.' . $extension;
@@ -91,9 +91,9 @@ if (!function_exists('media_path')) {
     /**
      * Prefixes the root media path to a given uri
      *
-     * @param  string|array  $uri URI string or array of URI segments
+     * @param  string|string[] $uri URI string or array of URI segments
      */
-    function media_path($uri = ''): string
+    function media_path(string|array $uri = ''): string
     {
         // convert segment array to string
         if (is_array($uri)) {
@@ -109,9 +109,9 @@ if (!function_exists('media_base_url')) {
     /**
      * Return the media base URL to use in views
      *
-     * @param  string|string[] $uri      URI string or array of URI segments
+     * @param  string|string[] $uri URI string or array of URI segments
      */
-    function media_base_url($uri = ''): string
+    function media_base_url(string|array $uri = ''): string
     {
         // convert segment array to string
         if (is_array($uri)) {
diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php
index 91983d48f3..d8d4e8a29e 100644
--- a/app/Helpers/misc_helper.php
+++ b/app/Helpers/misc_helper.php
@@ -23,23 +23,9 @@ if (!function_exists('get_browser_language')) {
     }
 }
 
-if (!function_exists('startsWith')) {
-    /**
-     * Check if a string starts with some characters
-     */
-    function startsWith(string $string, string $query): bool
-    {
-        return substr($string, 0, strlen($query)) === $query;
-    }
-}
-
 if (!function_exists('slugify')) {
-    function slugify($text)
+    function slugify(string $text): string
     {
-        if (empty($text)) {
-            return 'n-a';
-        }
-
         // replace non letter or digits by -
         $text = preg_replace('~[^\pL\d]+~u', '-', $text);
 
diff --git a/app/Helpers/persons_helper.php b/app/Helpers/persons_helper.php
deleted file mode 100644
index c7863d4a1d..0000000000
--- a/app/Helpers/persons_helper.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @copyright  2021 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-use App\Entities\Person;
-use App\Entities\EpisodePerson;
-use App\Entities\PodcastPerson;
-
-if (!function_exists('construct_person_array')) {
-    /**
-     * Fetches persons from an episode
-     *
-     * @param Person[]|PodcastPerson[]|EpisodePerson[] $persons
-     */
-    function construct_person_array(array $persons, array &$personsArray): void
-    {
-        foreach ($persons as $person) {
-            if (array_key_exists($person->id, $personsArray)) {
-                $personsArray[$person->id]['roles'] .=
-                    empty($person->person_group) || empty($person->person_role)
-                        ? ''
-                        : (empty($personsArray[$person->id]['roles'])
-                                ? ''
-                                : ', ') .
-                            lang(
-                                'PersonsTaxonomy.persons.' .
-                                    $person->person_group .
-                                    '.roles.' .
-                                    $person->person_role .
-                                    '.label',
-                            );
-            } else {
-                $personsArray[$person->person->id] = [
-                    'full_name' => $person->person->full_name,
-                    'information_url' => $person->person->information_url,
-                    'thumbnail_url' => $person->person->image->thumbnail_url,
-                    'roles' =>
-                        empty($person->person_group) ||
-                        empty($person->person_role)
-                            ? ''
-                            : lang(
-                                'PersonsTaxonomy.persons.' .
-                                    $person->person_group .
-                                    '.roles.' .
-                                    $person->person_role .
-                                    '.label',
-                            ),
-                ];
-            }
-        }
-    }
-}
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index 498fee97c1..41adc5460d 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -187,19 +187,19 @@ if (!function_exists('get_rss_feed')) {
         foreach ($podcast->persons as $podcastPerson) {
             $podcastPersonElement = $channel->addChild(
                 'person',
-                htmlspecialchars($podcastPerson->person->full_name),
+                htmlspecialchars($podcastPerson->full_name),
                 $podcast_namespace,
             );
 
             if (
-                $podcastPerson->person_role !== null &&
-                $podcastPerson->person_group !== null
+                $podcastPerson->role !== null &&
+                $podcastPerson->role !== null
             ) {
                 $podcastPersonElement->addAttribute(
                     'role',
                     htmlspecialchars(
                         lang(
-                            "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
+                            "PersonsTaxonomy.persons.{$podcastPerson->group}.roles.{$podcastPerson->role}.label",
                             [],
                             'en',
                         ),
@@ -207,27 +207,28 @@ if (!function_exists('get_rss_feed')) {
                 );
             }
 
-            if ($podcastPerson->person_group !== null) {
+            if ($podcastPerson->group !== null) {
                 $podcastPersonElement->addAttribute(
                     'group',
                     htmlspecialchars(
                         lang(
-                            "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
+                            "PersonsTaxonomy.persons.{$podcastPerson->group}.label",
                             [],
                             'en',
                         ),
                     ),
                 );
             }
+
             $podcastPersonElement->addAttribute(
                 'img',
-                $podcastPerson->person->image->large_url,
+                $podcastPerson->image->large_url,
             );
 
-            if ($podcastPerson->person->information_url !== null) {
+            if ($podcastPerson->information_url !== null) {
                 $podcastPersonElement->addAttribute(
                     'href',
-                    $podcastPerson->person->information_url,
+                    $podcastPerson->information_url,
                 );
             }
         }
@@ -417,18 +418,18 @@ if (!function_exists('get_rss_feed')) {
             foreach ($episode->persons as $episodePerson) {
                 $episodePersonElement = $item->addChild(
                     'person',
-                    htmlspecialchars($episodePerson->person->full_name),
+                    htmlspecialchars($episodePerson->full_name),
                     $podcast_namespace,
                 );
                 if (
-                    !empty($episodePerson->person_role) &&
-                    !empty($episodePerson->person_group)
+                    !empty($episodePerson->role) &&
+                    !empty($episodePerson->group)
                 ) {
                     $episodePersonElement->addAttribute(
                         'role',
                         htmlspecialchars(
                             lang(
-                                "PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
+                                "PersonsTaxonomy.persons.{$episodePerson->group}.roles.{$episodePerson->role}.label",
                                 [],
                                 'en',
                             ),
@@ -440,7 +441,7 @@ if (!function_exists('get_rss_feed')) {
                         'group',
                         htmlspecialchars(
                             lang(
-                                "PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
+                                "PersonsTaxonomy.persons.{$episodePerson->group}.label",
                                 [],
                                 'en',
                             ),
@@ -449,12 +450,12 @@ if (!function_exists('get_rss_feed')) {
                 }
                 $episodePersonElement->addAttribute(
                     'img',
-                    $episodePerson->person->image->large_url,
+                    $episodePerson->image->large_url,
                 );
-                if (!empty($episodePerson->person->information_url)) {
+                if (!empty($episodePerson->information_url)) {
                     $episodePersonElement->addAttribute(
                         'href',
-                        $episodePerson->person->information_url,
+                        $episodePerson->information_url,
                     );
                 }
             }
@@ -512,10 +513,11 @@ if (!function_exists('rss_to_array')) {
     /**
      * Converts XML to array
      *
-     * FIXME: should be SimpleRSSElement
-     * @param SimpleXMLElement $xmlNode
+     * FIXME: param should be SimpleRSSElement
+     *
+     * @return array<string, mixed>
      */
-    function rss_to_array(SimpleXMLElement $xmlNode): array
+    function rss_to_array(SimpleXMLElement $rssNode): array
     {
         $nameSpaces = [
             '',
@@ -523,17 +525,17 @@ if (!function_exists('rss_to_array')) {
             'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
         ];
         $arrayNode = [];
-        $arrayNode['name'] = $xmlNode->getName();
-        $arrayNode['namespace'] = $xmlNode->getNamespaces(false);
-        foreach ($xmlNode->attributes() as $key => $value) {
+        $arrayNode['name'] = $rssNode->getName();
+        $arrayNode['namespace'] = $rssNode->getNamespaces(false);
+        foreach ($rssNode->attributes() as $key => $value) {
             $arrayNode['attributes'][$key] = (string) $value;
         }
-        $textcontent = trim((string) $xmlNode);
+        $textcontent = trim((string) $rssNode);
         if (strlen($textcontent) > 0) {
             $arrayNode['content'] = $textcontent;
         }
         foreach ($nameSpaces as $currentNameSpace) {
-            foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
+            foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {
                 $arrayNode['elements'][] = rss_to_array($childXmlNode);
             }
         }
@@ -546,10 +548,13 @@ if (!function_exists('array_to_rss')) {
     /**
      * Inserts array (converted to XML node) in XML node
      *
+     * @param array<string, mixed> $arrayNode
      * @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
      */
-    function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode)
-    {
+    function array_to_rss(
+        array $arrayNode,
+        SimpleRSSElement &$xmlNode
+    ): SimpleRSSElement {
         if (array_key_exists('elements', $arrayNode)) {
             foreach ($arrayNode['elements'] as $childArrayNode) {
                 $childXmlNode = $xmlNode->addChild(
diff --git a/app/Helpers/url_helper.php b/app/Helpers/url_helper.php
index 80b4f7b562..f92d34b0b8 100644
--- a/app/Helpers/url_helper.php
+++ b/app/Helpers/url_helper.php
@@ -47,7 +47,9 @@ if (!function_exists('current_season_url')) {
 
 if (!function_exists('extract_params_from_episode_uri')) {
     /**
-     * Returns podcast name and episode slug from episode string uri
+     * Returns podcast name and episode slug from episode string
+     *
+     * @return array<string, string>|null
      */
     function extract_params_from_episode_uri(URI $episodeUri): ?array
     {
diff --git a/app/Libraries/ActivityPub/Activities/AnnounceActivity.php b/app/Libraries/ActivityPub/Activities/AnnounceActivity.php
index 30dcf4d292..e96508982c 100644
--- a/app/Libraries/ActivityPub/Activities/AnnounceActivity.php
+++ b/app/Libraries/ActivityPub/Activities/AnnounceActivity.php
@@ -14,6 +14,7 @@
 namespace ActivityPub\Activities;
 
 use ActivityPub\Core\Activity;
+use ActivityPub\Entities\Note;
 
 class AnnounceActivity extends Activity
 {
@@ -22,7 +23,7 @@ class AnnounceActivity extends Activity
      */
     protected $type = 'Announce';
 
-    public function __construct($reblogNote)
+    public function __construct(Note $reblogNote)
     {
         $this->actor = $reblogNote->actor->uri;
         $this->object = $reblogNote->reblog_of_note->uri;
diff --git a/app/Libraries/ActivityPub/ActivityRequest.php b/app/Libraries/ActivityPub/ActivityRequest.php
index 9b3accbdaf..f2c88e09da 100644
--- a/app/Libraries/ActivityPub/ActivityRequest.php
+++ b/app/Libraries/ActivityPub/ActivityRequest.php
@@ -34,7 +34,7 @@ class ActivityRequest
     protected $activity;
 
     /**
-     * @var array
+     * @var array<string, string[]>
      */
     protected $options = [
         'headers' => [
@@ -71,7 +71,7 @@ class ActivityRequest
             ($this->uri->getPort() ? ':' . $this->uri->getPort() : '');
     }
 
-    public function sign($keyId, $privateKey): void
+    public function sign(string $keyId, string $privateKey): void
     {
         $rsa = new RSA();
         $rsa->loadKey($privateKey); // private key
diff --git a/app/Libraries/ActivityPub/Config/ActivityPub.php b/app/Libraries/ActivityPub/Config/ActivityPub.php
index 3ac4e4fc67..47ae7d3e9b 100644
--- a/app/Libraries/ActivityPub/Config/ActivityPub.php
+++ b/app/Libraries/ActivityPub/Config/ActivityPub.php
@@ -8,6 +8,8 @@
 
 namespace ActivityPub\Config;
 
+use ActivityPub\Objects\ActorObject;
+use ActivityPub\Objects\NoteObject;
 use CodeIgniter\Config\BaseConfig;
 
 class ActivityPub extends BaseConfig
@@ -18,12 +20,12 @@ class ActivityPub extends BaseConfig
      * --------------------------------------------------------------------
      * @var string
      */
-    public $actorObject = 'ActivityPub\Objects\ActorObject';
+    public $actorObject = ActorObject::class;
 
     /**
      * @var string
      */
-    public $noteObject = 'ActivityPub\Objects\NoteObject';
+    public $noteObject = NoteObject::class;
 
     /**
      * --------------------------------------------------------------------
diff --git a/app/Libraries/ActivityPub/Controllers/ActorController.php b/app/Libraries/ActivityPub/Controllers/ActorController.php
index 4bc7b77e85..671fa56a00 100644
--- a/app/Libraries/ActivityPub/Controllers/ActorController.php
+++ b/app/Libraries/ActivityPub/Controllers/ActorController.php
@@ -41,7 +41,7 @@ class ActorController extends Controller
         $this->config = config('ActivityPub');
     }
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (
             count($params) > 0 &&
@@ -301,10 +301,7 @@ class ActorController extends Controller
             ->setBody($followersCollection->toJSON());
     }
 
-    /**
-     * @return mixed|ResponseInterface
-     */
-    public function attemptFollow()
+    public function attemptFollow(): RedirectResponse|ResponseInterface
     {
         $rules = [
             'handle' =>
@@ -354,7 +351,7 @@ class ActorController extends Controller
         );
     }
 
-    public function activity($activityId): RedirectResponse
+    public function activity(string $activityId): RedirectResponse
     {
         if (
             !($activity = model('ActivityModel')->getActivityById($activityId))
diff --git a/app/Libraries/ActivityPub/Controllers/BlockController.php b/app/Libraries/ActivityPub/Controllers/BlockController.php
index 22c2e1484a..ef8f1d8938 100644
--- a/app/Libraries/ActivityPub/Controllers/BlockController.php
+++ b/app/Libraries/ActivityPub/Controllers/BlockController.php
@@ -8,6 +8,7 @@
 
 namespace ActivityPub\Controllers;
 
+use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\Controller;
 
 class BlockController extends Controller
@@ -17,7 +18,7 @@ class BlockController extends Controller
      */
     protected $helpers = ['activitypub'];
 
-    public function attemptBlockActor()
+    public function attemptBlockActor(): RedirectResponse
     {
         $rules = [
             'handle' => 'required',
@@ -51,7 +52,7 @@ class BlockController extends Controller
         return redirect()->back();
     }
 
-    function attemptBlockDomain()
+    function attemptBlockDomain(): RedirectResponse
     {
         $rules = [
             'domain' => 'required',
@@ -71,7 +72,7 @@ class BlockController extends Controller
         return redirect()->back();
     }
 
-    function attemptUnblockActor()
+    function attemptUnblockActor(): RedirectResponse
     {
         $rules = [
             'actor_id' => 'required',
@@ -89,7 +90,7 @@ class BlockController extends Controller
         return redirect()->back();
     }
 
-    function attemptUnblockDomain()
+    function attemptUnblockDomain(): RedirectResponse
     {
         $rules = [
             'domain' => 'required',
diff --git a/app/Libraries/ActivityPub/Controllers/NoteController.php b/app/Libraries/ActivityPub/Controllers/NoteController.php
index 4883acc650..bea02669ef 100644
--- a/app/Libraries/ActivityPub/Controllers/NoteController.php
+++ b/app/Libraries/ActivityPub/Controllers/NoteController.php
@@ -41,7 +41,7 @@ class NoteController extends Controller
         $this->config = config('ActivityPub');
     }
 
-    public function _remap(string $method, string ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (!($this->note = model('NoteModel')->getNoteById($params[0]))) {
             throw PageNotFoundException::forPageNotFound();
@@ -63,8 +63,7 @@ class NoteController extends Controller
 
     public function replies(): RedirectResponse
     {
-        /** get note replies
-         * @var NoteModel */
+        /** get note replies */
         $noteReplies = model('NoteModel')
             ->where(
                 'in_reply_to_id',
@@ -216,10 +215,7 @@ class NoteController extends Controller
         return redirect()->back();
     }
 
-    /**
-     * @return mixed|ResponseInterface
-     */
-    public function attemptRemoteAction(string $action)
+    public function attemptRemoteAction(string $action): RedirectResponse|ResponseInterface
     {
         $rules = [
             'handle' =>
diff --git a/app/Libraries/ActivityPub/Controllers/WebFingerController.php b/app/Libraries/ActivityPub/Controllers/WebFingerController.php
index 0ec6f92ab7..136232cb0b 100644
--- a/app/Libraries/ActivityPub/Controllers/WebFingerController.php
+++ b/app/Libraries/ActivityPub/Controllers/WebFingerController.php
@@ -20,7 +20,7 @@ class WebFingerController extends Controller
     {
         try {
             $webfinger = new WebFinger($this->request->getGet('resource'));
-        } catch (Exception $exception) {
+        } catch (Exception) {
             // return 404, actor not found
             throw PageNotFoundException::forPageNotFound();
         }
diff --git a/app/Libraries/ActivityPub/Core/AbstractObject.php b/app/Libraries/ActivityPub/Core/AbstractObject.php
index aeca7c4a48..fa366e7de4 100644
--- a/app/Libraries/ActivityPub/Core/AbstractObject.php
+++ b/app/Libraries/ActivityPub/Core/AbstractObject.php
@@ -18,7 +18,7 @@ abstract class AbstractObject
     /**
      * @param mixed $value
      */
-    public function set(string $property, $value): self
+    public function set(string $property, $value): static
     {
         $this->$property = $value;
 
@@ -49,10 +49,7 @@ abstract class AbstractObject
         });
     }
 
-    /**
-     * @return string|bool
-     */
-    public function toJSON()
+    public function toJSON(): string|bool
     {
         return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
     }
diff --git a/app/Libraries/ActivityPub/Core/ObjectType.php b/app/Libraries/ActivityPub/Core/ObjectType.php
index 5ef6ddcd18..caf64e6d33 100644
--- a/app/Libraries/ActivityPub/Core/ObjectType.php
+++ b/app/Libraries/ActivityPub/Core/ObjectType.php
@@ -16,7 +16,7 @@ namespace ActivityPub\Core;
 class ObjectType extends AbstractObject
 {
     /**
-     * @var array|string
+     * @var string|string[]
      */
     protected $context = 'https://www.w3.org/ns/activitystreams';
 
@@ -41,12 +41,12 @@ class ObjectType extends AbstractObject
     protected $published;
 
     /**
-     * @var array
+     * @var string[]
      */
     protected $to = ['https://www.w3.org/ns/activitystreams#Public'];
 
     /**
-     * @var array
+     * @var string[]
      */
     protected $cc = [];
 }
diff --git a/app/Libraries/ActivityPub/Entities/Note.php b/app/Libraries/ActivityPub/Entities/Note.php
index 9d68ef6cf4..484fc3dddb 100644
--- a/app/Libraries/ActivityPub/Entities/Note.php
+++ b/app/Libraries/ActivityPub/Entities/Note.php
@@ -243,7 +243,7 @@ class Note extends UuidEntity
         return $this->reblog_of_note;
     }
 
-    public function setMessage(string $message): self
+    public function setMessage(string $message): static
     {
         helper('activitypub');
 
diff --git a/app/Libraries/ActivityPub/Filters/ActivityPubFilter.php b/app/Libraries/ActivityPub/Filters/ActivityPubFilter.php
index 325035432e..7355941722 100644
--- a/app/Libraries/ActivityPub/Filters/ActivityPubFilter.php
+++ b/app/Libraries/ActivityPub/Filters/ActivityPubFilter.php
@@ -23,7 +23,7 @@ class ActivityPubFilter implements FilterInterface
      * sent back to the client, allowing for error pages,
      * redirects, etc.
      *
-     * @param array|null                         $params
+     * @param string[]|null                         $params
      * @return void|mixed
      */
     public function before(RequestInterface $request, $params = null)
@@ -67,7 +67,7 @@ class ActivityPubFilter implements FilterInterface
             try {
                 // securityCheck: check activity signature before handling it
                 (new HttpSignature())->verify();
-            } catch (Exception $exception) {
+            } catch (Exception) {
                 // Invalid HttpSignature (401 = unauthorized)
                 // TODO: show error message?
                 return service('response')->setStatusCode(401);
@@ -82,7 +82,7 @@ class ActivityPubFilter implements FilterInterface
      * to stop execution of other after filters, short of
      * throwing an Exception or Error.
      *
-     * @param array|null                          $arguments
+     * @param string[]|null                          $arguments
      */
     public function after(
         RequestInterface $request,
diff --git a/app/Libraries/ActivityPub/Helpers/activitypub_helper.php b/app/Libraries/ActivityPub/Helpers/activitypub_helper.php
index ed7931ca02..53df425dfc 100644
--- a/app/Libraries/ActivityPub/Helpers/activitypub_helper.php
+++ b/app/Libraries/ActivityPub/Helpers/activitypub_helper.php
@@ -18,8 +18,6 @@ use CodeIgniter\HTTP\Exceptions\HTTPException;
 if (!function_exists('get_webfinger_data')) {
     /**
      * Retrieve actor webfinger data from username and domain
-     *
-     * @return object|null
      */
     function get_webfinger_data(string $username, string $domain): ?object
     {
@@ -45,8 +43,7 @@ if (!function_exists('split_handle')) {
     /**
      * Splits handle into its parts (username, host and port)
      *
-     * @param  string $handle
-     * @return bool|array
+     * @return array<string, string>|false
      */
     function split_handle(string $handle)
     {
@@ -107,7 +104,7 @@ if (!function_exists('accept_follow')) {
             );
             $acceptRequest->sign($actor->public_key_id, $actor->private_key);
             $acceptRequest->post();
-        } catch (Exception $exception) {
+        } catch (Exception) {
             $db->transRollback();
         }
 
@@ -163,8 +160,6 @@ if (!function_exists('extract_urls_from_message')) {
 if (!function_exists('create_preview_card_from_url')) {
     /**
      * Extract open graph metadata from given url and create preview card
-     *
-     * @return PreviewCard|null
      */
     function create_preview_card_from_url(URI $url): ?PreviewCard
     {
@@ -223,8 +218,6 @@ if (!function_exists('create_preview_card_from_url')) {
 if (!function_exists('get_or_create_preview_card_from_url')) {
     /**
      * Extract open graph metadata from given url and create preview card
-     *
-     * @return PreviewCard|null
      */
     function get_or_create_preview_card_from_url(URI $url): ?PreviewCard
     {
@@ -246,8 +239,6 @@ if (!function_exists('get_or_create_actor_from_uri')) {
     /**
      * Retrieves actor from database using the actor uri
      * If Actor is not present, it creates the record in the database and returns it.
-     *
-     * @return Actor|null
      */
     function get_or_create_actor_from_uri(string $actorUri): ?Actor
     {
@@ -265,8 +256,6 @@ if (!function_exists('get_or_create_actor')) {
     /**
      * Retrieves actor from database using the actor username and domain
      * If actor is not present, it creates the record in the database and returns it.
-     *
-     * @return Actor|null
      */
     function get_or_create_actor(string $username, string $domain): ?Actor
     {
@@ -292,8 +281,6 @@ if (!function_exists('create_actor_from_uri')) {
     /**
      * Creates actor record in database using
      * the info gathered from the actorUri parameter
-     *
-     * @return Actor|null
      */
     function create_actor_from_uri(string $actorUri): ?Actor
     {
@@ -352,8 +339,6 @@ if (!function_exists('get_current_domain')) {
 if (!function_exists('extract_text_from_html')) {
     /**
      * Extracts the text from html content
-     *
-     * @return string|null
      */
     function extract_text_from_html(string $content): ?string
     {
@@ -381,7 +366,7 @@ if (!function_exists('linkify')) {
                 case 'https':
                     $text = preg_replace_callback(
                         '~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i',
-                        function ($match) use ($protocol, &$links) {
+                        function (array $match) use ($protocol, &$links) {
                             if ($match[1]) {
                                 $protocol = $match[1];
                             }
@@ -452,7 +437,7 @@ if (!function_exists('linkify')) {
                                             ]),
                                         ) .
                                         '>';
-                                } catch (\CodeIgniter\HTTP\Exceptions\HTTPException $httpException) {
+                                } catch (\CodeIgniter\HTTP\Exceptions\HTTPException) {
                                     // Couldn't retrieve actor, do not wrap the text in link
                                     return '<' .
                                         array_push($links, $match[0]) .
@@ -485,7 +470,7 @@ if (!function_exists('linkify')) {
                         '~' .
                             preg_quote($protocol, '~') .
                             '://([^\s<]+?)(?<![\.,:])~i',
-                        function ($match) use ($protocol, &$links) {
+                        function (array $match) use ($protocol, &$links) {
                             return '<' .
                                 array_push(
                                     $links,
diff --git a/app/Libraries/ActivityPub/HttpSignature.php b/app/Libraries/ActivityPub/HttpSignature.php
index 30d301960d..f62e2fe690 100644
--- a/app/Libraries/ActivityPub/HttpSignature.php
+++ b/app/Libraries/ActivityPub/HttpSignature.php
@@ -41,7 +41,7 @@ class HttpSignature
     /**
      * @var IncomingRequest
      */
-    protected $request;
+    protected ?IncomingRequest $request;
 
     public function __construct(IncomingRequest $request = null)
     {
@@ -130,9 +130,9 @@ class HttpSignature
     /**
      * Split HTTP signature into its parts (keyId, headers and signature)
      *
-     * @return bool|mixed
+     * @return array<string, string>|false
      */
-    private function splitSignature(string $signature)
+    private function splitSignature(string $signature): array|false
     {
         if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) {
             // Signature pattern failed
@@ -150,7 +150,7 @@ class HttpSignature
     /**
      * Get plain text that has been originally signed
      *
-     * @param  array $headers HTTP header keys
+     * @param string[] $headers HTTP header keys
      */
     private function getPlainText(array $headers): string
     {
diff --git a/app/Libraries/ActivityPub/Models/ActivityModel.php b/app/Libraries/ActivityPub/Models/ActivityModel.php
index 388eddb52b..13d4114c38 100644
--- a/app/Libraries/ActivityPub/Models/ActivityModel.php
+++ b/app/Libraries/ActivityPub/Models/ActivityModel.php
@@ -10,6 +10,7 @@ namespace ActivityPub\Models;
 
 use ActivityPub\Entities\Activity;
 use CodeIgniter\Database\BaseResult;
+use CodeIgniter\Database\Exceptions\DataException;
 use CodeIgniter\I18n\Time;
 use DateTimeInterface;
 use Michalsn\Uuid\UuidModel;
@@ -59,7 +60,7 @@ class ActivityModel extends UuidModel
     protected $useTimestamps = true;
     protected $updatedField;
 
-    public function getActivityById($activityId)
+    public function getActivityById(string $activityId): ?Activity
     {
         $cacheName =
             config('ActivityPub')->cachePrefix . "activity#{$activityId}";
@@ -76,8 +77,6 @@ class ActivityModel extends UuidModel
      * Inserts a new activity record in the database
      *
      * @param Time $scheduledAt
-     *
-     * @return BaseResult|int|string|false
      */
     public function newActivity(
         string $type,
@@ -87,7 +86,7 @@ class ActivityModel extends UuidModel
         string $payload,
         DateTimeInterface $scheduledAt = null,
         ?string $status = null
-    ) {
+    ): BaseResult|int|string|false {
         return $this->insert(
             [
                 'actor_id' => $actorId,
@@ -102,7 +101,10 @@ class ActivityModel extends UuidModel
         );
     }
 
-    public function getScheduledActivities()
+    /**
+     * @return Activity[] 
+     */
+    public function getScheduledActivities(): array
     {
         return $this->where('`scheduled_at` <= NOW()', null, false)
             ->where('status', 'queued')
diff --git a/app/Libraries/ActivityPub/Models/ActorModel.php b/app/Libraries/ActivityPub/Models/ActorModel.php
index 795d5c8ffa..93871e108b 100644
--- a/app/Libraries/ActivityPub/Models/ActorModel.php
+++ b/app/Libraries/ActivityPub/Models/ActorModel.php
@@ -58,7 +58,7 @@ class ActorModel extends Model
      */
     protected $useTimestamps = true;
 
-    public function getActorById($id): Actor
+    public function getActorById(int $id): Actor
     {
         $cacheName = config('ActivityPub')->cachePrefix . "actor#{$id}";
         if (!($found = cache($cacheName))) {
@@ -98,7 +98,7 @@ class ActorModel extends Model
         return $found;
     }
 
-    public function getActorByUri($actorUri)
+    public function getActorByUri(string $actorUri): ?Actor
     {
         $hashedActorUri = md5($actorUri);
         $cacheName =
@@ -112,7 +112,10 @@ class ActorModel extends Model
         return $found;
     }
 
-    public function getFollowers($actorId)
+    /**
+     * @return Actor[]
+     */
+    public function getFollowers(int $actorId): array
     {
         $cacheName =
             config('ActivityPub')->cachePrefix . "actor#{$actorId}_followers";
@@ -137,7 +140,7 @@ class ActorModel extends Model
      */
     public function isActorBlocked(string $actorUri): bool
     {
-        if ($actor = $this->getActorByUri($actorUri)) {
+        if (($actor = $this->getActorByUri($actorUri)) !== null) {
             return $actor->is_blocked;
         }
 
@@ -161,7 +164,7 @@ class ActorModel extends Model
         return $found;
     }
 
-    public function blockActor($actorId): void
+    public function blockActor(int $actorId): void
     {
         $prefix = config('ActivityPub')->cachePrefix;
         cache()->delete($prefix . 'blocked_actors');
@@ -172,7 +175,7 @@ class ActorModel extends Model
         $this->update($actorId, ['is_blocked' => 1]);
     }
 
-    public function unblockActor($actorId): void
+    public function unblockActor(int $actorId): void
     {
         $prefix = config('ActivityPub')->cachePrefix;
         cache()->delete($prefix . 'blocked_actors');
diff --git a/app/Libraries/ActivityPub/Models/BlockedDomainModel.php b/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
index 5f67279b07..2e2745a412 100644
--- a/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
+++ b/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
@@ -49,8 +49,10 @@ class BlockedDomainModel extends Model
 
     /**
      * Retrieves instance or podcast domain blocks depending on whether or not $podcastId param is set.
+     * 
+     * @return BlockedDomain[]
      */
-    public function getBlockedDomains()
+    public function getBlockedDomains(): array
     {
         $cacheName = config('ActivityPub')->cachePrefix . 'blocked_domains';
         if (!($found = cache($cacheName))) {
@@ -61,14 +63,14 @@ class BlockedDomainModel extends Model
         return $found;
     }
 
-    public function isDomainBlocked($domain)
+    public function isDomainBlocked(string $name): bool
     {
-        $hashedDomain = md5($domain);
+        $hashedDomainName = md5($name);
         $cacheName =
             config('ActivityPub')->cachePrefix .
-            "domain#{$hashedDomain}_isBlocked";
+            "domain#{$hashedDomainName}_isBlocked";
         if (!($found = cache($cacheName))) {
-            $found = (bool) $this->find($domain);
+            $found = (bool) $this->find($name);
 
             cache()->save($cacheName, $found, DECADE);
         }
@@ -76,7 +78,7 @@ class BlockedDomainModel extends Model
         return $found;
     }
 
-    public function blockDomain($name)
+    public function blockDomain(string $name): int|bool
     {
         $hashedDomain = md5($name);
         $prefix = config('ActivityPub')->cachePrefix;
@@ -104,10 +106,7 @@ class BlockedDomainModel extends Model
         return $result;
     }
 
-    /**
-     * @return bool|BaseResult
-     */
-    public function unblockDomain($name)
+    public function unblockDomain(string $name): BaseResult|bool
     {
         $hashedDomain = md5($name);
         $prefix = config('ActivityPub')->cachePrefix;
diff --git a/app/Libraries/ActivityPub/Models/FavouriteModel.php b/app/Libraries/ActivityPub/Models/FavouriteModel.php
index b61fba3915..c6480a1352 100644
--- a/app/Libraries/ActivityPub/Models/FavouriteModel.php
+++ b/app/Libraries/ActivityPub/Models/FavouriteModel.php
@@ -109,9 +109,9 @@ class FavouriteModel extends UuidModel
     }
 
     public function removeFavourite(
-        $actor,
-        $note,
-        $registerActivity = true
+        Actor $actor,
+        Note $note,
+        bool $registerActivity = true
     ): void {
         $this->db->transStart();
 
diff --git a/app/Libraries/ActivityPub/Models/FollowModel.php b/app/Libraries/ActivityPub/Models/FollowModel.php
index 4bd0031bf0..09ec65577f 100644
--- a/app/Libraries/ActivityPub/Models/FollowModel.php
+++ b/app/Libraries/ActivityPub/Models/FollowModel.php
@@ -103,7 +103,7 @@ class FollowModel extends Model
             }
 
             $this->db->transComplete();
-        } catch (Exception $exception) {
+        } catch (Exception) {
             // follow already exists, do nothing
         }
     }
@@ -117,7 +117,7 @@ class FollowModel extends Model
     public function removeFollower(
         Actor $actor,
         Actor $targetActor,
-        $registerActivity = true
+        bool $registerActivity = true
     ): void {
         $this->db->transStart();
 
diff --git a/app/Libraries/ActivityPub/Models/NoteModel.php b/app/Libraries/ActivityPub/Models/NoteModel.php
index 2a5e0da617..01c0988482 100644
--- a/app/Libraries/ActivityPub/Models/NoteModel.php
+++ b/app/Libraries/ActivityPub/Models/NoteModel.php
@@ -21,6 +21,8 @@ use CodeIgniter\Database\BaseResult;
 use CodeIgniter\Events\Events;
 use CodeIgniter\HTTP\URI;
 use CodeIgniter\I18n\Time;
+use CodeIgniter\Router\Exceptions\RouterException;
+use InvalidArgumentException;
 use Michalsn\Uuid\UuidModel;
 
 class NoteModel extends UuidModel
@@ -86,7 +88,7 @@ class NoteModel extends UuidModel
      */
     protected $beforeInsert = ['setNoteId'];
 
-    public function getNoteById($noteId)
+    public function getNoteById(string $noteId): ?Note
     {
         $cacheName = config('ActivityPub')->cachePrefix . "note#{$noteId}";
         if (!($found = cache($cacheName))) {
@@ -98,7 +100,7 @@ class NoteModel extends UuidModel
         return $found;
     }
 
-    public function getNoteByUri($noteUri)
+    public function getNoteByUri(string $noteUri): ?Note
     {
         $hashedNoteUri = md5($noteUri);
         $cacheName =
@@ -117,7 +119,7 @@ class NoteModel extends UuidModel
      *
      * @return Note[]
      */
-    public function getActorPublishedNotes($actorId): array
+    public function getActorPublishedNotes(int $actorId): array
     {
         $cacheName =
             config('ActivityPub')->cachePrefix .
@@ -179,8 +181,10 @@ class NoteModel extends UuidModel
 
     /**
      * Retrieves all published reblogs for a given note
+     * 
+     * @return Note[]
      */
-    public function getNoteReblogs($noteId)
+    public function getNoteReblogs(string $noteId): array
     {
         $cacheName =
             config('ActivityPub')->cachePrefix . "note#{$noteId}_reblogs";
@@ -200,10 +204,7 @@ class NoteModel extends UuidModel
         return $found;
     }
 
-    /**
-     * @return bool|Query
-     */
-    public function addPreviewCard($noteId, $previewCardId)
+    public function addPreviewCard(string $noteId, int $previewCardId): Query|bool
     {
         return $this->db->table('activitypub_notes_preview_cards')->insert([
             'note_id' => $this->uuid->fromString($noteId)->getBytes(),
@@ -220,7 +221,7 @@ class NoteModel extends UuidModel
         Note $note,
         bool $createPreviewCard = true,
         bool $registerActivity = true
-    ) {
+    ): string|false {
         helper('activitypub');
 
         $this->db->transStart();
@@ -301,7 +302,7 @@ class NoteModel extends UuidModel
         return $newNoteId;
     }
 
-    public function editNote($updatedNote): bool
+    public function editNote(Note $updatedNote): bool
     {
         $this->db->transStart();
 
@@ -341,10 +342,8 @@ class NoteModel extends UuidModel
 
     /**
      * Removes a note from the database and decrements meta data
-     *
-     * @return BaseResult|bool
      */
-    public function removeNote(Note $note, bool $registerActivity = true)
+    public function removeNote(Note $note, bool $registerActivity = true): BaseResult|bool
     {
         $this->db->transStart();
 
@@ -450,14 +449,11 @@ class NoteModel extends UuidModel
         return $result;
     }
 
-    /**
-     * @return string|bool
-     */
     public function addReply(
-        $reply,
-        $createPreviewCard = true,
-        $registerActivity = true
-    ) {
+        Note $reply,
+        bool $createPreviewCard = true,
+        bool $registerActivity = true
+    ): string|false {
         if (!$reply->in_reply_to_id) {
             throw new Exception('Passed note is not a reply!');
         }
@@ -489,10 +485,7 @@ class NoteModel extends UuidModel
         return $noteId;
     }
 
-    /**
-     * @return BaseResult|int|string|false
-     */
-    public function reblog(Actor $actor, Note $note, $registerActivity = true)
+    public function reblog(Actor $actor, Note $note, bool $registerActivity = true): string|false
     {
         $this->db->transStart();
 
@@ -503,7 +496,7 @@ class NoteModel extends UuidModel
         ]);
 
         // add reblog
-        $reblogId = $this->insert($reblog, true);
+        $reblogId = $this->insert($reblog);
 
         model('ActorModel')
             ->where('id', $actor->id)
@@ -554,10 +547,7 @@ class NoteModel extends UuidModel
         return $reblogId;
     }
 
-    /**
-     * @return BaseResult|bool
-     */
-    public function undoReblog(Note $reblogNote, bool $registerActivity = true)
+    public function undoReblog(Note $reblogNote, bool $registerActivity = true): BaseResult|bool
     {
         $this->db->transStart();
 
@@ -649,7 +639,7 @@ class NoteModel extends UuidModel
         return $result;
     }
 
-    public function toggleReblog($actor, $note): void
+    public function toggleReblog(Actor $actor, Note $note): void
     {
         if (
             !($reblogNote = $this->where([
@@ -665,7 +655,11 @@ class NoteModel extends UuidModel
         }
     }
 
-    protected function setNoteId($data)
+    /** 
+     * @param array<string, array<string|int, mixed>> $data
+     * @return array<string, array<string|int, mixed>>
+     */
+    protected function setNoteId(array $data): array
     {
         $uuid4 = $this->uuid->{$this->uuidVersion}();
         $data['data']['id'] = $uuid4->toString();
diff --git a/app/Libraries/ActivityPub/Models/PreviewCardModel.php b/app/Libraries/ActivityPub/Models/PreviewCardModel.php
index 5580e7bb94..b6ae826837 100644
--- a/app/Libraries/ActivityPub/Models/PreviewCardModel.php
+++ b/app/Libraries/ActivityPub/Models/PreviewCardModel.php
@@ -51,7 +51,7 @@ class PreviewCardModel extends Model
      */
     protected $useTimestamps = true;
 
-    public function getPreviewCardFromUrl($url)
+    public function getPreviewCardFromUrl(string $url): ?PreviewCard
     {
         $hashedPreviewCardUrl = md5($url);
         $cacheName =
@@ -65,7 +65,7 @@ class PreviewCardModel extends Model
         return $found;
     }
 
-    public function getNotePreviewCard($noteId)
+    public function getNotePreviewCard(string $noteId): ?PreviewCard
     {
         $cacheName =
             config('ActivityPub')->cachePrefix . "note#{$noteId}_preview_card";
@@ -89,10 +89,7 @@ class PreviewCardModel extends Model
         return $found;
     }
 
-    /**
-     * @return bool|BaseResult
-     */
-    public function deletePreviewCard($id, $url)
+    public function deletePreviewCard(int $id, string $url): BaseResult|bool
     {
         $hashedPreviewCardUrl = md5($url);
         cache()->delete(
diff --git a/app/Libraries/ActivityPub/Objects/ActorObject.php b/app/Libraries/ActivityPub/Objects/ActorObject.php
index 766f09460f..20cc9cb83d 100644
--- a/app/Libraries/ActivityPub/Objects/ActorObject.php
+++ b/app/Libraries/ActivityPub/Objects/ActorObject.php
@@ -14,7 +14,7 @@ use ActivityPub\Core\ObjectType;
 class ActorObject extends ObjectType
 {
     /**
-     * @var array|string
+     * @var string|string[]
      */
     protected $context = [
         'https://www.w3.org/ns/activitystreams',
@@ -62,12 +62,12 @@ class ActorObject extends ObjectType
     protected $url;
 
     /**
-     * @var array|null
+     * @var array<string, string>|null
      */
     protected $image;
 
     /**
-     * @var array
+     * @var array<string, string>
      */
     protected $icon = [];
 
diff --git a/app/Libraries/ActivityPub/Objects/NoteObject.php b/app/Libraries/ActivityPub/Objects/NoteObject.php
index 31d7556d54..2a43392770 100644
--- a/app/Libraries/ActivityPub/Objects/NoteObject.php
+++ b/app/Libraries/ActivityPub/Objects/NoteObject.php
@@ -38,10 +38,7 @@ class NoteObject extends ObjectType
      */
     protected $replies;
 
-    /**
-     * @param Note $note
-     */
-    public function __construct($note)
+    public function __construct(Note $note)
     {
         $this->id = $note->uri;
 
diff --git a/app/Libraries/ActivityPub/Objects/OrderedCollectionObject.php b/app/Libraries/ActivityPub/Objects/OrderedCollectionObject.php
index 74e5f2d590..e407965a7e 100644
--- a/app/Libraries/ActivityPub/Objects/OrderedCollectionObject.php
+++ b/app/Libraries/ActivityPub/Objects/OrderedCollectionObject.php
@@ -42,15 +42,10 @@ class OrderedCollectionObject extends ObjectType
     protected $last;
 
     /**
-     * @var array|null
-     */
-    protected $orderedItems;
-
-    /**
-     * @param array $orderedItems
+     * @param ObjectType[] $orderedItems
      */
     public function __construct(
-        ?array $orderedItems = null,
+        protected ?array $orderedItems = null,
         ?Pager $pager = null
     ) {
         $this->id = current_url();
@@ -65,7 +60,5 @@ class OrderedCollectionObject extends ObjectType
                 $this->last = $pager->getPageURI($pager->getLastPage());
             }
         }
-
-        $this->orderedItems = $orderedItems;
     }
 }
diff --git a/app/Libraries/ActivityPub/WebFinger.php b/app/Libraries/ActivityPub/WebFinger.php
index 85edf401a7..09113f9289 100644
--- a/app/Libraries/ActivityPub/WebFinger.php
+++ b/app/Libraries/ActivityPub/WebFinger.php
@@ -38,29 +38,19 @@ class WebFinger
     protected $port;
 
     /**
-     * @var string
-     */
-    protected $subject;
-
-    /**
-     * @var array
+     * @var string[]
      */
     protected $aliases = [];
 
     /**
-     * @var array
+     * @var array<array<string, string>>
      */
     protected $links = [];
 
-    /**
-     * @param string $resource
-     */
-    public function __construct($resource)
+    public function __construct(protected string $subject)
     {
-        $this->subject = $resource;
-
         // Split resource into its parts (username, domain)
-        $parts = $this->splitResource($resource);
+        $parts = $this->splitResource($subject);
         if (!$parts) {
             throw new Exception('Wrong WebFinger resource pattern.');
         }
@@ -120,9 +110,9 @@ class WebFinger
     /**
      * Split resource into its parts (username, domain)
      *
-     * @return bool|mixed
+     * @return array<string, string>|false
      */
-    private function splitResource(string $resource)
+    private function splitResource(string $resource): array|false
     {
         if (!preg_match(self::RESOURCE_PATTERN, $resource, $matches)) {
             // Resource pattern failed
diff --git a/app/Libraries/Analytics/AnalyticsTrait.php b/app/Libraries/Analytics/AnalyticsTrait.php
index 4f9a374c5b..0e05b9ed1a 100644
--- a/app/Libraries/Analytics/AnalyticsTrait.php
+++ b/app/Libraries/Analytics/AnalyticsTrait.php
@@ -10,6 +10,7 @@ namespace Analytics;
 
 use Config\Services;
 use Config\Database;
+
 trait AnalyticsTrait
 {
     protected function registerPodcastWebpageHit(int $podcastId): void
diff --git a/app/Libraries/Analytics/Config/Analytics.php b/app/Libraries/Analytics/Config/Analytics.php
index 1ab066edd5..7b00ebb4f3 100644
--- a/app/Libraries/Analytics/Config/Analytics.php
+++ b/app/Libraries/Analytics/Config/Analytics.php
@@ -31,7 +31,7 @@ class Analytics extends BaseConfig
      *
      * @param string|string[] $audioFilePath
      */
-    public function getAudioFileUrl($audioFilePath): string
+    public function getAudioFileUrl(string|array $audioFilePath): string
     {
         return base_url($audioFilePath);
     }
diff --git a/app/Libraries/Analytics/Controllers/AnalyticsController.php b/app/Libraries/Analytics/Controllers/AnalyticsController.php
index 139892b332..9f0ea4ace1 100644
--- a/app/Libraries/Analytics/Controllers/AnalyticsController.php
+++ b/app/Libraries/Analytics/Controllers/AnalyticsController.php
@@ -24,7 +24,7 @@ class AnalyticsController extends Controller
      */
     protected $methodName;
 
-    public function _remap($method, ...$params)
+    public function _remap(string $method, string ...$params): mixed
     {
         if (!isset($params[1])) {
             throw PageNotFoundException::forPageNotFound();
@@ -39,11 +39,11 @@ class AnalyticsController extends Controller
         );
     }
 
-    public function getData($podcastId, $episodeId): ResponseInterface
+    public function getData(int $podcastId, int $episodeId): ResponseInterface
     {
         $analytics_model = new $this->className();
         $methodName = $this->methodName;
-        if ($episodeId) {
+        if ($episodeId !== 0) {
             return $this->response->setJSON(
                 $analytics_model->$methodName($podcastId, $episodeId),
             );
diff --git a/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php b/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
index a8dfd1a751..5c02415e5e 100644
--- a/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
+++ b/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
@@ -23,7 +23,7 @@ class EpisodeAnalyticsController extends Controller
      * class instantiation. These helpers will be available
      * to all other controllers that extend Analytics.
      *
-     * @var array
+     * @var string[]
      */
     protected $helpers = ['analytics'];
 
diff --git a/app/Libraries/Analytics/Controllers/EpisodeController.php b/app/Libraries/Analytics/Controllers/EpisodeController.php
new file mode 100644
index 0000000000..ec531e16c0
--- /dev/null
+++ b/app/Libraries/Analytics/Controllers/EpisodeController.php
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers;
+
+use CodeIgniter\HTTP\ResponseInterface;
+use Config\Services;
+use Analytics\AnalyticsTrait;
+use App\Entities\Episode;
+use App\Entities\Podcast;
+use App\Models\EpisodeModel;
+use App\Models\PodcastModel;
+use CodeIgniter\Exceptions\PageNotFoundException;
+use SimpleXMLElement;
+
+class EpisodeController extends BaseController
+{
+    use AnalyticsTrait;
+
+    /**
+     * @var Podcast
+     */
+    protected $podcast;
+
+    /**
+     * @var Episode
+     */
+    protected $episode;
+
+    public function _remap(string $method, string ...$params): mixed
+    {
+        if (count($params) < 2) {
+            throw PageNotFoundException::forPageNotFound();
+        }
+
+        if (
+            ($this->podcast = (new PodcastModel())->getPodcastByName(
+                $params[0],
+            )) === null
+        ) {
+            throw PageNotFoundException::forPageNotFound();
+        }
+
+        if (
+            ($this->episode = (new EpisodeModel())->getEpisodeBySlug(
+                $this->podcast->id,
+                $params[1],
+            )) !== null
+        ) {
+            unset($params[1]);
+            unset($params[0]);
+            return $this->$method(...$params);
+        }
+
+        throw PageNotFoundException::forPageNotFound();
+    }
+
+    public function index(): string
+    {
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->episode->podcast_id);
+        }
+
+        $locale = service('request')->getLocale();
+        $cacheName =
+            "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
+            (can_user_interact() ? '_authenticated' : '');
+
+        if (!($cachedView = cache($cacheName))) {
+            $data = [
+                'podcast' => $this->podcast,
+                'episode' => $this->episode,
+            ];
+
+            $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
+                $this->podcast->id,
+            );
+
+            if (can_user_interact()) {
+                helper('form');
+                return view('podcast/episode_authenticated', $data);
+            }
+            // The page cache is set to a decade so it is deleted manually upon podcast update
+            return view('podcast/episode', $data, [
+                'cache' => $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+                'cache_name' => $cacheName,
+            ]);
+        }
+
+        return $cachedView;
+    }
+
+    public function embeddablePlayer(
+        string $theme = 'light-transparent'
+    ): string {
+        header('Content-Security-Policy: frame-ancestors https://* http://*');
+
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->episode->podcast_id);
+        }
+
+        $session = Services::session();
+        $session->start();
+        if (isset($_SERVER['HTTP_REFERER'])) {
+            $session->set(
+                'embeddable_player_domain',
+                parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST),
+            );
+        }
+
+        $locale = service('request')->getLocale();
+
+        $cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
+
+        if (!($cachedView = cache($cacheName))) {
+            $theme = EpisodeModel::$themes[$theme];
+
+            $data = [
+                'podcast' => $this->podcast,
+                'episode' => $this->episode,
+                'theme' => $theme,
+            ];
+
+            $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
+                $this->podcast->id,
+            );
+
+            // The page cache is set to a decade so it is deleted manually upon podcast update
+            return view('embeddable_player', $data, [
+                'cache' => $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+                'cache_name' => $cacheName,
+            ]);
+        }
+
+        return $cachedView;
+    }
+
+    public function oembedJSON(): ResponseInterface
+    {
+        return $this->response->setJSON([
+            'type' => 'rich',
+            'version' => '1.0',
+            'title' => $this->episode->title,
+            'provider_name' => $this->podcast->title,
+            'provider_url' => $this->podcast->link,
+            'author_name' => $this->podcast->title,
+            'author_url' => $this->podcast->link,
+            'html' =>
+                '<iframe src="' .
+                $this->episode->embeddable_player_url .
+                '" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
+            'width' => 600,
+            'height' => 200,
+            'thumbnail_url' => $this->episode->image->large_url,
+            'thumbnail_width' => config('Images')->largeSize,
+            'thumbnail_height' => config('Images')->largeSize,
+        ]);
+    }
+
+    public function oembedXML(): ResponseInterface
+    {
+        $oembed = new SimpleXMLElement(
+            "<?xml version='1.0' encoding='utf-8' standalone='yes'?><oembed></oembed>",
+        );
+
+        $oembed->addChild('type', 'rich');
+        $oembed->addChild('version', '1.0');
+        $oembed->addChild('title', $this->episode->title);
+        $oembed->addChild('provider_name', $this->podcast->title);
+        $oembed->addChild('provider_url', $this->podcast->link);
+        $oembed->addChild('author_name', $this->podcast->title);
+        $oembed->addChild('author_url', $this->podcast->link);
+        $oembed->addChild('thumbnail', $this->episode->image->large_url);
+        $oembed->addChild('thumbnail_width', config('Images')->largeSize);
+        $oembed->addChild('thumbnail_height', config('Images')->largeSize);
+        $oembed->addChild(
+            'html',
+            htmlentities(
+                '<iframe src="' .
+                    $this->episode->embeddable_player_url .
+                    '" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
+            ),
+        );
+        $oembed->addChild('width', '600');
+        $oembed->addChild('height', '200');
+
+        return $this->response->setXML($oembed);
+    }
+}
diff --git a/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php b/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
index 7d5549a026..5c9e424278 100644
--- a/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
+++ b/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
@@ -13,9 +13,9 @@ use CodeIgniter\Controller;
 
 class UnknownUserAgentsController extends Controller
 {
-    public function index($lastKnownId = 0): ResponseInterface
+    public function index(int $lastKnownId = 0): ResponseInterface
     {
-        $model = model('UnknownUserAgentsModel');
+        $model = model('AnalyticsUnknownUserAgentsModel');
 
         return $this->response->setJSON($model->getUserAgents($lastKnownId));
     }
diff --git a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
index 6744b9ac82..ae231232ce 100644
--- a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
@@ -37,7 +37,7 @@ class AnalyticsPodcastsByCountry extends Entity
         'hits' => 'integer',
     ];
 
-    public function getLabels()
+    public function getLabels(): string
     {
         return lang('Countries.' . $this->attributes['labels']);
     }
diff --git a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
index dad55121ec..b1a56e6f75 100644
--- a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
@@ -42,7 +42,7 @@ class AnalyticsPodcastsByRegion extends Entity
         'hits' => 'integer',
     ];
 
-    public function getCountryCode()
+    public function getCountryCode(): string
     {
         return lang('Countries.' . $this->attributes['country_code']);
     }
diff --git a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
index 2022f28391..82218943e4 100644
--- a/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
@@ -44,7 +44,7 @@ class AnalyticsPodcastsByService extends Entity
         'hits' => 'integer',
     ];
 
-    public function getLabels()
+    public function getLabels(): string
     {
         return UserAgentsRSS::getName($this->attributes['labels']) ??
             $this->attributes['labels'];
diff --git a/app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php b/app/Libraries/Analytics/Entities/AnalyticsUnknownUserAgent.php
similarity index 74%
rename from app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php
rename to app/Libraries/Analytics/Entities/AnalyticsUnknownUserAgent.php
index b442849107..7cf4f45534 100644
--- a/app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsUnknownUserAgent.php
@@ -13,17 +13,24 @@ namespace Analytics\Entities;
 use CodeIgniter\Entity\Entity;
 
 /**
+ * @property int $id
  * @property int $useragent
  * @property int $hits
  * @property Time $created_at
  * @property Time $updated_at
  */
-class AnalyticsUnknownUseragents extends Entity
+class AnalyticsUnknownUserAgent extends Entity
 {
+    /**
+     * @var string[]
+     */
+    protected $dates = ['created_at', 'updated_at'];
+
     /**
      * @var array<string, string>
      */
     protected $casts = [
+        'id' => 'integer',
         'useragent' => 'integer',
         'hits' => 'integer',
     ];
diff --git a/app/Libraries/Analytics/Helpers/analytics_helper.php b/app/Libraries/Analytics/Helpers/analytics_helper.php
index 364eab1ce5..ed2cec5f8a 100644
--- a/app/Libraries/Analytics/Helpers/analytics_helper.php
+++ b/app/Libraries/Analytics/Helpers/analytics_helper.php
@@ -1,5 +1,6 @@
 <?php
 
+use CodeIgniter\I18n\Time;
 use Config\Services;
 use Podlibre\Ipcat\IpDb;
 use GeoIp2\Database\Reader;
@@ -18,7 +19,7 @@ if (!function_exists('base64_url_encode')) {
     /**
      * Encode Base64 for URLs
      */
-    function base64_url_encode($input)
+    function base64_url_encode(string $input): string
     {
         return strtr(base64_encode($input), '+/=', '._-');
     }
@@ -28,7 +29,7 @@ if (!function_exists('base64_url_decode')) {
     /**
      * Decode Base64 from URL
      */
-    function base64_url_decode($input)
+    function base64_url_decode(string $input): string
     {
         return base64_decode(strtr($input, '._-', '+/='));
     }
@@ -131,7 +132,7 @@ if (!function_exists('set_user_session_location')) {
                     'longitude' => round($city->location->longitude, 3),
                 ];
                 // If things go wrong the show must go on and the user must be able to download the file
-            } catch (Exception $exception) {
+            } catch (Exception) {
             }
             $session->set('location', $location);
         }
@@ -154,7 +155,7 @@ if (!function_exists('set_user_session_player')) {
             try {
                 $playerFound = UserAgents::find($userAgent);
                 // If things go wrong the show must go on and the user must be able to download the file
-            } catch (Exception $exception) {
+            } catch (Exception) {
             }
             if ($playerFound) {
                 $session->set('player', $playerFound);
@@ -176,7 +177,7 @@ if (!function_exists('set_user_session_player')) {
                         [$userAgent],
                     );
                     // If things go wrong the show must go on and the user must be able to download the file
-                } catch (Exception $exception) {
+                } catch (Exception) {
                 }
             }
         }
@@ -197,7 +198,7 @@ if (!function_exists('set_user_session_browser')) {
             try {
                 $whichbrowser = new Parser(getallheaders());
                 $browserName = $whichbrowser->browser->name;
-            } catch (Exception $exception) {
+            } catch (Exception) {
                 $browserName = '- Could not get browser name -';
             }
             if ($browserName == null) {
@@ -267,6 +268,8 @@ if (!function_exists('podcast_hit')) {
      * @param integer $episodeId The Episode ID
      * @param integer $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn)
      * @param integer $fileSize The podcast complete file size
+     * @param integer $duration The episode duration in seconds
+     * @param int $publicationTime The episode's publication time as a UNIX timestamp
      * @param string $serviceName The name of the service that had fetched the RSS feed
      */
     function podcast_hit(
@@ -274,8 +277,8 @@ if (!function_exists('podcast_hit')) {
         int $episodeId,
         int $bytesThreshold,
         int $fileSize,
-        $duration,
-        $publicationDate,
+        int $duration,
+        int $publicationTime,
         string $serviceName
     ): void {
         $session = Services::session();
@@ -341,7 +344,7 @@ if (!function_exists('podcast_hit')) {
                     $db = Database::connect();
                     $procedureName = $db->prefixTable('analytics_podcasts');
 
-                    $age = intdiv(time() - $publicationDate, 86400);
+                    $age = intdiv(time() - $publicationTime, 86400);
 
                     // We create a sha1 hash for this IP_Address+User_Agent+Podcast_ID (used to count unique listeners):
                     $listenerHashId =
diff --git a/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php b/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
index c76772f652..1261e35529 100644
--- a/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
@@ -10,24 +10,20 @@
 
 namespace Analytics\Models;
 
-use Analytics\Entities\AnalyticsUnknownUseragents;
+use Analytics\Entities\AnalyticsUnknownUserAgent;
 use CodeIgniter\Model;
 
-class AnalyticsUnknownUseragentsModel extends Model
+class AnalyticsUnknownUserAgentModel extends Model
 {
     /**
      * @var string
      */
     protected $table = 'analytics_unknown_useragents';
-    /**
-     * @var string
-     */
-    protected $primaryKey = 'id';
 
     /**
      * @var string
      */
-    protected $returnType = AnalyticsUnknownUseragents::class;
+    protected $returnType = AnalyticsUnknownUserAgent::class;
     /**
      * @var bool
      */
@@ -37,4 +33,12 @@ class AnalyticsUnknownUseragentsModel extends Model
      * @var bool
      */
     protected $useTimestamps = false;
+
+    /**
+     * @return mixed[]
+     */
+    public function getUserAgents(int $lastKnownId = 0): array
+    {
+        return $this->where('id >', $lastKnownId)->findAll();
+    }
 }
diff --git a/app/Libraries/Analytics/Models/UnknownUserAgentsModel.php b/app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
deleted file mode 100644
index b0173a5840..0000000000
--- a/app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * Class UnknownUserAgentsModel
- * Model for analytics_unknown_useragents table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace Analytics\Models;
-
-use CodeIgniter\Model;
-
-class UnknownUserAgentsModel extends Model
-{
-    /**
-     * @var string
-     */
-    protected $table = 'analytics_unknown_useragents';
-
-    public function getUserAgents($last_known_id = 0)
-    {
-        return $this->where('id >', $last_known_id)->findAll();
-    }
-}
diff --git a/app/Libraries/Breadcrumb.php b/app/Libraries/Breadcrumb.php
index e520b3987b..68d76bf072 100644
--- a/app/Libraries/Breadcrumb.php
+++ b/app/Libraries/Breadcrumb.php
@@ -15,11 +15,12 @@ class Breadcrumb
     /**
      * List of breadcrumb links.
      *
-     * @var array
      * $links = [
-     *  'text' => (string) the anchor text,
-     *  'href' => (string) the anchor href,
+     *  'text' => 'Example Link',
+     *  'href' => 'https://example.com/',
      * ]
+     *
+     * @var array<array<string, string>>
      */
     protected $links = [];
 
@@ -57,6 +58,8 @@ class Breadcrumb
      * replaceParams($newParams);
      *
      * The breadcrumb is now `Home / podcasts / foo / episodes / bar`
+     *
+     * @param string[] $newParams
      */
     public function replaceParams(array $newParams): void
     {
@@ -71,7 +74,7 @@ class Breadcrumb
     /**
      * Renders the breadcrumb object as an accessible html breadcrumb nav
      */
-    public function render($class = null): string
+    public function render(string $class = null): string
     {
         $listItems = '';
         $keys = array_keys($this->links);
diff --git a/app/Libraries/Negotiate.php b/app/Libraries/Negotiate.php
index 22827dc5ee..3c8bd8932f 100644
--- a/app/Libraries/Negotiate.php
+++ b/app/Libraries/Negotiate.php
@@ -6,6 +6,9 @@ use CodeIgniter\HTTP\Negotiate as CodeIgniterHTTPNegotiate;
 
 class Negotiate extends CodeIgniterHTTPNegotiate
 {
+    /**
+     * @param mixed[] $acceptable
+     */
     public function callMatch(
         array $acceptable,
         string $supported,
diff --git a/app/Libraries/Router.php b/app/Libraries/Router.php
index e9ac8b49f7..dc41fa9b2c 100644
--- a/app/Libraries/Router.php
+++ b/app/Libraries/Router.php
@@ -52,7 +52,7 @@ class Router extends CodeIgniterRouter
             $matchedKey = $key;
 
             // Are we dealing with a locale?
-            if (strpos($key, '{locale}') !== false) {
+            if (str_contains($key, '{locale}')) {
                 $localeSegment = array_search(
                     '{locale}',
                     preg_split(
@@ -167,9 +167,9 @@ class Router extends CodeIgniterRouter
                 // Support resource route when function with subdirectory
                 // ex: $routes->resource('Admin/Admins');
                 if (
-                    strpos($val, '$') !== false &&
-                    strpos($key, '(') !== false &&
-                    strpos($key, '/') !== false
+                    str_contains($val, '$') &&
+                    str_contains($key, '(') &&
+                    str_contains($key, '/')
                 ) {
                     $replacekey = str_replace('/(.*)', '', $key);
                     $val = preg_replace('#^' . $key . '$#u', $val, $uri);
@@ -179,11 +179,11 @@ class Router extends CodeIgniterRouter
                         $val,
                     );
                 } elseif (
-                    strpos($val, '$') !== false &&
-                    strpos($key, '(') !== false
+                    str_contains($val, '$') &&
+                    str_contains($key, '(')
                 ) {
                     $val = preg_replace('#^' . $key . '$#u', $val, $uri);
-                } elseif (strpos($val, '/') !== false) {
+                } elseif (str_contains($val, '/')) {
                     [$controller, $method] = explode('::', $val);
 
                     // Only replace slashes in the controller, not in the method.
diff --git a/app/Models/ActorModel.php b/app/Models/ActorModel.php
index 37a70b04be..b58b095578 100644
--- a/app/Models/ActorModel.php
+++ b/app/Models/ActorModel.php
@@ -13,5 +13,8 @@ use App\Entities\Actor;
 
 class ActorModel extends ActivityPubActorModel
 {
+    /**
+     * @var string
+     */
     protected $returnType = Actor::class;
 }
diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php
index 0bf5043626..d827fcd5f0 100644
--- a/app/Models/CategoryModel.php
+++ b/app/Models/CategoryModel.php
@@ -9,6 +9,7 @@
 namespace App\Models;
 
 use App\Entities\Category;
+use CodeIgniter\Database\Exceptions\DataException;
 use CodeIgniter\Model;
 
 class CategoryModel extends Model
@@ -46,12 +47,15 @@ class CategoryModel extends Model
      */
     protected $useTimestamps = false;
 
-    public function getCategoryById($id): ?Category
+    public function getCategoryById(int $id): ?Category
     {
         return $this->find($id);
     }
 
-    public function getCategoryOptions()
+    /**
+     * @return array<int, string>
+     */
+    public function getCategoryOptions(): array
     {
         $locale = service('request')->getLocale();
         $cacheName = "category_options_{$locale}";
@@ -61,7 +65,7 @@ class CategoryModel extends Model
 
             $options = array_reduce(
                 $categories,
-                function ($result, $category) {
+                function (array $result, Category $category): array {
                     $result[$category->id] = lang(
                         'Podcast.category_options.' . $category->code,
                     );
@@ -79,9 +83,11 @@ class CategoryModel extends Model
     /**
      * Sets categories for a given podcast
      *
-     * @return int|bool Number of rows inserted or FALSE on failure
+     * @param int[] $categories
+     * 
+     * @return int|false Number of rows inserted or FALSE on failure
      */
-    public function setPodcastCategories(int $podcastId, ?array $categories)
+    public function setPodcastCategories(int $podcastId, array $categories): int|false
     {
         cache()->delete("podcast#{$podcastId}_categories");
 
@@ -98,7 +104,7 @@ class CategoryModel extends Model
         // prepare data for `podcasts_categories` table
         $data = array_reduce(
             $categories,
-            function ($result, $categoryId) use ($podcastId) {
+            function (array $result, int $categoryId) use ($podcastId): array {
                 $result[] = [
                     'podcast_id' => $podcastId,
                     'category_id' => $categoryId,
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 7bf6614766..c39e741719 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -149,7 +149,7 @@ class EpisodeModel extends Model
     /**
      * @param int|string $podcastId may be the id or podcast name
      */
-    public function getEpisodeBySlug($podcastId, string $episodeSlug): ?Episode
+    public function getEpisodeBySlug(int|string $podcastId, string $episodeSlug): ?Episode
     {
         $cacheName = "podcast#{$podcastId}_episode-{$episodeSlug}";
         if (!($found = cache($cacheName))) {
@@ -175,7 +175,7 @@ class EpisodeModel extends Model
         return $found;
     }
 
-    public function getEpisodeById($episodeId)
+    public function getEpisodeById(int $episodeId): ?Episode
     {
         // TODO: episode id should be a composite key. The cache should include podcast_id.
         $cacheName = "podcast_episode#{$episodeId}";
@@ -192,7 +192,7 @@ class EpisodeModel extends Model
         return $found;
     }
 
-    public function getPublishedEpisodeById($podcastId, $episodeId)
+    public function getPublishedEpisodeById(int $podcastId, int $episodeId): ?Episode
     {
         $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_published";
         if (!($found = cache($cacheName))) {
@@ -278,7 +278,7 @@ class EpisodeModel extends Model
      *
      * @return int|bool seconds
      */
-    public function getSecondsToNextUnpublishedEpisode(int $podcastId)
+    public function getSecondsToNextUnpublishedEpisode(int $podcastId): int|bool
     {
         $result = $this->select(
             'TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff',
@@ -297,9 +297,11 @@ class EpisodeModel extends Model
     }
 
     /**
+     * @param mixed[] $data
+     * 
      * @return array<string, array<string|int, mixed>>
      */
-    public function clearCache($data): array
+    public function clearCache(array $data): array
     {
         $episode = (new EpisodeModel())->find(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
@@ -348,6 +350,8 @@ class EpisodeModel extends Model
     }
 
     /**
+     * @param mixed[] $data
+     * 
      * @return array<string, array<string|int, mixed>>
      */
     protected function writeEnclosureMetadata(array $data): array
diff --git a/app/Models/EpisodePersonModel.php b/app/Models/EpisodePersonModel.php
deleted file mode 100644
index bec23995de..0000000000
--- a/app/Models/EpisodePersonModel.php
+++ /dev/null
@@ -1,159 +0,0 @@
-<?php
-
-/**
- * @copyright  2021 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Database\BaseResult;
-use App\Entities\EpisodePerson;
-use CodeIgniter\Model;
-
-class EpisodePersonModel extends Model
-{
-    /**
-     * @var string
-     */
-    protected $table = 'episodes_persons';
-    /**
-     * @var string
-     */
-    protected $primaryKey = 'id';
-
-    /**
-     * @var string[]
-     */
-    protected $allowedFields = [
-        'id',
-        'podcast_id',
-        'episode_id',
-        'person_id',
-        'person_group',
-        'person_role',
-    ];
-
-    /**
-     * @var string
-     */
-    protected $returnType = EpisodePerson::class;
-    /**
-     * @var bool
-     */
-    protected $useSoftDeletes = false;
-
-    /**
-     * @var bool
-     */
-    protected $useTimestamps = false;
-
-    /**
-     * @var array<string, string>
-     */
-    protected $validationRules = [
-        'episode_id' => 'required',
-        'person_id' => 'required',
-    ];
-
-    /**
-     * @var string[]
-     */
-    protected $afterInsert = ['clearCache'];
-    /**
-     * @var string[]
-     */
-    protected $beforeDelete = ['clearCache'];
-
-    public function getEpisodePersons($podcastId, $episodeId)
-    {
-        $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
-        if (!($found = cache($cacheName))) {
-            $found = $this->select('episodes_persons.*')
-                ->where('episode_id', $episodeId)
-                ->join('persons', 'person_id=persons.id')
-                ->orderby('full_name')
-                ->findAll();
-
-            cache()->save($cacheName, $found, DECADE);
-        }
-        return $found;
-    }
-
-    /**
-     * Add persons to episode
-     *
-     * @return bool|int Number of rows inserted or FALSE on failure
-     */
-    public function addEpisodePersons(
-        int $podcastId,
-        int $episodeId,
-        array $persons,
-        array $groups_roles
-    ) {
-        if (!empty($persons)) {
-            $this->clearCache([
-                'episode_id' => $episodeId,
-            ]);
-
-            $data = [];
-            foreach ($persons as $person) {
-                if ($groups_roles !== []) {
-                    foreach ($groups_roles as $group_role) {
-                        $group_role = explode(',', $group_role);
-                        $data[] = [
-                            'podcast_id' => $podcastId,
-                            'episode_id' => $episodeId,
-                            'person_id' => $person,
-                            'person_group' => $group_role[0],
-                            'person_role' => $group_role[1],
-                        ];
-                    }
-                } else {
-                    $data[] = [
-                        'podcast_id' => $podcastId,
-                        'episode_id' => $episodeId,
-                        'person_id' => $person,
-                    ];
-                }
-            }
-            return $this->insertBatch($data);
-        }
-        return 0;
-    }
-
-    /**
-     * @return bool|BaseResult
-     */
-    public function removeEpisodePersons(
-        $podcastId,
-        $episodeId,
-        $episodePersonId
-    ) {
-        return $this->delete([
-            'podcast_id' => $podcastId,
-            'episode_id' => $episodeId,
-            'id' => $episodePersonId,
-        ]);
-    }
-
-    /**
-     * @return array<string, array<string|int, mixed>>
-     */
-    protected function clearCache(array $data): array
-    {
-        if (isset($data['episode_id'])) {
-            $episodeId = $data['episode_id'];
-        } else {
-            $person = (new EpisodePersonModel())->find(
-                is_array($data['id']) ? $data['id']['id'] : $data['id'],
-            );
-            $episodeId = $person->episode_id;
-        }
-
-        (new EpisodeModel())->clearCache(['id' => $episodeId]);
-
-        return $data;
-    }
-}
diff --git a/app/Models/LanguageModel.php b/app/Models/LanguageModel.php
index 09c52dfb92..b488ba981e 100644
--- a/app/Models/LanguageModel.php
+++ b/app/Models/LanguageModel.php
@@ -9,6 +9,7 @@
 namespace App\Models;
 
 use App\Entities\Language;
+use CodeIgniter\Database\Exceptions\DataException;
 use CodeIgniter\Model;
 
 class LanguageModel extends Model
@@ -41,14 +42,17 @@ class LanguageModel extends Model
      */
     protected $useTimestamps = false;
 
-    public function getLanguageOptions()
+    /**
+     * @return array<string, string>
+     */
+    public function getLanguageOptions(): array
     {
         if (!($options = cache('language_options'))) {
             $languages = $this->findAll();
 
             $options = array_reduce(
                 $languages,
-                function ($result, $language) {
+                function (array $result, Language $language): array {
                     $result[$language->code] = $language->native_name;
                     return $result;
                 },
diff --git a/app/Models/NoteModel.php b/app/Models/NoteModel.php
index 819df9a51b..4dbe62e300 100644
--- a/app/Models/NoteModel.php
+++ b/app/Models/NoteModel.php
@@ -8,12 +8,19 @@
 
 namespace App\Models;
 
+use App\Entities\Note;
 use ActivityPub\Models\NoteModel as ActivityPubNoteModel;
 
 class NoteModel extends ActivityPubNoteModel
 {
-    protected $returnType = \App\Entities\Note::class;
+    /**
+     * @var string
+     */
+    protected $returnType = Note::class;
 
+    /**
+     * @var string[]
+     */
     protected $allowedFields = [
         'id',
         'uri',
@@ -33,9 +40,9 @@ class NoteModel extends ActivityPubNoteModel
     /**
      * Retrieves all published notes for a given episode ordered by publication date
      *
-     * @return \App\Entities\Note[]
+     * @return Note[]
      */
-    public function getEpisodeNotes($episodeId)
+    public function getEpisodeNotes(int $episodeId): array
     {
         return $this->where([
             'episode_id' => $episodeId,
diff --git a/app/Models/PageModel.php b/app/Models/PageModel.php
index f52eaf9438..c11f6dfc6f 100644
--- a/app/Models/PageModel.php
+++ b/app/Models/PageModel.php
@@ -69,6 +69,8 @@ class PageModel extends Model
     protected $beforeDelete = ['clearCache'];
 
     /**
+     * @param mixed[] $data
+     *
      * @return array<string, array<string|int, mixed>>
      */
     protected function clearCache(array $data): array
diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php
index be18d6f5bd..ae861561d7 100644
--- a/app/Models/PersonModel.php
+++ b/app/Models/PersonModel.php
@@ -10,6 +10,7 @@ namespace App\Models;
 
 use App\Entities\Image;
 use App\Entities\Person;
+use CodeIgniter\Database\BaseResult;
 use CodeIgniter\Model;
 
 class PersonModel extends Model
@@ -80,7 +81,7 @@ class PersonModel extends Model
      */
     protected $beforeDelete = ['clearCache'];
 
-    public function getPersonById($personId)
+    public function getPersonById(int $personId): ?Person
     {
         $cacheName = "person#{$personId}";
         if (!($found = cache($cacheName))) {
@@ -92,13 +93,16 @@ class PersonModel extends Model
         return $found;
     }
 
-    public function getPerson($fullName)
+    public function getPerson(string $fullName): ?Person
     {
         return $this->where('full_name', $fullName)->first();
     }
 
-    public function createPerson($fullName, $informationUrl, $image)
-    {
+    public function addPerson(
+        string $fullName,
+        ?string $informationUrl,
+        string $image
+    ): int|bool {
         $person = new Person([
             'full_name' => $fullName,
             'unique_name' => slugify($fullName),
@@ -107,10 +111,200 @@ class PersonModel extends Model
             'created_by' => user_id(),
             'updated_by' => user_id(),
         ]);
+
         return $this->insert($person);
     }
 
-    public function getPersonOptions()
+    /**
+     * @return Person[]
+     */
+    public function getEpisodePersons(int $podcastId, int $episodeId): array
+    {
+        $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
+        if (!($found = cache($cacheName))) {
+            $found = $this->db
+                ->table('episodes_persons')
+                ->select('episodes_persons.*')
+                ->where('episode_id', $episodeId)
+                ->join('persons', 'person_id=persons.id')
+                ->orderby('full_name')
+                ->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    /**
+     * @return Person[]
+     */
+    public function getPodcastPersons(int $podcastId): array
+    {
+        $cacheName = "podcast#{$podcastId}_persons";
+        if (!($found = cache($cacheName))) {
+            $found = $this->db
+                ->table('podcasts_persons')
+                ->select('podcasts_persons.*')
+                ->where('podcast_id', $podcastId)
+                ->join('persons', 'person_id=persons.id')
+                ->orderby('full_name')
+                ->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    public function addEpisodePerson(
+        int $podcastId,
+        int $episodeId,
+        int $personId,
+        string $group,
+        string $role
+    ): int|bool {
+        return $this->db->table('episodes_persons')->insert([
+            'podcast_id' => $podcastId,
+            'episode_id' => $episodeId,
+            'person_id' => $personId,
+            'person_group' => $group,
+            'person_role' => $role,
+        ]);
+    }
+
+    public function addPodcastPerson(
+        int $podcastId,
+        int $personId,
+        string $group,
+        string $role
+    ): int|bool {
+        return $this->db->table('podcasts_persons')->insert([
+            'podcast_id' => $podcastId,
+            'person_id' => $personId,
+            'person_group' => $group,
+            'person_role' => $role,
+        ]);
+    }
+
+    /**
+     * Add persons to podcast
+     *
+     * @param array<string> $persons
+     * @param array<string, string> $groupsRoles
+     *
+     * @return bool|int Number of rows inserted or FALSE on failure
+     */
+    public function addPodcastPersons(
+        int $podcastId,
+        array $persons = [],
+        array $groupsRoles = []
+    ): int|bool {
+        if ($persons === []) {
+            return 0;
+        }
+
+        $this->clearCache(['podcast_id' => $podcastId]);
+        $data = [];
+        foreach ($persons as $person) {
+            if ($groupsRoles === []) {
+                $data[] = [
+                    'podcast_id' => $podcastId,
+                    'person_id' => $person,
+                ];
+            }
+
+            foreach ($groupsRoles as $group_role) {
+                $group_role = explode(',', $group_role);
+                $data[] = [
+                    'podcast_id' => $podcastId,
+                    'person_id' => $person,
+                    'person_group' => $group_role[0],
+                    'person_role' => $group_role[1],
+                ];
+            }
+        }
+
+        return $this->insertBatch($data);
+    }
+
+    /**
+     * Add persons to episode
+     *
+     * @return BaseResult|bool Number of rows inserted or FALSE on failure
+     */
+    public function removePodcastPersons(int $podcastId, int $personId): BaseResult|bool
+    {
+        return $this->delete([
+            'id' => $personId,
+            'podcast_id' => $podcastId,
+        ]);
+    }
+
+    /**
+     * Add persons to episode
+     *
+     * @param int[] $personIds
+     * @param string[] $groups_roles
+     * 
+     * @return bool|int Number of rows inserted or FALSE on failure
+     */
+    public function addEpisodePersons(
+        int $podcastId,
+        int $episodeId,
+        array $personIds,
+        array $groups_roles
+    ): bool|int {
+        if (!empty($personIds)) {
+            $this->clearCache([
+                'episode_id' => $episodeId,
+            ]);
+
+            $data = [];
+            foreach ($personIds as $personId) {
+                if ($groups_roles !== []) {
+                    foreach ($groups_roles as $group_role) {
+                        $group_role = explode(',', $group_role);
+                        $data[] = [
+                            'podcast_id' => $podcastId,
+                            'episode_id' => $episodeId,
+                            'person_id' => $personId,
+                            'person_group' => $group_role[0],
+                            'person_role' => $group_role[1],
+                        ];
+                    }
+                } else {
+                    $data[] = [
+                        'podcast_id' => $podcastId,
+                        'episode_id' => $episodeId,
+                        'person_id' => $personId,
+                    ];
+                }
+            }
+            return $this->insertBatch($data);
+        }
+        return 0;
+    }
+
+    /**
+     * @return BaseResult|bool
+     */
+    public function removeEpisodePersons(
+        int $podcastId,
+        int $episodeId,
+        int $personId
+    ): BaseResult|bool {
+        return $this->delete([
+            'podcast_id' => $podcastId,
+            'episode_id' => $episodeId,
+            'id' => $personId,
+        ]);
+    }
+
+    /**
+     * @return array<string, string> 
+     */
+    public function getPersonOptions(): array
     {
         $options = [];
 
@@ -131,7 +325,10 @@ class PersonModel extends Model
         return $options;
     }
 
-    public function getTaxonomyOptions()
+    /**
+     * @return array<string, string> 
+     */
+    public function getTaxonomyOptions(): array
     {
         $options = [];
         $locale = service('request')->getLocale();
@@ -156,6 +353,8 @@ class PersonModel extends Model
     }
 
     /**
+     * @param mixed[] $data
+     * 
      * @return array<string, array<string|int, mixed>>
      */
     protected function clearCache(array $data): array
diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php
index 9ce8e241dc..aba70b0d6c 100644
--- a/app/Models/PlatformModel.php
+++ b/app/Models/PlatformModel.php
@@ -12,6 +12,7 @@
 namespace App\Models;
 
 use App\Entities\Platform;
+use CodeIgniter\Database\Exceptions\DatabaseException;
 use CodeIgniter\Model;
 
 class PlatformModel extends Model
@@ -50,7 +51,10 @@ class PlatformModel extends Model
      */
     protected $useTimestamps = false;
 
-    public function getPlatforms()
+    /**
+     * @return Platform[]
+     */
+    public function getPlatforms(): array
     {
         if (!($found = cache('platforms'))) {
             $baseUrl = rtrim(config('app')->baseURL, '/');
@@ -62,7 +66,7 @@ class PlatformModel extends Model
         return $found;
     }
 
-    public function getPlatform($slug)
+    public function getPlatform(string $slug): ?Platform
     {
         $cacheName = "platform-{$slug}";
         if (!($found = cache($cacheName))) {
@@ -73,12 +77,12 @@ class PlatformModel extends Model
     }
 
     public function createPlatform(
-        $slug,
-        $type,
-        $label,
-        $homeUrl,
-        $submitUrl = null
-    ) {
+        string $slug,
+        string $type,
+        string $label,
+        string $homeUrl,
+        string $submitUrl = null
+    ): int|false {
         $data = [
             'slug' => $slug,
             'type' => $type,
@@ -86,10 +90,14 @@ class PlatformModel extends Model
             'home_url' => $homeUrl,
             'submit_url' => $submitUrl,
         ];
+
         return $this->insert($data, false);
     }
 
-    public function getPlatformsWithLinks($podcastId, $platformType)
+    /**
+     * @return Platform[] 
+     */
+    public function getPlatformsWithLinks(int $podcastId, string $platformType): array
     {
         if (
             !($found = cache(
@@ -117,7 +125,10 @@ class PlatformModel extends Model
         return $found;
     }
 
-    public function getPodcastPlatforms($podcastId, $platformType)
+    /**
+     * @return Platform[]
+     */
+    public function getPodcastPlatforms(int $podcastId, string $platformType): array
     {
         $cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
         if (!($found = cache($cacheName))) {
@@ -139,13 +150,13 @@ class PlatformModel extends Model
     }
 
     /**
-     * @return int|bool
+     * @param mixed[] $podcastsPlatformsData 
      */
     public function savePodcastPlatforms(
-        $podcastId,
-        $platformType,
-        $podcastsPlatformsData
-    ) {
+        int $podcastId,
+        string $platformType,
+        array $podcastsPlatformsData
+    ): int|false {
         $this->clearCache($podcastId);
 
         $podcastsPlatformsTable = $this->db->prefixTable('podcasts_platforms');
@@ -167,9 +178,9 @@ class PlatformModel extends Model
     }
 
     /**
-     * @return int|bool
+     * @param mixed[] $podcastsPlatformsData
      */
-    public function createPodcastPlatforms($podcastId, $podcastsPlatformsData)
+    public function createPodcastPlatforms(int $podcastId, array $podcastsPlatformsData): int|false
     {
         $this->clearCache($podcastId);
 
@@ -179,10 +190,7 @@ class PlatformModel extends Model
             ->insertBatch($podcastsPlatformsData);
     }
 
-    /**
-     * @return bool|string
-     */
-    public function removePodcastPlatform($podcastId, $platformSlug)
+    public function removePodcastPlatform(int $podcastId, string $platformSlug): bool|string
     {
         $this->clearCache($podcastId);
 
@@ -192,7 +200,7 @@ class PlatformModel extends Model
         ]);
     }
 
-    public function clearCache($podcastId): void
+    public function clearCache(int $podcastId): void
     {
         cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
 
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 7eaf25ec40..daf491beb4 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -8,13 +8,21 @@
 
 namespace App\Models;
 
+use App\Entities\Podcast;
+use CodeIgniter\Database\Query;
 use CodeIgniter\HTTP\URI;
 use CodeIgniter\Model;
 use phpseclib\Crypt\RSA;
 
 class PodcastModel extends Model
 {
+    /**
+     * @var string
+     */
     protected $table = 'podcasts';
+    /**
+     * @var string
+     */
     protected $primaryKey = 'id';
 
     /**
@@ -55,11 +63,23 @@ class PodcastModel extends Model
         'updated_by',
     ];
 
-    protected $returnType = \App\Entities\Podcast::class;
+    /**
+     * @var string
+     */
+    protected $returnType = Podcast::class;
+    /**
+     * @var bool
+     */
     protected $useSoftDeletes = true;
 
+    /**
+     * @var bool
+     */
     protected $useTimestamps = true;
 
+    /**
+     * @var array<string, string>
+     */
     protected $validationRules = [
         'title' => 'required',
         'name' =>
@@ -73,17 +93,35 @@ class PodcastModel extends Model
         'created_by' => 'required',
         'updated_by' => 'required',
     ];
+    /**
+     * @var mixed[]
+     */
     protected $validationMessages = [];
 
+    /**
+     * @var string[]
+     */
     protected $beforeInsert = ['createPodcastActor'];
+    /**
+     * @var string[]
+     */
     protected $afterInsert = ['setActorAvatar'];
+    /**
+     * @var string[]
+     */
     protected $afterUpdate = ['updatePodcastActor'];
 
     // clear cache before update if by any chance, the podcast name changes, so will the podcast link
+    /**
+     * @var string[]
+     */
     protected $beforeUpdate = ['clearCache'];
+    /**
+     * @var string[]
+     */
     protected $beforeDelete = ['clearCache'];
 
-    public function getPodcastByName($podcastName)
+    public function getPodcastByName(string $podcastName): ?Podcast
     {
         $cacheName = "podcast-{$podcastName}";
         if (!($found = cache($cacheName))) {
@@ -94,7 +132,7 @@ class PodcastModel extends Model
         return $found;
     }
 
-    public function getPodcastById($podcastId)
+    public function getPodcastById(int $podcastId): ?Podcast
     {
         $cacheName = "podcast#{$podcastId}";
         if (!($found = cache($cacheName))) {
@@ -106,7 +144,7 @@ class PodcastModel extends Model
         return $found;
     }
 
-    public function getPodcastByActorId($actorId)
+    public function getPodcastByActorId(int $actorId): ?Podcast
     {
         $cacheName = "podcast_actor#{$actorId}";
         if (!($found = cache($cacheName))) {
@@ -121,11 +159,9 @@ class PodcastModel extends Model
     /**
      *  Gets all the podcasts a given user is contributing to
      *
-     * @param int $userId
-     *
-     * @return \App\Entities\Podcast[] podcasts
+     * @return Podcast[] podcasts
      */
-    public function getUserPodcasts($userId)
+    public function getUserPodcasts(int $userId): array
     {
         $cacheName = "user{$userId}_podcasts";
         if (!($found = cache($cacheName))) {
@@ -143,33 +179,33 @@ class PodcastModel extends Model
         return $found;
     }
 
-    public function addPodcastContributor($userId, $podcastId, $groupId)
+    public function addPodcastContributor(int $userId, int $podcastId, int $groupId): Query|bool
     {
         cache()->delete("podcast#{$podcastId}_contributors");
 
         $data = [
-            'user_id' => (int) $userId,
-            'podcast_id' => (int) $podcastId,
-            'group_id' => (int) $groupId,
+            'user_id' => $userId,
+            'podcast_id' => $podcastId,
+            'group_id' => $groupId,
         ];
 
         return $this->db->table('podcasts_users')->insert($data);
     }
 
-    public function updatePodcastContributor($userId, $podcastId, $groupId)
+    public function updatePodcastContributor(int $userId, int $podcastId, int $groupId): bool
     {
         cache()->delete("podcast#{$podcastId}_contributors");
 
         return $this->db
             ->table('podcasts_users')
             ->where([
-                'user_id' => (int) $userId,
-                'podcast_id' => (int) $podcastId,
+                'user_id' => $userId,
+                'podcast_id' => $podcastId,
             ])
             ->update(['group_id' => $groupId]);
     }
 
-    public function removePodcastContributor($userId, $podcastId)
+    public function removePodcastContributor(int $userId, int $podcastId): string|bool
     {
         cache()->delete("podcast#{$podcastId}_contributors");
 
@@ -182,7 +218,7 @@ class PodcastModel extends Model
             ->delete();
     }
 
-    public function getContributorGroupId($userId, $podcastId)
+    public function getContributorGroupId(int $userId, int $podcastId): int|false
     {
         if (!is_numeric($podcastId)) {
             // identifier is the podcast name, request must be a join
@@ -208,11 +244,14 @@ class PodcastModel extends Model
                 ->getResultObject();
         }
 
-        return (int) count($user_podcast) > 0
+        return count($user_podcast) > 0
             ? $user_podcast[0]->group_id
             : false;
     }
 
+    /**
+     * @return array<string, string>[]
+     */
     public function getYears(int $podcastId): array
     {
         $cacheName = "podcast#{$podcastId}_years";
@@ -249,6 +288,9 @@ class PodcastModel extends Model
         return $found;
     }
 
+    /**
+     * @return array<string, string>[]
+     */
     public function getSeasons(int $podcastId): array
     {
         $cacheName = "podcast#{$podcastId}_seasons";
@@ -286,11 +328,9 @@ class PodcastModel extends Model
     /**
      * Returns the default query for displaying the episode list on the podcast page
      *
-     * @param int $podcastId
-     *
-     * @return array|null
+     * @return array<string, mixed>|null
      */
-    public function getDefaultQuery(int $podcastId)
+    public function getDefaultQuery(int $podcastId): ?array
     {
         $cacheName = "podcast#{$podcastId}_defaultQuery";
         if (!($defaultQuery = cache($cacheName))) {
@@ -301,12 +341,7 @@ class PodcastModel extends Model
                 $defaultQuery = ['type' => 'season', 'data' => end($seasons)];
             } else {
                 $years = $this->getYears($podcastId);
-                if (!empty($years)) {
-                    // get most recent year
-                    $defaultQuery = ['type' => 'year', 'data' => $years[0]];
-                } else {
-                    $defaultQuery = null;
-                }
+                $defaultQuery = $years === [] ? null : ['type' => 'year', 'data' => $years[0]];
             }
 
             cache()->save($cacheName, $defaultQuery, DECADE);
@@ -318,9 +353,11 @@ class PodcastModel extends Model
      * Creates an actor linked to the podcast
      * (Triggered before insert)
      *
-     * @param array $data
+     * @param mixed[] $data
+     * 
+     * @return mixed[]
      */
-    protected function createPodcastActor(array $data)
+    protected function createPodcastActor(array $data): array
     {
         $rsa = new RSA();
         $rsa->setHash('sha256');
@@ -356,7 +393,12 @@ class PodcastModel extends Model
         return $data;
     }
 
-    protected function setActorAvatar($data)
+    /**
+     * @param mixed[] $data
+     * 
+     * @return mixed[]
+     */
+    protected function setActorAvatar(array $data): array
     {
         $podcast = (new PodcastModel())->getPodcastById(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
@@ -372,7 +414,12 @@ class PodcastModel extends Model
         return $data;
     }
 
-    protected function updatePodcastActor(array $data)
+    /**
+     * @param mixed[] $data
+     * 
+     * @return mixed[]
+     */
+    protected function updatePodcastActor(array $data): array
     {
         $podcast = (new PodcastModel())->getPodcastById(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
@@ -394,7 +441,12 @@ class PodcastModel extends Model
         return $data;
     }
 
-    public function clearCache(array $data)
+    /**
+     * @param mixed[] $data
+     *
+     * @return mixed[]
+     */
+    protected function clearCache(array $data): array
     {
         $podcast = (new PodcastModel())->getPodcastById(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
diff --git a/app/Models/PodcastPersonModel.php b/app/Models/PodcastPersonModel.php
deleted file mode 100644
index 94904b9026..0000000000
--- a/app/Models/PodcastPersonModel.php
+++ /dev/null
@@ -1,159 +0,0 @@
-<?php
-
-/**
- * @copyright  2021 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Database\BaseResult;
-use App\Entities\PodcastPerson;
-use CodeIgniter\Model;
-
-class PodcastPersonModel extends Model
-{
-    /**
-     * @var string
-     */
-    protected $table = 'podcasts_persons';
-    /**
-     * @var string
-     */
-    protected $primaryKey = 'id';
-
-    /**
-     * @var string[]
-     */
-    protected $allowedFields = [
-        'id',
-        'podcast_id',
-        'person_id',
-        'person_group',
-        'person_role',
-    ];
-
-    /**
-     * @var string
-     */
-    protected $returnType = PodcastPerson::class;
-    /**
-     * @var bool
-     */
-    protected $useSoftDeletes = false;
-
-    /**
-     * @var bool
-     */
-    protected $useTimestamps = false;
-
-    /**
-     * @var array<string, string>
-     */
-    protected $validationRules = [
-        'podcast_id' => 'required',
-        'person_id' => 'required',
-    ];
-
-    /**
-     * @var string[]
-     */
-    protected $afterInsert = ['clearCache'];
-
-    /**
-     * @var string[]
-     */
-    protected $beforeDelete = ['clearCache'];
-
-    /**
-     * @return PodcastPerson[]
-     */
-    public function getPodcastPersons(int $podcastId): array
-    {
-        $cacheName = "podcast#{$podcastId}_persons";
-        if (!($found = cache($cacheName))) {
-            $found = $this->select('podcasts_persons.*')
-                ->where('podcast_id', $podcastId)
-                ->join('persons', 'person_id=persons.id')
-                ->orderby('full_name')
-                ->findAll();
-
-            cache()->save($cacheName, $found, DECADE);
-        }
-
-        return $found;
-    }
-
-    /**
-     * Add persons to podcast
-     *
-     * @param array<string> $persons
-     * @param array<string, string> $groupsRoles
-     *
-     * @return bool|int Number of rows inserted or FALSE on failure
-     */
-    public function addPodcastPersons(
-        int $podcastId,
-        array $persons = [],
-        array $groupsRoles = []
-    ) {
-        if ($persons === []) {
-            return 0;
-        }
-
-        $this->clearCache(['podcast_id' => $podcastId]);
-        $data = [];
-        foreach ($persons as $person) {
-            if ($groupsRoles === []) {
-                $data[] = [
-                    'podcast_id' => $podcastId,
-                    'person_id' => $person,
-                ];
-            }
-
-            foreach ($groupsRoles as $group_role) {
-                $group_role = explode(',', $group_role);
-                $data[] = [
-                    'podcast_id' => $podcastId,
-                    'person_id' => $person,
-                    'person_group' => $group_role[0],
-                    'person_role' => $group_role[1],
-                ];
-            }
-        }
-
-        return $this->insertBatch($data);
-    }
-
-    /**
-     * @return bool|BaseResult
-     */
-    public function removePodcastPersons($podcastId, $podcastPersonId)
-    {
-        return $this->delete([
-            'podcast_id' => $podcastId,
-            'id' => $podcastPersonId,
-        ]);
-    }
-
-    /**
-     * @return array<string, array<string|int, mixed>>
-     */
-    protected function clearCache(array $data): array
-    {
-        if (isset($data['podcast_id'])) {
-            $podcastId = $data['podcast_id'];
-        } else {
-            $person = (new PodcastPersonModel())->find(
-                is_array($data['id']) ? $data['id']['id'] : $data['id'],
-            );
-            $podcastId = $person->podcast_id;
-        }
-
-        cache()->delete("podcast#{$podcastId}_persons");
-        (new PodcastModel())->clearCache(['id' => $podcastId]);
-
-        return $data;
-    }
-}
diff --git a/app/Models/SoundbiteModel.php b/app/Models/SoundbiteModel.php
index 8bdf04ead2..3c00ae03a7 100644
--- a/app/Models/SoundbiteModel.php
+++ b/app/Models/SoundbiteModel.php
@@ -66,10 +66,7 @@ class SoundbiteModel extends Model
      */
     protected $beforeDelete = ['clearCache'];
 
-    /**
-     * @return bool|BaseResult
-     */
-    public function deleteSoundbite($podcastId, $episodeId, $soundbiteId)
+    public function deleteSoundbite(int $podcastId, int $episodeId, int $soundbiteId): BaseResult|bool
     {
         return $this->delete([
             'podcast_id' => $podcastId,
@@ -99,6 +96,7 @@ class SoundbiteModel extends Model
     }
 
     /**
+     * @param array<string, array<string|int, mixed>> $data
      * @return array<string, array<string|int, mixed>>
      */
     public function clearCache(array $data): array
diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php
index 3cc6d285e1..1d7c7e858c 100644
--- a/app/Models/UserModel.php
+++ b/app/Models/UserModel.php
@@ -18,7 +18,10 @@ class UserModel extends MythAuthUserModel
      */
     protected $returnType = User::class;
 
-    public function getPodcastContributors($podcastId)
+    /**
+     * @return User[]
+     */
+    public function getPodcastContributors(int $podcastId): array
     {
         $cacheName = "podcast#{$podcastId}_contributors";
         if (!($found = cache($cacheName))) {
@@ -37,7 +40,7 @@ class UserModel extends MythAuthUserModel
         return $found;
     }
 
-    public function getPodcastContributor($user_id, $podcast_id)
+    public function getPodcastContributor(int $userId, int $podcastId): ?User
     {
         return $this->select(
             'users.*, podcasts_users.podcast_id as podcast_id, auth_groups.name as podcast_role',
@@ -45,8 +48,8 @@ class UserModel extends MythAuthUserModel
             ->join('podcasts_users', 'podcasts_users.user_id = users.id')
             ->join('auth_groups', 'auth_groups.id = podcasts_users.group_id')
             ->where([
-                'users.id' => $user_id,
-                'podcast_id' => $podcast_id,
+                'users.id' => $userId,
+                'podcast_id' => $podcastId,
             ])
             ->first();
     }
diff --git a/app/Validation/FileRules.php b/app/Validation/FileRules.php
index 05a7284a63..3ccc42b0d3 100644
--- a/app/Validation/FileRules.php
+++ b/app/Validation/FileRules.php
@@ -17,7 +17,7 @@ class FileRules extends ValidationFileRules
      * a specified allowable dimension.
      *
      * @param string|null $blank
-     *
+     * 
      */
     public function min_dims(string $blank = null, string $params): bool
     {
diff --git a/app/Views/errors/cli/error_exception.php b/app/Views/errors/cli/error_exception.php
index 8fe9a97a22..14a0907d35 100644
--- a/app/Views/errors/cli/error_exception.php
+++ b/app/Views/errors/cli/error_exception.php
@@ -4,7 +4,7 @@ use CodeIgniter\CLI\CLI;
 
 // The main Exception
 CLI::newLine();
-CLI::write('[' . get_class($exception) . ']', 'light_gray', 'red');
+CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
 CLI::newLine();
 CLI::write($message);
 CLI::newLine();
@@ -58,7 +58,7 @@ if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
             array_map(function ($value) {
                 switch (true) {
                     case is_object($value):
-                        return 'Object(' . get_class($value) . ')';
+                        return 'Object(' . $value::class . ')';
                     case is_array($value):
                         return count($value) > 0 ? '[...]' : '[]';
                     case is_null($value):
diff --git a/composer.json b/composer.json
index 958cdad1f5..8431bdfe5c 100644
--- a/composer.json
+++ b/composer.json
@@ -6,15 +6,15 @@
   "homepage": "https://castopod.org",
   "license": "AGPL-3.0-or-later",
   "require": {
-    "php": "^7.3||^8.0",
+    "php": "^8.0",
     "james-heinrich/getid3": "~2.0.0-dev",
     "whichbrowser/parser": "^v2.1.1",
     "geoip2/geoip2": "^v2.11.0",
     "myth/auth": "dev-develop",
     "codeigniter4/codeigniter4": "dev-develop",
-    "league/commonmark": "^1.6.0",
+    "league/commonmark": "^1.6.2",
     "vlucas/phpdotenv": "^v5.3.0",
-    "league/html-to-markdown": "^4.10",
+    "league/html-to-markdown": "^4.10.0",
     "opawg/user-agents-php": "^v1.0",
     "podlibre/ipcat": "^v1.0",
     "podlibre/podcast-namespace": "^v1.0.6",
@@ -24,14 +24,14 @@
   },
   "require-dev": {
     "mikey179/vfsstream": "^v1.6.8",
-    "phpunit/phpunit": "^9.5.1",
+    "phpunit/phpunit": "^9.5.4",
     "squizlabs/php_codesniffer": "^3.6.0",
     "rector/rector": "^0.10.17",
     "captainhook/captainhook": "^5.9",
     "captainhook/plugin-composer": "^5.2",
     "phpstan/phpstan": "^0.12.85",
-    "phpstan/extension-installer": "^1.1",
-    "rector/rector-phpstan-rules": "^0.2.1"
+    "phpstan/extension-installer": "^1.1.0",
+    "rector/rector-phpstan-rules": "^0.2.6"
   },
   "autoload": {
     "psr-4": {
diff --git a/composer.lock b/composer.lock
index be9d77faca..16611f06cf 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "cdbc538742689916718b17f15ba7d875",
+    "content-hash": "d5423c58c26549da4a5a20bfa4e65909",
     "packages": [
         {
             "name": "brick/math",
@@ -68,12 +68,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/codeigniter4/CodeIgniter4.git",
-                "reference": "5713da574bfaa4d73949d84330ccf80758cd6ae4"
+                "reference": "43e0e9611b7e527a4b927127fc6f26a1c6d123a5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/5713da574bfaa4d73949d84330ccf80758cd6ae4",
-                "reference": "5713da574bfaa4d73949d84330ccf80758cd6ae4",
+                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/43e0e9611b7e527a4b927127fc6f26a1c6d123a5",
+                "reference": "43e0e9611b7e527a4b927127fc6f26a1c6d123a5",
                 "shasum": ""
             },
             "require": {
@@ -152,7 +152,7 @@
                 "slack": "https://codeigniterchat.slack.com",
                 "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
             },
-            "time": "2021-05-12T10:19:48+00:00"
+            "time": "2021-05-13T17:30:38+00:00"
         },
         {
             "name": "composer/ca-bundle",
@@ -1196,12 +1196,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/lonnieezell/myth-auth.git",
-                "reference": "2b42da1884745eec22ac10f7941a4f9350576a86"
+                "reference": "c7f79d1b938e371cfafdc7e3c59c810f2c672727"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/2b42da1884745eec22ac10f7941a4f9350576a86",
-                "reference": "2b42da1884745eec22ac10f7941a4f9350576a86",
+                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/c7f79d1b938e371cfafdc7e3c59c810f2c672727",
+                "reference": "c7f79d1b938e371cfafdc7e3c59c810f2c672727",
                 "shasum": ""
             },
             "require": {
@@ -1216,7 +1216,7 @@
                 "fakerphp/faker": "^1.9",
                 "mockery/mockery": "^1.0",
                 "phpstan/phpstan": "^0.12",
-                "phpunit/phpunit": "^9.0",
+                "phpunit/phpunit": "^9.2",
                 "squizlabs/php_codesniffer": "^3.5"
             },
             "default-branch": true,
@@ -1262,7 +1262,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2021-05-02T05:32:03+00:00"
+            "time": "2021-05-13T18:19:37+00:00"
         },
         {
             "name": "opawg/user-agents-php",
@@ -2834,118 +2834,6 @@
             ],
             "time": "2020-11-10T18:47:58+00:00"
         },
-        {
-            "name": "ergebnis/json-printer",
-            "version": "3.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ergebnis/json-printer.git",
-                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
-                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-mbstring": "*",
-                "php": "^7.2 || ^8.0"
-            },
-            "require-dev": {
-                "ergebnis/license": "^1.0.0",
-                "ergebnis/php-cs-fixer-config": "^2.2.1",
-                "ergebnis/phpstan-rules": "~0.15.2",
-                "ergebnis/test-util": "^1.1.0",
-                "infection/infection": "~0.15.3",
-                "phpstan/extension-installer": "^1.0.4",
-                "phpstan/phpstan": "~0.12.40",
-                "phpstan/phpstan-deprecation-rules": "~0.12.5",
-                "phpstan/phpstan-phpunit": "~0.12.16",
-                "phpstan/phpstan-strict-rules": "~0.12.4",
-                "phpunit/phpunit": "^8.5.8",
-                "psalm/plugin-phpunit": "~0.11.0",
-                "vimeo/psalm": "^3.14.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Ergebnis\\Json\\Printer\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andreas Möller",
-                    "email": "am@localheinz.com"
-                }
-            ],
-            "description": "Provides a JSON printer, allowing for flexible indentation.",
-            "homepage": "https://github.com/ergebnis/json-printer",
-            "keywords": [
-                "formatter",
-                "json",
-                "printer"
-            ],
-            "support": {
-                "issues": "https://github.com/ergebnis/json-printer/issues",
-                "source": "https://github.com/ergebnis/json-printer"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/localheinz",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-08-30T12:17:03+00:00"
-        },
-        {
-            "name": "idiosyncratic/editorconfig",
-            "version": "0.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/idiosyncratic-code/editorconfig-php.git",
-                "reference": "50f742daee8b7a632b795f5927d8d88c43dd3a4f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/idiosyncratic-code/editorconfig-php/zipball/50f742daee8b7a632b795f5927d8d88c43dd3a4f",
-                "reference": "50f742daee8b7a632b795f5927d8d88c43dd3a4f",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.3"
-            },
-            "require-dev": {
-                "idiosyncratic/devtools": "^0.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Idiosyncratic\\EditorConfig\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "ISC"
-            ],
-            "authors": [
-                {
-                    "name": "Jason Silkey",
-                    "email": "jason@jasonsilkey.com"
-                }
-            ],
-            "description": "PHP implementation of EditorConfig",
-            "support": {
-                "issues": "https://github.com/idiosyncratic-code/editorconfig-php/issues",
-                "source": "https://github.com/idiosyncratic-code/editorconfig-php/tree/0.1.1"
-            },
-            "time": "2021-05-03T15:39:40+00:00"
-        },
         {
             "name": "jean85/pretty-package-versions",
             "version": "1.6.0",
@@ -3313,6 +3201,73 @@
             },
             "time": "2021-02-28T12:30:32+00:00"
         },
+        {
+            "name": "nette/robot-loader",
+            "version": "v3.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nette/robot-loader.git",
+                "reference": "3973cf3970d1de7b30888fd10b92dac9e0c2fd82"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nette/robot-loader/zipball/3973cf3970d1de7b30888fd10b92dac9e0c2fd82",
+                "reference": "3973cf3970d1de7b30888fd10b92dac9e0c2fd82",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "nette/finder": "^2.5 || ^3.0",
+                "nette/utils": "^3.0",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "nette/tester": "^2.0",
+                "phpstan/phpstan": "^0.12",
+                "tracy/tracy": "^2.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.4-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause",
+                "GPL-2.0-only",
+                "GPL-3.0-only"
+            ],
+            "authors": [
+                {
+                    "name": "David Grudl",
+                    "homepage": "https://davidgrudl.com"
+                },
+                {
+                    "name": "Nette Community",
+                    "homepage": "https://nette.org/contributors"
+                }
+            ],
+            "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.",
+            "homepage": "https://nette.org",
+            "keywords": [
+                "autoload",
+                "class",
+                "interface",
+                "nette",
+                "trait"
+            ],
+            "support": {
+                "issues": "https://github.com/nette/robot-loader/issues",
+                "source": "https://github.com/nette/robot-loader/tree/v3.4.0"
+            },
+            "time": "2021-03-07T15:12:01+00:00"
+        },
         {
             "name": "nette/utils",
             "version": "v3.2.2",
@@ -3400,16 +3355,16 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.10.4",
+            "version": "v4.10.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e"
+                "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e",
-                "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f",
+                "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f",
                 "shasum": ""
             },
             "require": {
@@ -3450,9 +3405,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5"
             },
-            "time": "2020-12-20T10:01:03+00:00"
+            "time": "2021-05-03T19:11:20+00:00"
         },
         {
             "name": "phar-io/manifest",
@@ -3887,16 +3842,16 @@
         },
         {
             "name": "phpstan/phpstan",
-            "version": "0.12.85",
+            "version": "0.12.86",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "20e6333c0067875ad7697cd8acdf245c6ef69d03"
+                "reference": "a84fdc53ecca7643dbc89ef8880d8b393a6c155a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/20e6333c0067875ad7697cd8acdf245c6ef69d03",
-                "reference": "20e6333c0067875ad7697cd8acdf245c6ef69d03",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a84fdc53ecca7643dbc89ef8880d8b393a6c155a",
+                "reference": "a84fdc53ecca7643dbc89ef8880d8b393a6c155a",
                 "shasum": ""
             },
             "require": {
@@ -3927,7 +3882,7 @@
             "description": "PHPStan - PHP Static Analysis Tool",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/0.12.85"
+                "source": "https://github.com/phpstan/phpstan/tree/0.12.86"
             },
             "funding": [
                 {
@@ -3943,25 +3898,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-27T14:13:16+00:00"
+            "time": "2021-05-08T11:29:01+00:00"
         },
         {
             "name": "phpstan/phpstan-phpunit",
-            "version": "0.12.18",
+            "version": "0.12.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan-phpunit.git",
-                "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72"
+                "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72",
-                "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72",
+                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/52f7072ddc5f81492f9d2de65a24813a48c90b18",
+                "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.1 || ^8.0",
-                "phpstan/phpstan": "^0.12.60"
+                "phpstan/phpstan": "^0.12.86"
             },
             "conflict": {
                 "phpunit/phpunit": "<7.0"
@@ -3996,9 +3951,9 @@
             "description": "PHPUnit extensions and rules for PHPStan",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
-                "source": "https://github.com/phpstan/phpstan-phpunit/tree/0.12.18"
+                "source": "https://github.com/phpstan/phpstan-phpunit/tree/0.12.19"
             },
-            "time": "2021-03-06T11:51:27+00:00"
+            "time": "2021-04-30T11:10:37+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -4519,102 +4474,49 @@
             },
             "time": "2019-01-08T18:20:26+00:00"
         },
-        {
-            "name": "rector/extension-installer",
-            "version": "0.10.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/rectorphp/extension-installer.git",
-                "reference": "56c97630fca170b5586b2f08e76348f924ebb8dd"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/rectorphp/extension-installer/zipball/56c97630fca170b5586b2f08e76348f924ebb8dd",
-                "reference": "56c97630fca170b5586b2f08e76348f924ebb8dd",
-                "shasum": ""
-            },
-            "require": {
-                "composer-plugin-api": "^1.1 || ^2.0",
-                "php": "^7.3 || ^8.0"
-            },
-            "require-dev": {
-                "composer/composer": "^2.0",
-                "composer/xdebug-handler": "2.0 as 1.4",
-                "friendsofphp/php-cs-fixer": "^3.0",
-                "jangregor/phpstan-prophecy": "^0.8.1",
-                "phpspec/prophecy-phpunit": "^2.0",
-                "phpstan/extension-installer": "^1.1",
-                "phpunit/phpunit": "^9.5",
-                "rector/rector-phpstan-rules": "^0.1",
-                "symplify/easy-coding-standard": "^9.3.1",
-                "symplify/phpstan-extensions": "^9.3",
-                "symplify/phpstan-rules": "^9.3"
-            },
-            "type": "composer-plugin",
-            "extra": {
-                "class": "Rector\\RectorInstaller\\Plugin"
-            },
-            "autoload": {
-                "psr-4": {
-                    "Rector\\RectorInstaller\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "Composer plugin for automatic installation of Rector extensions",
-            "support": {
-                "issues": "https://github.com/rectorphp/extension-installer/issues",
-                "source": "https://github.com/rectorphp/extension-installer/tree/0.10.2"
-            },
-            "time": "2021-05-06T21:14:19+00:00"
-        },
         {
             "name": "rector/rector",
-            "version": "0.10.22",
+            "version": "0.10.17",
             "source": {
                 "type": "git",
-                "url": "https://github.com/rectorphp/rector-src.git",
-                "reference": "d2b8907ce6fb86dd4d8fe2dd661f348b97e812e3"
+                "url": "https://github.com/rectorphp/rector.git",
+                "reference": "ef5167c528cb4b2ca983adaf5a4cccbcad2547b7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/rectorphp/rector-src/zipball/d2b8907ce6fb86dd4d8fe2dd661f348b97e812e3",
-                "reference": "d2b8907ce6fb86dd4d8fe2dd661f348b97e812e3",
+                "url": "https://api.github.com/repos/rectorphp/rector/zipball/ef5167c528cb4b2ca983adaf5a4cccbcad2547b7",
+                "reference": "ef5167c528cb4b2ca983adaf5a4cccbcad2547b7",
                 "shasum": ""
             },
             "require": {
                 "composer/semver": "^3.2",
-                "composer/xdebug-handler": "^1.3|^2.0",
+                "composer/xdebug-handler": "^1.4|^2.0",
                 "danielstjules/stringy": "^3.1",
                 "doctrine/inflector": "^2.0",
-                "ergebnis/json-printer": "^3.1",
                 "ext-dom": "*",
                 "ext-json": "*",
-                "idiosyncratic/editorconfig": "^0.1.0",
                 "jean85/pretty-package-versions": "^1.6",
                 "nette/caching": "^3.1",
+                "nette/robot-loader": "^3.4",
                 "nette/utils": "^3.2",
-                "nikic/php-parser": "4.10.4",
+                "nikic/php-parser": "^4.10.4",
                 "php": "^7.3|^8.0",
                 "phpstan/phpdoc-parser": "^0.5.4",
-                "phpstan/phpstan": "0.12.85",
+                "phpstan/phpstan": "^0.12.83",
                 "phpstan/phpstan-phpunit": "^0.12.18",
-                "rector/extension-installer": "^0.10.2",
                 "rector/rector-cakephp": "^0.10.4",
                 "rector/rector-doctrine": "^0.10.6",
+                "rector/rector-installer": "^0.10.0",
                 "rector/rector-laravel": "^0.10.2",
-                "rector/rector-nette": "^0.10.9",
-                "rector/rector-nette-to-symfony": "^0.10.0",
+                "rector/rector-nette": "^0.10.8",
                 "rector/rector-phpunit": "^0.10.8",
                 "rector/rector-symfony": "^0.10.5",
                 "sebastian/diff": "^4.0.4",
-                "shanethehat/pretty-xml": "^1.0",
                 "symfony/console": "^4.4.8|^5.1",
                 "symfony/dependency-injection": "^5.1",
                 "symfony/finder": "^4.4.8|^5.1",
                 "symfony/http-kernel": "^4.4.8|^5.1",
+                "symfony/process": "^4.4.8|^5.1",
                 "symplify/astral": "^9.3",
                 "symplify/autowire-array-parameter": "^9.3",
                 "symplify/console-color-diff": "^9.3",
@@ -4632,13 +4534,16 @@
                 "rector/rector-prefixed": "self.version"
             },
             "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.18.6",
+                "nette/application": "^3.0.7",
+                "nette/di": "^3.0",
+                "nette/forms": "^3.0",
                 "phpstan/extension-installer": "^1.1",
                 "phpstan/phpstan-nette": "^0.12.16",
                 "phpunit/phpunit": "^9.5",
                 "rector/rector-generator": "^0.1.7",
                 "rector/rector-phpstan-rules": "^0.1",
                 "symplify/coding-standard": "^9.3",
-                "symplify/composer-json-manipulator": "^9.3",
                 "symplify/easy-ci": "^9.3",
                 "symplify/easy-coding-standard": "^9.3",
                 "symplify/easy-testing": "^9.3",
@@ -4665,18 +4570,36 @@
                     "Rector\\Compiler\\": "utils/compiler/src"
                 },
                 "files": [
-                    "src/functions/node_helper.php",
-                    "src/constants.php"
+                    "src/functions/node_helper.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
+            "authors": [
+                {
+                    "name": "Tomas Votruba",
+                    "email": "tomas.vot@gmail.com",
+                    "homepage": "https://tomasvotruba.com"
+                },
+                {
+                    "name": "Jan Mikes",
+                    "email": "j.mikes@me.com",
+                    "homepage": "https://janmikes.cz"
+                }
+            ],
             "description": "Instant upgrade and refactoring of your PHP code",
             "homepage": "https://getrector.org",
+            "keywords": [
+                "ast",
+                "automated refactoring",
+                "instant refactoring",
+                "instant upgrades"
+            ],
             "support": {
-                "source": "https://github.com/rectorphp/rector-src/tree/0.10.22"
+                "issues": "https://github.com/rectorphp/rector/issues",
+                "source": "https://github.com/rectorphp/rector/tree/0.10.17"
             },
             "funding": [
                 {
@@ -4684,7 +4607,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:28:39+00:00"
+            "time": "2021-05-02T22:55:38+00:00"
         },
         {
             "name": "rector/rector-cakephp",
@@ -4798,6 +4721,58 @@
             },
             "time": "2021-04-24T12:17:00+00:00"
         },
+        {
+            "name": "rector/rector-installer",
+            "version": "0.10.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/rectorphp/extension-installer.git",
+                "reference": "56c97630fca170b5586b2f08e76348f924ebb8dd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/rectorphp/extension-installer/zipball/56c97630fca170b5586b2f08e76348f924ebb8dd",
+                "reference": "56c97630fca170b5586b2f08e76348f924ebb8dd",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.1 || ^2.0",
+                "php": "^7.3 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^2.0",
+                "composer/xdebug-handler": "2.0 as 1.4",
+                "friendsofphp/php-cs-fixer": "^3.0",
+                "jangregor/phpstan-prophecy": "^0.8.1",
+                "phpspec/prophecy-phpunit": "^2.0",
+                "phpstan/extension-installer": "^1.1",
+                "phpunit/phpunit": "^9.5",
+                "rector/rector-phpstan-rules": "^0.1",
+                "symplify/easy-coding-standard": "^9.3.1",
+                "symplify/phpstan-extensions": "^9.3",
+                "symplify/phpstan-rules": "^9.3"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "Rector\\RectorInstaller\\Plugin"
+            },
+            "autoload": {
+                "psr-4": {
+                    "Rector\\RectorInstaller\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Composer plugin for automatic installation of Rector extensions",
+            "support": {
+                "issues": "https://github.com/rectorphp/extension-installer/issues",
+                "source": "https://github.com/rectorphp/extension-installer/tree/0.10.2"
+            },
+            "abandoned": "rector/extension-installer",
+            "time": "2021-05-06T21:14:19+00:00"
+        },
         {
             "name": "rector/rector-laravel",
             "version": "0.10.2",
@@ -4856,22 +4831,22 @@
         },
         {
             "name": "rector/rector-nette",
-            "version": "0.10.10",
+            "version": "0.10.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/rectorphp/rector-nette.git",
-                "reference": "e9f0de1ffb3ba1eecdaa281fcd64fda1d3515e6f"
+                "reference": "19c85c870f9a7a90d1f2e9be9b3e048b4db69697"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/rectorphp/rector-nette/zipball/e9f0de1ffb3ba1eecdaa281fcd64fda1d3515e6f",
-                "reference": "e9f0de1ffb3ba1eecdaa281fcd64fda1d3515e6f",
+                "url": "https://api.github.com/repos/rectorphp/rector-nette/zipball/19c85c870f9a7a90d1f2e9be9b3e048b4db69697",
+                "reference": "19c85c870f9a7a90d1f2e9be9b3e048b4db69697",
                 "shasum": ""
             },
             "require": {
                 "ext-xml": "*",
                 "php": ">=7.3",
-                "rector/rector": "^0.10.19"
+                "rector/rector": "^0.10.12"
             },
             "conflict": {
                 "rector/rector": "<=0.10.3"
@@ -4881,14 +4856,13 @@
                 "nette/di": "^3.0",
                 "nette/forms": "3.0.*",
                 "phpstan/extension-installer": "^1.1",
-                "phpstan/phpstan": "^0.12.85",
                 "phpstan/phpstan-nette": "^0.12.16",
                 "phpunit/phpunit": "^9.5",
-                "rector/rector-phpstan-rules": "^0.2",
-                "symplify/easy-coding-standard": "^9.3",
-                "symplify/phpstan-extensions": "^9.3",
-                "symplify/phpstan-rules": "^9.3",
-                "symplify/rule-doc-generator": "^9.3"
+                "rector/rector-phpstan-rules": "^0.1",
+                "symplify/easy-coding-standard": "^9.2",
+                "symplify/phpstan-extensions": "^9.2",
+                "symplify/phpstan-rules": "^9.2",
+                "symplify/rule-doc-generator": "^9.2"
             },
             "type": "rector-extension",
             "extra": {
@@ -4913,91 +4887,29 @@
             "description": "Rector upgrades rules for Nette Framework",
             "support": {
                 "issues": "https://github.com/rectorphp/rector-nette/issues",
-                "source": "https://github.com/rectorphp/rector-nette/tree/0.10.10"
-            },
-            "time": "2021-05-06T23:55:02+00:00"
-        },
-        {
-            "name": "rector/rector-nette-to-symfony",
-            "version": "0.10.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/rectorphp/rector-nette-to-symfony.git",
-                "reference": "27be9cb982ac7ad3799e0ac7fd45be0a46d7fb0b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/rectorphp/rector-nette-to-symfony/zipball/27be9cb982ac7ad3799e0ac7fd45be0a46d7fb0b",
-                "reference": "27be9cb982ac7ad3799e0ac7fd45be0a46d7fb0b",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.3",
-                "rector/rector": "^0.10.19",
-                "rector/rector-nette": "^0.10.9"
+                "source": "https://github.com/rectorphp/rector-nette/tree/0.10.9"
             },
-            "require-dev": {
-                "nette/application": "^3.1",
-                "nette/forms": "^3.1",
-                "phpstan/extension-installer": "^1.1",
-                "phpstan/phpstan-nette": "^0.12.16",
-                "phpunit/phpunit": "^9.5",
-                "rector/rector-phpstan-rules": "^0.1",
-                "symfony/form": "^5.2",
-                "symplify/easy-coding-standard": "^9.3",
-                "symplify/phpstan-extensions": "^9.3",
-                "symplify/phpstan-rules": "^9.3",
-                "symplify/rule-doc-generator": "^9.3"
-            },
-            "type": "rector-extension",
-            "extra": {
-                "rector": {
-                    "includes": [
-                        "config/config.php"
-                    ]
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Rector\\NetteToSymfony\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "Generate Rector rules from command line",
-            "support": {
-                "issues": "https://github.com/rectorphp/rector-nette-to-symfony/issues",
-                "source": "https://github.com/rectorphp/rector-nette-to-symfony/tree/0.10.0"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/tomasvotruba",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-05-05T21:07:31+00:00"
+            "time": "2021-04-26T10:35:59+00:00"
         },
         {
             "name": "rector/rector-phpstan-rules",
-            "version": "0.2.1",
+            "version": "0.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/rectorphp/phpstan-rules.git",
-                "reference": "6f43a35676463c4688e120194d56928c4be523bb"
+                "reference": "765d8bf702a3928a155be02d3b7413a16b46bf95"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/rectorphp/phpstan-rules/zipball/6f43a35676463c4688e120194d56928c4be523bb",
-                "reference": "6f43a35676463c4688e120194d56928c4be523bb",
+                "url": "https://api.github.com/repos/rectorphp/phpstan-rules/zipball/765d8bf702a3928a155be02d3b7413a16b46bf95",
+                "reference": "765d8bf702a3928a155be02d3b7413a16b46bf95",
                 "shasum": ""
             },
             "require": {
                 "nette/utils": "^3.2",
                 "php": ">=7.3",
-                "phpstan/phpstan": "^0.12.84",
-                "symplify/phpstan-rules": "^9.2.22"
+                "phpstan/phpstan": "^0.12.86",
+                "symplify/phpstan-rules": "^9.3.5"
             },
             "require-dev": {
                 "phpstan/extension-installer": "^1.1",
@@ -5015,7 +4927,7 @@
             },
             "autoload": {
                 "psr-4": {
-                    "Rector\\RectorPHPStanRules\\": "src"
+                    "Rector\\PHPStanRules\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -5025,9 +4937,9 @@
             "description": "PHPStan rules for Rector projects - with focus on static reflection, constant re-use and Rector design patterns",
             "support": {
                 "issues": "https://github.com/rectorphp/phpstan-rules/issues",
-                "source": "https://github.com/rectorphp/phpstan-rules/tree/0.2.1"
+                "source": "https://github.com/rectorphp/phpstan-rules/tree/0.2.8"
             },
-            "time": "2021-05-06T23:51:11+00:00"
+            "time": "2021-05-14T08:39:51+00:00"
         },
         {
             "name": "rector/rector-phpunit",
@@ -6281,53 +6193,6 @@
             ],
             "time": "2021-04-10T08:31:02+00:00"
         },
-        {
-            "name": "shanethehat/pretty-xml",
-            "version": "1.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/shanethehat/pretty-xml.git",
-                "reference": "2b063c6544c8dc9563c53cb72eb06d1d74c9e75f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/shanethehat/pretty-xml/zipball/2b063c6544c8dc9563c53cb72eb06d1d74c9e75f",
-                "reference": "2b063c6544c8dc9563c53cb72eb06d1d74c9e75f",
-                "shasum": ""
-            },
-            "require-dev": {
-                "behat/behat": "~3.0",
-                "bossa/phpspec2-expect": "*",
-                "phpspec/phpspec": "~2.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "PrettyXml": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Shane Auckland",
-                    "email": "shane.auckland@gmail.com",
-                    "homepage": "http://shaneauckland.co.uk"
-                }
-            ],
-            "description": "Library for pretty-printing XML",
-            "keywords": [
-                "pretty",
-                "xml"
-            ],
-            "support": {
-                "issues": "https://github.com/shanethehat/pretty-xml/issues",
-                "source": "https://github.com/shanethehat/pretty-xml/tree/master"
-            },
-            "time": "2015-08-10T14:22:54+00:00"
-        },
         {
             "name": "squizlabs/php_codesniffer",
             "version": "3.6.0",
@@ -6386,16 +6251,16 @@
         },
         {
             "name": "symfony/config",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "3817662ada105c8c4d1afdb4ec003003efd1d8d8"
+                "reference": "8dfa5f8adea9cd5155920069224beb04f11d6b7e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/3817662ada105c8c4d1afdb4ec003003efd1d8d8",
-                "reference": "3817662ada105c8c4d1afdb4ec003003efd1d8d8",
+                "url": "https://api.github.com/repos/symfony/config/zipball/8dfa5f8adea9cd5155920069224beb04f11d6b7e",
+                "reference": "8dfa5f8adea9cd5155920069224beb04f11d6b7e",
                 "shasum": ""
             },
             "require": {
@@ -6444,7 +6309,7 @@
             "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/config/tree/v5.2.7"
+                "source": "https://github.com/symfony/config/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -6460,20 +6325,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-07T16:07:52+00:00"
+            "time": "2021-05-07T13:41:16+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "90374b8ed059325b49a29b55b3f8bb4062c87629"
+                "reference": "864568fdc0208b3eba3638b6000b69d2386e6768"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/90374b8ed059325b49a29b55b3f8bb4062c87629",
-                "reference": "90374b8ed059325b49a29b55b3f8bb4062c87629",
+                "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768",
+                "reference": "864568fdc0208b3eba3638b6000b69d2386e6768",
                 "shasum": ""
             },
             "require": {
@@ -6541,7 +6406,7 @@
                 "terminal"
             ],
             "support": {
-                "source": "https://github.com/symfony/console/tree/v5.2.7"
+                "source": "https://github.com/symfony/console/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -6557,20 +6422,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-19T14:07:32+00:00"
+            "time": "2021-05-11T15:45:21+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "6ca378b99e3c9ba6127eb43b68389fb2b7348577"
+                "reference": "024e929da5a994cbab0ce2291d332f7edf926acf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6ca378b99e3c9ba6127eb43b68389fb2b7348577",
-                "reference": "6ca378b99e3c9ba6127eb43b68389fb2b7348577",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/024e929da5a994cbab0ce2291d332f7edf926acf",
+                "reference": "024e929da5a994cbab0ce2291d332f7edf926acf",
                 "shasum": ""
             },
             "require": {
@@ -6628,7 +6493,7 @@
             "description": "Allows you to standardize and centralize the way objects are constructed in your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dependency-injection/tree/v5.2.7"
+                "source": "https://github.com/symfony/dependency-injection/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -6644,7 +6509,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-24T14:32:26+00:00"
+            "time": "2021-05-11T16:07:35+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
@@ -6715,16 +6580,16 @@
         },
         {
             "name": "symfony/error-handler",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "ea3ddbf67615e883ca7c33a4de61213789846782"
+                "reference": "1416bc16317a8188aabde251afef7618bf4687ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/ea3ddbf67615e883ca7c33a4de61213789846782",
-                "reference": "ea3ddbf67615e883ca7c33a4de61213789846782",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/1416bc16317a8188aabde251afef7618bf4687ac",
+                "reference": "1416bc16317a8188aabde251afef7618bf4687ac",
                 "shasum": ""
             },
             "require": {
@@ -6764,7 +6629,7 @@
             "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/error-handler/tree/v5.2.7"
+                "source": "https://github.com/symfony/error-handler/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -6780,7 +6645,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-07T15:57:33+00:00"
+            "time": "2021-05-07T13:42:21+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
@@ -7010,16 +6875,16 @@
         },
         {
             "name": "symfony/finder",
-            "version": "v5.2.4",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "0d639a0943822626290d169965804f79400e6a04"
+                "reference": "eccb8be70d7a6a2230d05f6ecede40f3fdd9e252"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04",
-                "reference": "0d639a0943822626290d169965804f79400e6a04",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/eccb8be70d7a6a2230d05f6ecede40f3fdd9e252",
+                "reference": "eccb8be70d7a6a2230d05f6ecede40f3fdd9e252",
                 "shasum": ""
             },
             "require": {
@@ -7051,7 +6916,7 @@
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v5.2.4"
+                "source": "https://github.com/symfony/finder/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -7067,7 +6932,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-02-15T18:55:04+00:00"
+            "time": "2021-05-10T14:39:23+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
@@ -7149,16 +7014,16 @@
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f"
+                "reference": "e8fbbab7c4a71592985019477532629cb2e142dc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f",
-                "reference": "a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e8fbbab7c4a71592985019477532629cb2e142dc",
+                "reference": "e8fbbab7c4a71592985019477532629cb2e142dc",
                 "shasum": ""
             },
             "require": {
@@ -7202,7 +7067,7 @@
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-foundation/tree/v5.2.7"
+                "source": "https://github.com/symfony/http-foundation/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -7218,20 +7083,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-01T13:46:24+00:00"
+            "time": "2021-05-07T13:41:16+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "1e9f6879f070f718e0055fbac232a56f67b8b6bd"
+                "reference": "c3cb71ee7e2d3eae5fe1001f81780d6a49b37937"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1e9f6879f070f718e0055fbac232a56f67b8b6bd",
-                "reference": "1e9f6879f070f718e0055fbac232a56f67b8b6bd",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c3cb71ee7e2d3eae5fe1001f81780d6a49b37937",
+                "reference": "c3cb71ee7e2d3eae5fe1001f81780d6a49b37937",
                 "shasum": ""
             },
             "require": {
@@ -7314,7 +7179,7 @@
             "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-kernel/tree/v5.2.7"
+                "source": "https://github.com/symfony/http-kernel/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -7330,7 +7195,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-01T14:53:15+00:00"
+            "time": "2021-05-12T13:27:53+00:00"
         },
         {
             "name": "symfony/polyfill-intl-grapheme",
@@ -7719,16 +7584,16 @@
         },
         {
             "name": "symfony/string",
-            "version": "v5.2.6",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/string.git",
-                "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572"
+                "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
-                "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
+                "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db",
+                "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db",
                 "shasum": ""
             },
             "require": {
@@ -7782,7 +7647,7 @@
                 "utf8"
             ],
             "support": {
-                "source": "https://github.com/symfony/string/tree/v5.2.6"
+                "source": "https://github.com/symfony/string/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -7798,20 +7663,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-03-17T17:12:15+00:00"
+            "time": "2021-05-10T14:56:10+00:00"
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v5.2.7",
+            "version": "v5.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "27cb9f7cfa3853c736425c7233a8f68814b19636"
+                "reference": "d693200a73fae179d27f8f1b16b4faf3e8569eba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/27cb9f7cfa3853c736425c7233a8f68814b19636",
-                "reference": "27cb9f7cfa3853c736425c7233a8f68814b19636",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d693200a73fae179d27f8f1b16b4faf3e8569eba",
+                "reference": "d693200a73fae179d27f8f1b16b4faf3e8569eba",
                 "shasum": ""
             },
             "require": {
@@ -7870,7 +7735,7 @@
                 "dump"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-dumper/tree/v5.2.7"
+                "source": "https://github.com/symfony/var-dumper/tree/v5.2.8"
             },
             "funding": [
                 {
@@ -7886,34 +7751,34 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-19T14:07:32+00:00"
+            "time": "2021-05-07T13:42:21+00:00"
         },
         {
             "name": "symplify/astral",
-            "version": "v9.3.4",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/astral.git",
-                "reference": "2ec71b4aad8995c526eee88c3d040bd7a03523e7"
+                "reference": "4934e1f0fef054051441db7ecaa267bd80231e4f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/astral/zipball/2ec71b4aad8995c526eee88c3d040bd7a03523e7",
-                "reference": "2ec71b4aad8995c526eee88c3d040bd7a03523e7",
+                "url": "https://api.github.com/repos/symplify/astral/zipball/4934e1f0fef054051441db7ecaa267bd80231e4f",
+                "reference": "4934e1f0fef054051441db7ecaa267bd80231e4f",
                 "shasum": ""
             },
             "require": {
                 "nette/utils": "^3.2",
-                "nikic/php-parser": "4.10.4",
+                "nikic/php-parser": "4.10.5",
                 "php": ">=7.3",
                 "symfony/dependency-injection": "^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/autowire-array-parameter": "^9.3.4",
-                "symplify/package-builder": "^9.3.4"
+                "symplify/autowire-array-parameter": "^9.3.11",
+                "symplify/package-builder": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5",
-                "symplify/easy-testing": "^9.3.4"
+                "symplify/easy-testing": "^9.3.11"
             },
             "type": "library",
             "extra": {
@@ -7932,7 +7797,7 @@
             ],
             "description": "Toolking for smart daily work with AST",
             "support": {
-                "source": "https://github.com/symplify/astral/tree/v9.3.4"
+                "source": "https://github.com/symplify/astral/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -7944,27 +7809,27 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T13:35:47+00:00"
+            "time": "2021-05-13T11:33:59+00:00"
         },
         {
             "name": "symplify/autowire-array-parameter",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/autowire-array-parameter.git",
-                "reference": "00044807ab4b8ae7df610f80d54360a49b253146"
+                "reference": "269fcb76ed64ca1e9dd0abe6c13273be8dfba64c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/autowire-array-parameter/zipball/00044807ab4b8ae7df610f80d54360a49b253146",
-                "reference": "00044807ab4b8ae7df610f80d54360a49b253146",
+                "url": "https://api.github.com/repos/symplify/autowire-array-parameter/zipball/269fcb76ed64ca1e9dd0abe6c13273be8dfba64c",
+                "reference": "269fcb76ed64ca1e9dd0abe6c13273be8dfba64c",
                 "shasum": ""
             },
             "require": {
                 "nette/utils": "^3.2",
                 "php": ">=7.3",
                 "symfony/dependency-injection": "^5.2",
-                "symplify/package-builder": "^9.3.6"
+                "symplify/package-builder": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -7986,7 +7851,7 @@
             ],
             "description": "Autowire array parameters for your Symfony applications",
             "support": {
-                "source": "https://github.com/symplify/autowire-array-parameter/tree/v9.3.6"
+                "source": "https://github.com/symplify/autowire-array-parameter/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -7998,20 +7863,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:44:31+00:00"
+            "time": "2021-05-13T11:33:56+00:00"
         },
         {
             "name": "symplify/composer-json-manipulator",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/composer-json-manipulator.git",
-                "reference": "c5218020d999c775f5bcc625c7e3d46f7bd21e81"
+                "reference": "87a675a761aa5df90f5d5cf2c7d49d1e7e7a91be"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/composer-json-manipulator/zipball/c5218020d999c775f5bcc625c7e3d46f7bd21e81",
-                "reference": "c5218020d999c775f5bcc625c7e3d46f7bd21e81",
+                "url": "https://api.github.com/repos/symplify/composer-json-manipulator/zipball/87a675a761aa5df90f5d5cf2c7d49d1e7e7a91be",
+                "reference": "87a675a761aa5df90f5d5cf2c7d49d1e7e7a91be",
                 "shasum": ""
             },
             "require": {
@@ -8021,8 +7886,8 @@
                 "symfony/dependency-injection": "^5.2",
                 "symfony/filesystem": "^4.4|^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/package-builder": "^9.3.6",
-                "symplify/smart-file-system": "^9.3.6"
+                "symplify/package-builder": "^9.3.11",
+                "symplify/smart-file-system": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8044,7 +7909,7 @@
             ],
             "description": "Package to load, merge and save composer.json file(s)",
             "support": {
-                "source": "https://github.com/symplify/composer-json-manipulator/tree/v9.3.6"
+                "source": "https://github.com/symplify/composer-json-manipulator/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8056,20 +7921,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:44:23+00:00"
+            "time": "2021-05-13T11:33:58+00:00"
         },
         {
             "name": "symplify/console-color-diff",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/console-color-diff.git",
-                "reference": "8f25607a0b05754bbf6ac8f06d6662adf201e1cb"
+                "reference": "7b97c8e124eb790a24b2e3319e3795abeffef557"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/console-color-diff/zipball/8f25607a0b05754bbf6ac8f06d6662adf201e1cb",
-                "reference": "8f25607a0b05754bbf6ac8f06d6662adf201e1cb",
+                "url": "https://api.github.com/repos/symplify/console-color-diff/zipball/7b97c8e124eb790a24b2e3319e3795abeffef557",
+                "reference": "7b97c8e124eb790a24b2e3319e3795abeffef557",
                 "shasum": ""
             },
             "require": {
@@ -8079,7 +7944,7 @@
                 "symfony/console": "^4.4|^5.2",
                 "symfony/dependency-injection": "^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/package-builder": "^9.3.6"
+                "symplify/package-builder": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8101,7 +7966,7 @@
             ],
             "description": "Package to print diffs in console with colors",
             "support": {
-                "source": "https://github.com/symplify/console-color-diff/tree/v9.3.6"
+                "source": "https://github.com/symplify/console-color-diff/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8113,32 +7978,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:44:55+00:00"
+            "time": "2021-05-13T11:34:04+00:00"
         },
         {
             "name": "symplify/console-package-builder",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/console-package-builder.git",
-                "reference": "4a7d5d412e2bdf805c4603a02c69ef16183521c9"
+                "reference": "4fd36633c3607f74a21eee5bd32c00f24130261f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/console-package-builder/zipball/4a7d5d412e2bdf805c4603a02c69ef16183521c9",
-                "reference": "4a7d5d412e2bdf805c4603a02c69ef16183521c9",
+                "url": "https://api.github.com/repos/symplify/console-package-builder/zipball/4fd36633c3607f74a21eee5bd32c00f24130261f",
+                "reference": "4fd36633c3607f74a21eee5bd32c00f24130261f",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.3",
                 "symfony/console": "^4.4|^5.2",
                 "symfony/dependency-injection": "^5.2",
-                "symplify/symplify-kernel": "^9.3.6"
+                "symplify/symplify-kernel": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/package-builder": "^9.3.6"
+                "symplify/package-builder": "^9.3.11"
             },
             "type": "library",
             "extra": {
@@ -8157,22 +8022,22 @@
             ],
             "description": "Package to speed up building command line applications",
             "support": {
-                "source": "https://github.com/symplify/console-package-builder/tree/v9.3.6"
+                "source": "https://github.com/symplify/console-package-builder/tree/v9.3.11"
             },
-            "time": "2021-05-11T22:45:10+00:00"
+            "time": "2021-05-13T11:33:59+00:00"
         },
         {
             "name": "symplify/easy-testing",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/easy-testing.git",
-                "reference": "f8b617e6c0df1c76acb3150af40bd256bf2c0aba"
+                "reference": "5662ac6e55dca7d7ae7df0b47e84a165573359ed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/easy-testing/zipball/f8b617e6c0df1c76acb3150af40bd256bf2c0aba",
-                "reference": "f8b617e6c0df1c76acb3150af40bd256bf2c0aba",
+                "url": "https://api.github.com/repos/symplify/easy-testing/zipball/5662ac6e55dca7d7ae7df0b47e84a165573359ed",
+                "reference": "5662ac6e55dca7d7ae7df0b47e84a165573359ed",
                 "shasum": ""
             },
             "require": {
@@ -8182,10 +8047,10 @@
                 "symfony/dependency-injection": "^5.2",
                 "symfony/finder": "^4.4|^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/console-package-builder": "^9.3.6",
-                "symplify/package-builder": "^9.3.6",
-                "symplify/smart-file-system": "^9.3.6",
-                "symplify/symplify-kernel": "^9.3.6"
+                "symplify/console-package-builder": "^9.3.11",
+                "symplify/package-builder": "^9.3.11",
+                "symplify/smart-file-system": "^9.3.11",
+                "symplify/symplify-kernel": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8210,7 +8075,7 @@
             ],
             "description": "Testing made easy",
             "support": {
-                "source": "https://github.com/symplify/easy-testing/tree/v9.3.6"
+                "source": "https://github.com/symplify/easy-testing/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8222,20 +8087,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:44:47+00:00"
+            "time": "2021-05-13T11:34:12+00:00"
         },
         {
             "name": "symplify/package-builder",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/package-builder.git",
-                "reference": "9d572946f2483150aae1f7b7d5afe75c5206ffda"
+                "reference": "479752e9b19efbd0470aba8e92b9d6a01722430b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/package-builder/zipball/9d572946f2483150aae1f7b7d5afe75c5206ffda",
-                "reference": "9d572946f2483150aae1f7b7d5afe75c5206ffda",
+                "url": "https://api.github.com/repos/symplify/package-builder/zipball/479752e9b19efbd0470aba8e92b9d6a01722430b",
+                "reference": "479752e9b19efbd0470aba8e92b9d6a01722430b",
                 "shasum": ""
             },
             "require": {
@@ -8247,8 +8112,8 @@
                 "symfony/dependency-injection": "^5.2",
                 "symfony/finder": "^4.4|^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/easy-testing": "^9.3.6",
-                "symplify/symplify-kernel": "^9.3.6"
+                "symplify/easy-testing": "^9.3.11",
+                "symplify/symplify-kernel": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8270,7 +8135,7 @@
             ],
             "description": "Dependency Injection, Console and Kernel toolkit for Symplify packages.",
             "support": {
-                "source": "https://github.com/symplify/package-builder/tree/v9.3.6"
+                "source": "https://github.com/symplify/package-builder/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8282,33 +8147,33 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:45:02+00:00"
+            "time": "2021-05-13T11:34:38+00:00"
         },
         {
             "name": "symplify/phpstan-rules",
-            "version": "v9.3.4",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/phpstan-rules.git",
-                "reference": "4856ef701dcb0823f7455441201f11f378b99c97"
+                "reference": "e556dc413fc10804706d57b324e9aa14513c5f57"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/phpstan-rules/zipball/4856ef701dcb0823f7455441201f11f378b99c97",
-                "reference": "4856ef701dcb0823f7455441201f11f378b99c97",
+                "url": "https://api.github.com/repos/symplify/phpstan-rules/zipball/e556dc413fc10804706d57b324e9aa14513c5f57",
+                "reference": "e556dc413fc10804706d57b324e9aa14513c5f57",
                 "shasum": ""
             },
             "require": {
                 "nette/utils": "^3.2",
-                "nikic/php-parser": "4.10.4",
+                "nikic/php-parser": "4.10.5",
                 "php": ">=7.3",
                 "phpstan/phpdoc-parser": "^0.5",
-                "phpstan/phpstan": "0.12.85",
-                "symplify/astral": "^9.3.4",
-                "symplify/composer-json-manipulator": "^9.3.4",
-                "symplify/package-builder": "^9.3.4",
-                "symplify/rule-doc-generator-contracts": "^9.3.4",
-                "symplify/smart-file-system": "^9.3.4",
+                "phpstan/phpstan": "0.12.86",
+                "symplify/astral": "^9.3.11",
+                "symplify/composer-json-manipulator": "^9.3.11",
+                "symplify/package-builder": "^9.3.11",
+                "symplify/rule-doc-generator-contracts": "^9.3.11",
+                "symplify/smart-file-system": "^9.3.11",
                 "webmozart/assert": "^1.9"
             },
             "require-dev": {
@@ -8316,9 +8181,9 @@
                 "nette/forms": "^3.1",
                 "phpunit/phpunit": "^9.5",
                 "symfony/framework-bundle": "^4.4|^5.2",
-                "symplify/easy-testing": "^9.3.4",
-                "symplify/phpstan-extensions": "^9.3.4",
-                "symplify/rule-doc-generator": "^9.3.4"
+                "symplify/easy-testing": "^9.3.11",
+                "symplify/phpstan-extensions": "^9.3.11",
+                "symplify/rule-doc-generator": "^9.3.11"
             },
             "type": "phpstan-extension",
             "extra": {
@@ -8346,7 +8211,7 @@
             ],
             "description": "Set of Symplify rules for PHPStan",
             "support": {
-                "source": "https://github.com/symplify/phpstan-rules/tree/v9.3.4"
+                "source": "https://github.com/symplify/phpstan-rules/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8358,11 +8223,11 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T13:36:44+00:00"
+            "time": "2021-05-13T11:34:44+00:00"
         },
         {
             "name": "symplify/rule-doc-generator-contracts",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/rule-doc-generator-contracts.git",
@@ -8395,7 +8260,7 @@
             ],
             "description": "Contracts for production code of RuleDocGenerator",
             "support": {
-                "source": "https://github.com/symplify/rule-doc-generator-contracts/tree/v9.3.6"
+                "source": "https://github.com/symplify/rule-doc-generator-contracts/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8411,16 +8276,16 @@
         },
         {
             "name": "symplify/set-config-resolver",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/set-config-resolver.git",
-                "reference": "7da336c9144b3bfea439e8335680c2abe57cdca1"
+                "reference": "cced883469d32b45f83471c17443faf18a3c2e75"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/set-config-resolver/zipball/7da336c9144b3bfea439e8335680c2abe57cdca1",
-                "reference": "7da336c9144b3bfea439e8335680c2abe57cdca1",
+                "url": "https://api.github.com/repos/symplify/set-config-resolver/zipball/cced883469d32b45f83471c17443faf18a3c2e75",
+                "reference": "cced883469d32b45f83471c17443faf18a3c2e75",
                 "shasum": ""
             },
             "require": {
@@ -8431,8 +8296,8 @@
                 "symfony/dependency-injection": "^5.2",
                 "symfony/filesystem": "^4.4|^5.2",
                 "symfony/finder": "^4.4|^5.2",
-                "symplify/smart-file-system": "^9.3.6",
-                "symplify/symplify-kernel": "^9.3.6"
+                "symplify/smart-file-system": "^9.3.11",
+                "symplify/symplify-kernel": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8454,7 +8319,7 @@
             ],
             "description": "Resolve config and sets from configs and cli opptions for CLI applications",
             "support": {
-                "source": "https://github.com/symplify/set-config-resolver/tree/v9.3.6"
+                "source": "https://github.com/symplify/set-config-resolver/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8466,20 +8331,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:45:21+00:00"
+            "time": "2021-05-13T11:34:45+00:00"
         },
         {
             "name": "symplify/simple-php-doc-parser",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/simple-php-doc-parser.git",
-                "reference": "cea820fa5a20d2fa8a240e2e0fcd7b063ce21b9d"
+                "reference": "e70450dfaa94db70e34a4d2252891a43bc57f956"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/simple-php-doc-parser/zipball/cea820fa5a20d2fa8a240e2e0fcd7b063ce21b9d",
-                "reference": "cea820fa5a20d2fa8a240e2e0fcd7b063ce21b9d",
+                "url": "https://api.github.com/repos/symplify/simple-php-doc-parser/zipball/e70450dfaa94db70e34a4d2252891a43bc57f956",
+                "reference": "e70450dfaa94db70e34a4d2252891a43bc57f956",
                 "shasum": ""
             },
             "require": {
@@ -8488,11 +8353,11 @@
                 "symfony/config": "^4.4|^5.2",
                 "symfony/dependency-injection": "^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/package-builder": "^9.3.6"
+                "symplify/package-builder": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5",
-                "symplify/easy-testing": "^9.3.6"
+                "symplify/easy-testing": "^9.3.11"
             },
             "type": "library",
             "extra": {
@@ -8511,7 +8376,7 @@
             ],
             "description": "Service integration of phpstan/phpdoc-parser, with few extra goodies for practical simple use",
             "support": {
-                "source": "https://github.com/symplify/simple-php-doc-parser/tree/v9.3.6"
+                "source": "https://github.com/symplify/simple-php-doc-parser/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8523,20 +8388,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:45:32+00:00"
+            "time": "2021-05-13T11:34:46+00:00"
         },
         {
             "name": "symplify/skipper",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/skipper.git",
-                "reference": "67417fd44b4ddc3796df5c699ecd076b8c71927e"
+                "reference": "3b446ce9c78f0d455788d9bfd92915eac6da1054"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/skipper/zipball/67417fd44b4ddc3796df5c699ecd076b8c71927e",
-                "reference": "67417fd44b4ddc3796df5c699ecd076b8c71927e",
+                "url": "https://api.github.com/repos/symplify/skipper/zipball/3b446ce9c78f0d455788d9bfd92915eac6da1054",
+                "reference": "3b446ce9c78f0d455788d9bfd92915eac6da1054",
                 "shasum": ""
             },
             "require": {
@@ -8546,9 +8411,9 @@
                 "symfony/dependency-injection": "^5.2",
                 "symfony/filesystem": "^4.4|^5.2",
                 "symfony/finder": "^4.4|^5.2",
-                "symplify/package-builder": "^9.3.6",
-                "symplify/smart-file-system": "^9.3.6",
-                "symplify/symplify-kernel": "^9.3.6"
+                "symplify/package-builder": "^9.3.11",
+                "symplify/smart-file-system": "^9.3.11",
+                "symplify/symplify-kernel": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8570,7 +8435,7 @@
             ],
             "description": "Skip files by rule class, directory, file or fnmatch",
             "support": {
-                "source": "https://github.com/symplify/skipper/tree/v9.3.6"
+                "source": "https://github.com/symplify/skipper/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8582,11 +8447,11 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-11T22:45:36+00:00"
+            "time": "2021-05-13T11:34:52+00:00"
         },
         {
             "name": "symplify/smart-file-system",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/smart-file-system.git",
@@ -8625,7 +8490,7 @@
             ],
             "description": "Sanitized FileInfo with safe getRealPath() and other handy methods",
             "support": {
-                "source": "https://github.com/symplify/smart-file-system/tree/v9.3.6"
+                "source": "https://github.com/symplify/smart-file-system/tree/v9.3.11"
             },
             "funding": [
                 {
@@ -8641,23 +8506,23 @@
         },
         {
             "name": "symplify/symfony-php-config",
-            "version": "v9.3.5",
+            "version": "v9.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/symfony-php-config.git",
-                "reference": "9dc9a4010871f822394628487cbc5721c83fe3de"
+                "reference": "91f29a210de56fb208da5e18c8573937bf7bc37e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/symfony-php-config/zipball/9dc9a4010871f822394628487cbc5721c83fe3de",
-                "reference": "9dc9a4010871f822394628487cbc5721c83fe3de",
+                "url": "https://api.github.com/repos/symplify/symfony-php-config/zipball/91f29a210de56fb208da5e18c8573937bf7bc37e",
+                "reference": "91f29a210de56fb208da5e18c8573937bf7bc37e",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.3",
                 "symfony/dependency-injection": "^5.2",
-                "symplify/package-builder": "^9.3.5",
-                "symplify/symplify-kernel": "^9.3.5"
+                "symplify/package-builder": "^9.3.10",
+                "symplify/symplify-kernel": "^9.3.10"
             },
             "require-dev": {
                 "phpstan/phpstan": "0.12.86",
@@ -8681,22 +8546,22 @@
             ],
             "description": "Tools that easy work with Symfony PHP Configs",
             "support": {
-                "source": "https://github.com/symplify/symfony-php-config/tree/v9.3.5"
+                "source": "https://github.com/symplify/symfony-php-config/tree/v9.3.10"
             },
-            "time": "2021-05-11T14:11:22+00:00"
+            "time": "2021-05-12T14:29:08+00:00"
         },
         {
             "name": "symplify/symplify-kernel",
-            "version": "v9.3.6",
+            "version": "v9.3.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symplify/symplify-kernel.git",
-                "reference": "ba7f3b8ee3ce80a8543f248495a0999e0ea6af7a"
+                "reference": "d79a26c90ebd292d8b474dbee26b602379be3f66"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symplify/symplify-kernel/zipball/ba7f3b8ee3ce80a8543f248495a0999e0ea6af7a",
-                "reference": "ba7f3b8ee3ce80a8543f248495a0999e0ea6af7a",
+                "url": "https://api.github.com/repos/symplify/symplify-kernel/zipball/d79a26c90ebd292d8b474dbee26b602379be3f66",
+                "reference": "d79a26c90ebd292d8b474dbee26b602379be3f66",
                 "shasum": ""
             },
             "require": {
@@ -8704,10 +8569,10 @@
                 "symfony/console": "^4.4|^5.2",
                 "symfony/dependency-injection": "^5.2",
                 "symfony/http-kernel": "^4.4|^5.2",
-                "symplify/autowire-array-parameter": "^9.3.6",
-                "symplify/composer-json-manipulator": "^9.3.6",
-                "symplify/package-builder": "^9.3.6",
-                "symplify/smart-file-system": "^9.3.6"
+                "symplify/autowire-array-parameter": "^9.3.11",
+                "symplify/composer-json-manipulator": "^9.3.11",
+                "symplify/package-builder": "^9.3.11",
+                "symplify/smart-file-system": "^9.3.11"
             },
             "require-dev": {
                 "phpunit/phpunit": "^9.5"
@@ -8729,9 +8594,9 @@
             ],
             "description": "Internal Kernel for Symplify packages",
             "support": {
-                "source": "https://github.com/symplify/symplify-kernel/tree/v9.3.6"
+                "source": "https://github.com/symplify/symplify-kernel/tree/v9.3.11"
             },
-            "time": "2021-05-11T22:45:48+00:00"
+            "time": "2021-05-13T11:35:17+00:00"
         },
         {
             "name": "theseer/tokenizer",
@@ -8927,7 +8792,7 @@
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
-        "php": "^7.3||^8.0"
+        "php": "^8.0"
     },
     "platform-dev": [],
     "plugin-api-version": "2.0.0"
diff --git a/phpstan.neon b/phpstan.neon
index d942305775..e90f864fe3 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,9 +1,9 @@
 parameters:
     tmpDir: build/phpstan
-    level: 5
+    level: 6
     paths:
         - app
-        - tests
+        # - tests
     bootstrapFiles:
         - vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php
     scanDirectories:
@@ -17,10 +17,8 @@ parameters:
         - app/Libraries/Analytics/Config/Routes.php
         - app/Views/*
     ignoreErrors:
-        - '#Access to property [\$a-zA-Z]+ on an unknown class Myth\\Auth.*#' # TODO: remove when https://github.com/lonnieezell/myth-auth/pull/347 is merged 
-        - '#Call to method [\a-zA-Z]+\(\) on an unknown class Myth\\Auth.*#' # to remove as well
-        - '#^Access to an undefined property ActivityPub\\Entities\\Actor#'
-        - '#^Access to an undefined property ActivityPub\\Entities\\Note#'
+        - '#This property type might be inlined to PHP. Do you have confidence it is correct\? Put it here#'
+        - '#Function \"preg_.*\(\)\" cannot be used/left in the code#'
         - '#.* is forbidden to use#'
         - '#^Cognitive complexity for#'
         - '#^Class cognitive complexity is#'
diff --git a/public/index.php b/public/index.php
index b48871b015..a0451b7dfd 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,7 +1,9 @@
 <?php
 
+use Config\Paths;
+
 // Valid PHP Version?
-$minPHPVersion = '7.3';
+$minPHPVersion = '8.0';
 if (version_compare(PHP_VERSION, $minPHPVersion, '<')) {
     die(
         "Your PHP version must be {$minPHPVersion} or higher to run CodeIgniter. Current version: " .
@@ -31,7 +33,7 @@ require realpath(FCPATH . '../app/Config/Paths.php') ?:
     FCPATH . '../app/Config/Paths.php';
 // ^^^ Change this if you move your application folder
 
-$paths = new Config\Paths();
+$paths = new Paths();
 
 // Location of the framework bootstrap file.
 $bootstrap =
diff --git a/rector.php b/rector.php
index 9403850fbd..e3ba7ecf78 100644
--- a/rector.php
+++ b/rector.php
@@ -11,6 +11,7 @@ use Rector\Core\ValueObject\PhpVersion;
 use Rector\EarlyReturn\Rector\If_\ChangeOrIfContinueToMultiContinueRector;
 use Rector\EarlyReturn\Rector\If_\ChangeOrIfReturnToEarlyReturnRector;
 use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
+use Rector\Php80\Rector\ClassMethod\OptionalParametersAfterRequiredRector;
 use Rector\Set\ValueObject\SetList;
 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
 
@@ -24,8 +25,13 @@ return static function (ContainerConfigurator $containerConfigurator): void {
         __DIR__ . '/public',
     ]);
 
+    // do you need to include constants, class aliases or custom autoloader? files listed will be executed
+    $parameters->set(Option::BOOTSTRAP_FILES, [
+        __DIR__ . '/vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php',
+    ]);
+
     // Define what rule sets will be applied
-    $containerConfigurator->import(SetList::PHP_73);
+    $containerConfigurator->import(SetList::PHP_80);
     $containerConfigurator->import(SetList::TYPE_DECLARATION);
     $containerConfigurator->import(SetList::TYPE_DECLARATION_STRICT);
     $containerConfigurator->import(SetList::CODE_QUALITY);
@@ -36,8 +42,8 @@ return static function (ContainerConfigurator $containerConfigurator): void {
 
     // auto import fully qualified class names
     $parameters->set(Option::AUTO_IMPORT_NAMES, true);
-    $parameters->set(Option::ENABLE_CACHE, true);
-    $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_73);
+    // $parameters->set(Option::ENABLE_CACHE, true);
+    $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80);
 
     $parameters->set(Option::SKIP, [
         // skip specific generated files
@@ -54,8 +60,17 @@ return static function (ContainerConfigurator $containerConfigurator): void {
         StringClassNameToClassConstantRector::class => [
             __DIR__ . '/app/Language/*',
         ],
+        OptionalParametersAfterRequiredRector::class => [
+            __DIR__ . '/app/Validation',
+        ],
     ]);
 
+    // Path to phpstan with extensions, that PHPSTan in Rector uses to determine types
+    $parameters->set(
+        Option::PHPSTAN_FOR_RECTOR_PATH,
+        __DIR__ . '/phpstan.neon',
+    );
+
     $services = $containerConfigurator->services();
     $services->set(ConsistentPregDelimiterRector::class)->call('configure', [
         [
diff --git a/spark b/spark
index 83a5cc25c3..6446786fd6 100644
--- a/spark
+++ b/spark
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 // Valid PHP Version?
-$minPHPVersion = "7.3";
+$minPHPVersion = "8.0";
 if (version_compare(PHP_VERSION, $minPHPVersion, "<")) {
     die(
         "Your PHP version must be {$minPHPVersion} or higher to run CodeIgniter. Current version: " .
diff --git a/tests/unit/HealthTest.php b/tests/unit/HealthTest.php
index 30ae5219ab..28b64065e1 100644
--- a/tests/unit/HealthTest.php
+++ b/tests/unit/HealthTest.php
@@ -2,11 +2,12 @@
 
 namespace Tests\Unit;
 
+use CodeIgniter\Test\CIUnitTestCase;
 use Config\App;
 use Config\Services;
 use Tests\Support\Libraries\ConfigReader;
 
-class HealthTest extends \CodeIgniter\Test\CIUnitTestCase
+class HealthTest extends CIUnitTestCase
 {
     public function setUp(): void
     {
@@ -28,7 +29,7 @@ class HealthTest extends \CodeIgniter\Test\CIUnitTestCase
         // Check the baseURL in .env
         if (is_file(HOMEPATH . '.env')) {
             $env = (bool) preg_grep(
-                '/^app\.baseURL = ./',
+                '~^app\.baseURL = .~',
                 file(HOMEPATH . '.env'),
             );
         }
-- 
GitLab