From 54b84f96843af13f579fea49102c8c2ef81b0a54 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Tue, 20 Apr 2021 13:43:38 +0000
Subject: [PATCH] perf(cache): update CI4 to use cache's deleteMatching method

add missing locale to category_options cache name
---
 .devcontainer/devcontainer.json         |  27 ++--
 INSTALL.md                              |   9 +-
 app/Authorization/PermissionModel.php   |   7 +-
 app/Config/App.php                      |  27 +++-
 app/Config/Cookie.php                   | 119 ++++++++++++++
 app/Config/Routes.php                   |   2 +-
 app/Controllers/Admin/EpisodePerson.php |  10 +-
 app/Controllers/Admin/PodcastPerson.php |  10 +-
 app/Controllers/Episode.php             |   4 +-
 app/Controllers/Feed.php                |   8 +-
 app/Controllers/Page.php                |  31 ++--
 app/Controllers/Podcast.php             |  21 +--
 app/Entities/Episode.php                |   2 +-
 app/Entities/Podcast.php                |   2 +-
 app/Language/en/Install.php             |   2 +-
 app/Language/en/Validation.php          |   2 -
 app/Language/fr/Install.php             |   2 +-
 app/Language/fr/Validation.php          |   2 -
 app/Models/CategoryModel.php            |  26 ++--
 app/Models/EpisodeModel.php             | 196 ++++--------------------
 app/Models/EpisodePersonModel.php       |  44 ++----
 app/Models/PageModel.php                |  44 +-----
 app/Models/PersonModel.php              |  23 ++-
 app/Models/PlatformModel.php            |  76 ++-------
 app/Models/PodcastModel.php             | 180 ++++++++++++++--------
 app/Models/PodcastPersonModel.php       |  23 ++-
 app/Models/SoundbiteModel.php           |  26 ++--
 app/Models/UserModel.php                |   5 +-
 app/Validation/Rules.php                |  20 ---
 app/Views/install/cache_config.php      |   2 +-
 composer.lock                           |  88 +++++------
 31 files changed, 483 insertions(+), 557 deletions(-)
 create mode 100644 app/Config/Cookie.php

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 4422b82f12..45fcd1c022 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -13,17 +13,18 @@
     "color-highlight.markerType": "dot-before"
   },
   "extensions": [
-    "mikestead.dotenv",
-    "bmewburn.vscode-intelephense-client",
-    "streetsidesoftware.code-spell-checker",
-    "naumovs.color-highlight",
-    "heybourn.headwind",
-    "wayou.vscode-todo-highlight",
-    "esbenp.prettier-vscode",
-    "bradlc.vscode-tailwindcss",
-    "jamesbirtles.svelte-vscode",
-    "dbaeumer.vscode-eslint",
-    "stylelint.vscode-stylelint",
-    "wongjn.php-sniffer"
-  ]
+	"mikestead.dotenv",
+	"bmewburn.vscode-intelephense-client",
+	"streetsidesoftware.code-spell-checker",
+	"naumovs.color-highlight",
+	"heybourn.headwind",
+	"wayou.vscode-todo-highlight",
+	"esbenp.prettier-vscode",
+	"bradlc.vscode-tailwindcss",
+	"jamesbirtles.svelte-vscode",
+	"dbaeumer.vscode-eslint",
+	"stylelint.vscode-stylelint",
+	"wongjn.php-sniffer",
+	"eamodio.gitlens"
+]
 }
diff --git a/INSTALL.md b/INSTALL.md
index efc5f99592..7465139583 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -50,15 +50,16 @@ Before uploading Castopod files to your web server:
 
 PHP version 7.3 or higher is required, with the following extensions installed:
 
-- [intl](http://php.net/manual/en/intl.requirements.php)
-- [libcurl](http://php.net/manual/en/curl.requirements.php)
-- [mbstring](http://php.net/manual/en/mbstring.installation.php)
+- [intl](https://php.net/manual/en/intl.requirements.php)
+- [libcurl](https://php.net/manual/en/curl.requirements.php)
+- [mbstring](https://php.net/manual/en/mbstring.installation.php)
+- [gd](https://www.php.net/manual/en/image.installation.php)
 
 Additionally, make sure that the following extensions are enabled in your PHP:
 
 - json (enabled by default - don't turn it off)
 - xml (enabled by default - don't turn it off)
-- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php)
+- [mysqlnd](https://php.net/manual/en/mysqlnd.install.php)
 
 ### MySQL compatible database
 
diff --git a/app/Authorization/PermissionModel.php b/app/Authorization/PermissionModel.php
index f60fe7d66e..c15f4412b6 100644
--- a/app/Authorization/PermissionModel.php
+++ b/app/Authorization/PermissionModel.php
@@ -39,14 +39,15 @@ class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
      */
     public function getPermissionsForGroup(int $groupId): array
     {
-        if (!($found = cache("group{$groupId}_permissions"))) {
+        $cacheName = "group{$groupId}_permissions";
+        if (!($found = cache($cacheName))) {
             $groupPermissions = $this->db
                 ->table('auth_groups_permissions')
                 ->select('id, auth_permissions.name')
                 ->join(
                     'auth_permissions',
                     'auth_permissions.id = permission_id',
-                    'inner'
+                    'inner',
                 )
                 ->where('group_id', $groupId)
                 ->get()
@@ -57,7 +58,7 @@ class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
                 $found[$row->id] = strtolower($row->name);
             }
 
-            cache()->save("group{$groupId}_permissions", $found, 300);
+            cache()->save($cacheName, $found, 300);
         }
 
         return $found;
diff --git a/app/Config/App.php b/app/Config/App.php
index 5e9e88b849..75b986990b 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -253,6 +253,8 @@ class App extends BaseConfig
      * Set a cookie name prefix if you need to avoid collisions.
      *
      * @var string
+     *
+     * @deprecated use Config\Cookie::$prefix property instead.
      */
     public $cookiePrefix = '';
 
@@ -264,6 +266,8 @@ class App extends BaseConfig
      * Set to `.your-domain.com` for site-wide cookies.
      *
      * @var string
+     *
+     * @deprecated use Config\Cookie::$domain property instead.
      */
     public $cookieDomain = '';
 
@@ -275,6 +279,8 @@ class App extends BaseConfig
      * Typically will be a forward slash.
      *
      * @var string
+     *
+     * @deprecated use Config\Cookie::$path property instead.
      */
     public $cookiePath = '/';
 
@@ -286,19 +292,23 @@ class App extends BaseConfig
      * Cookie will only be set if a secure HTTPS connection exists.
      *
      * @var boolean
+     *
+     * @deprecated use Config\Cookie::$secure property instead.
      */
     public $cookieSecure = false;
 
     /**
      * --------------------------------------------------------------------------
-     * Cookie HTTP Only
+     * Cookie HttpOnly
      * --------------------------------------------------------------------------
      *
      * Cookie will only be accessible via HTTP(S) (no JavaScript).
      *
      * @var boolean
+     *
+     * @deprecated use Config\Cookie::$httponly property instead.
      */
-    public $cookieHTTPOnly = false;
+    public $cookieHTTPOnly = true;
 
     /**
      * --------------------------------------------------------------------------
@@ -311,11 +321,18 @@ class App extends BaseConfig
      * - Strict
      * - ''
      *
+     * Alternatively, you can use the constant names:
+     * - `Cookie::SAMESITE_NONE`
+     * - `Cookie::SAMESITE_LAX`
+     * - `Cookie::SAMESITE_STRICT`
+     *
      * Defaults to `Lax` for compatibility with modern browsers. Setting `''`
-     * (empty string) means no SameSite attribute will be set on cookies. If
-     * set to `None`, `$cookieSecure` must also be set.
+     * (empty string) means default SameSite attribute set by browsers (`Lax`)
+     * will be set on cookies. If set to `None`, `$cookieSecure` must also be set.
+     *
+     * @var string
      *
-     * @var string 'Lax'|'None'|'Strict'
+     * @deprecated use Config\Cookie::$samesite property instead.
      */
     public $cookieSameSite = 'Lax';
 
diff --git a/app/Config/Cookie.php b/app/Config/Cookie.php
new file mode 100644
index 0000000000..68c403e5d4
--- /dev/null
+++ b/app/Config/Cookie.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use DateTimeInterface;
+
+class Cookie extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Prefix
+     * --------------------------------------------------------------------------
+     *
+     * Set a cookie name prefix if you need to avoid collisions.
+     *
+     * @var string
+     */
+    public $prefix = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Expires Timestamp
+     * --------------------------------------------------------------------------
+     *
+     * Default expires timestamp for cookies. Setting this to `0` will mean the
+     * cookie will not have the `Expires` attribute and will behave as a session
+     * cookie.
+     *
+     * @var DateTimeInterface|integer|string
+     */
+    public $expires = 0;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Path
+     * --------------------------------------------------------------------------
+     *
+     * Typically will be a forward slash.
+     *
+     * @var string
+     */
+    public $path = '/';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Domain
+     * --------------------------------------------------------------------------
+     *
+     * Set to `.your-domain.com` for site-wide cookies.
+     *
+     * @var string
+     */
+    public $domain = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Secure
+     * --------------------------------------------------------------------------
+     *
+     * Cookie will only be set if a secure HTTPS connection exists.
+     *
+     * @var boolean
+     */
+    public $secure = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie HTTPOnly
+     * --------------------------------------------------------------------------
+     *
+     * Cookie will only be accessible via HTTP(S) (no JavaScript).
+     *
+     * @var boolean
+     */
+    public $httponly = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie SameSite
+     * --------------------------------------------------------------------------
+     *
+     * Configure cookie SameSite setting. Allowed values are:
+     * - None
+     * - Lax
+     * - Strict
+     * - ''
+     *
+     * Alternatively, you can use the constant names:
+     * - `Cookie::SAMESITE_NONE`
+     * - `Cookie::SAMESITE_LAX`
+     * - `Cookie::SAMESITE_STRICT`
+     *
+     * Defaults to `Lax` for compatibility with modern browsers. Setting `''`
+     * (empty string) means default SameSite attribute set by browsers (`Lax`)
+     * will be set on cookies. If set to `None`, `$secure` must also be set.
+     *
+     * @var string
+     */
+    public $samesite = 'Lax';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Raw
+     * --------------------------------------------------------------------------
+     *
+     * This flag allows setting a "raw" cookie, i.e., its name and value are
+     * not URL encoded using `rawurlencode()`.
+     *
+     * If this is set to `true`, cookie names should be compliant of RFC 2616's
+     * list of allowed characters.
+     *
+     * @var boolean
+     *
+     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
+     * @see https://tools.ietf.org/html/rfc2616#section-2.2
+     */
+    public $raw = false;
+}
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 3200946785..3c0064e318 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -651,7 +651,7 @@ $routes->group('@(:podcastName)', function ($routes) {
 
 // Other pages
 $routes->get('/credits', 'Page::credits', ['as' => 'credits']);
-$routes->get('/(:slug)', 'Page/$1', ['as' => 'page']);
+$routes->get('/pages/(:slug)', 'Page/$1', ['as' => 'page']);
 
 // interacting as an actor
 $routes->post('interact-as-actor', 'Auth::attemptInteractAsActor', [
diff --git a/app/Controllers/Admin/EpisodePerson.php b/app/Controllers/Admin/EpisodePerson.php
index 9d35dd9c2b..e6ccca6410 100644
--- a/app/Controllers/Admin/EpisodePerson.php
+++ b/app/Controllers/Admin/EpisodePerson.php
@@ -30,7 +30,7 @@ class EpisodePerson extends BaseController
         if (count($params) > 1) {
             if (
                 !($this->podcast = (new PodcastModel())->getPodcastById(
-                    $params[0]
+                    $params[0],
                 ))
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
@@ -61,9 +61,9 @@ class EpisodePerson extends BaseController
         $data = [
             'episode' => $this->episode,
             'podcast' => $this->podcast,
-            'episodePersons' => (new EpisodePersonModel())->getPersonsByEpisodeId(
+            'episodePersons' => (new EpisodePersonModel())->getEpisodePersons(
                 $this->podcast->id,
-                $this->episode->id
+                $this->episode->id,
             ),
             'personOptions' => (new PersonModel())->getPersonOptions(),
             'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
@@ -92,7 +92,7 @@ class EpisodePerson extends BaseController
             $this->podcast->id,
             $this->episode->id,
             $this->request->getPost('person'),
-            $this->request->getPost('person_group_role')
+            $this->request->getPost('person_group_role'),
         );
 
         return redirect()->back();
@@ -103,7 +103,7 @@ class EpisodePerson extends BaseController
         (new EpisodePersonModel())->removeEpisodePersons(
             $this->podcast->id,
             $this->episode->id,
-            $episodePersonId
+            $episodePersonId,
         );
 
         return redirect()->back();
diff --git a/app/Controllers/Admin/PodcastPerson.php b/app/Controllers/Admin/PodcastPerson.php
index 676037007d..4da46dbd83 100644
--- a/app/Controllers/Admin/PodcastPerson.php
+++ b/app/Controllers/Admin/PodcastPerson.php
@@ -24,7 +24,7 @@ class PodcastPerson extends BaseController
         if (count($params) > 0) {
             if (
                 !($this->podcast = (new PodcastModel())->getPodcastById(
-                    $params[0]
+                    $params[0],
                 ))
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
@@ -43,8 +43,8 @@ class PodcastPerson extends BaseController
 
         $data = [
             'podcast' => $this->podcast,
-            'podcastPersons' => (new PodcastPersonModel())->getPersonsByPodcastId(
-                $this->podcast->id
+            'podcastPersons' => (new PodcastPersonModel())->getPodcastPersons(
+                $this->podcast->id,
             ),
             'personOptions' => (new PersonModel())->getPersonOptions(),
             'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
@@ -71,7 +71,7 @@ class PodcastPerson extends BaseController
         (new PodcastPersonModel())->addPodcastPersons(
             $this->podcast->id,
             $this->request->getPost('person'),
-            $this->request->getPost('person_group_role')
+            $this->request->getPost('person_group_role'),
         );
 
         return redirect()->back();
@@ -81,7 +81,7 @@ class PodcastPerson extends BaseController
     {
         (new PodcastPersonModel())->removePodcastPersons(
             $this->podcast->id,
-            $podcastPersonId
+            $podcastPersonId,
         );
 
         return redirect()->back();
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index b791a6b068..8448e7565f 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -47,7 +47,7 @@ class Episode extends BaseController
         self::triggerWebpageHit($this->podcast->id);
 
         $locale = service('request')->getLocale();
-        $cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_{$locale}";
+        $cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_{$locale}";
 
         if (!($cachedView = cache($cacheName))) {
             helper('persons');
@@ -107,7 +107,7 @@ class Episode extends BaseController
 
         $locale = service('request')->getLocale();
 
-        $cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
+        $cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
 
         if (!($cachedView = cache($cacheName))) {
             $theme = EpisodeModel::$themes[$theme];
diff --git a/app/Controllers/Feed.php b/app/Controllers/Feed.php
index 77bfcbfe9a..3225e5f289 100644
--- a/app/Controllers/Feed.php
+++ b/app/Controllers/Feed.php
@@ -26,7 +26,7 @@ class Feed extends Controller
         $serviceSlug = '';
         try {
             $service = \Opawg\UserAgentsPhp\UserAgentsRSS::find(
-                $_SERVER['HTTP_USER_AGENT']
+                $_SERVER['HTTP_USER_AGENT'],
             );
             if ($service) {
                 $serviceSlug = $service['slug'];
@@ -37,14 +37,14 @@ class Feed extends Controller
         }
 
         $cacheName =
-            "podcast{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
+            "podcast#{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
 
         if (!($found = cache($cacheName))) {
             $found = get_rss_feed($podcast, $serviceSlug);
 
             // The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
             $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
-                $podcast->id
+                $podcast->id,
             );
 
             cache()->save(
@@ -52,7 +52,7 @@ class Feed extends Controller
                 $found,
                 $secondsToNextUnpublishedEpisode
                     ? $secondsToNextUnpublishedEpisode
-                    : DECADE
+                    : DECADE,
             );
         }
         return $this->response->setXML($found);
diff --git a/app/Controllers/Page.php b/app/Controllers/Page.php
index 46d28d22da..8f97b5de99 100644
--- a/app/Controllers/Page.php
+++ b/app/Controllers/Page.php
@@ -36,37 +36,42 @@ class Page extends BaseController
 
     public function index()
     {
-        // The page cache is set to a decade so it is deleted manually upon page update
-        $this->cachePage(DECADE);
+        $cacheName = "page@{$this->page->slug}";
+        if (!($found = cache($cacheName))) {
+            $data = [
+                'page' => $this->page,
+            ];
 
-        $data = [
-            'page' => $this->page,
-        ];
-        return view('page', $data);
+            $found = view('page', $data);
+
+            // The page cache is set to a decade so it is deleted manually upon page update
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function credits()
     {
         $locale = service('request')->getLocale();
-        $model = new PodcastModel();
-        $allPodcasts = $model->findAll();
+        $allPodcasts = (new PodcastModel())->findAll();
 
-        if (!($found = cache("credits_{$locale}"))) {
+        $cacheName = "paĝe_credits_{$locale}";
+        if (!($found = cache($cacheName))) {
             $page = new \App\Entities\Page([
                 'title' => lang('Person.credits', [], $locale),
                 'slug' => 'credits',
                 'content' => '',
             ]);
 
-            $creditModel = (new CreditModel())->findAll();
+            $allCredits = (new CreditModel())->findAll();
 
             // Unlike the carpenter, we make a tree from a table:
-
             $person_group = null;
             $person_id = null;
             $person_role = null;
             $credits = [];
-            foreach ($creditModel as $credit) {
+            foreach ($allCredits as $credit) {
                 if ($person_group !== $credit->person_group) {
                     $person_group = $credit->person_group;
                     $person_id = $credit->person_id;
@@ -200,7 +205,7 @@ class Page extends BaseController
 
             $found = view('credits', $data);
 
-            cache()->save("credits_{$locale}", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index ef11e95483..2b351e73d2 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -68,7 +68,7 @@ class Podcast extends BaseController
         $seasonQuery = $this->request->getGet('season');
 
         if (!$yearQuery and !$seasonQuery) {
-            $defaultQuery = (new EpisodeModel())->getDefaultQuery(
+            $defaultQuery = (new PodcastModel())->getDefaultQuery(
                 $this->podcast->id,
             );
             if ($defaultQuery) {
@@ -84,18 +84,19 @@ class Podcast extends BaseController
             '_',
             array_filter([
                 'page',
-                "podcast{$this->podcast->id}",
+                "podcast#{$this->podcast->id}",
                 $yearQuery ? 'year' . $yearQuery : null,
                 $seasonQuery ? 'season' . $seasonQuery : null,
                 service('request')->getLocale(),
+                can_user_interact() ? '_interact' : '',
             ]),
         );
 
         if (!($found = cache($cacheName))) {
             // Build navigation array
-            $episodeModel = new EpisodeModel();
-            $years = $episodeModel->getYears($this->podcast->id);
-            $seasons = $episodeModel->getSeasons($this->podcast->id);
+            $podcastModel = new PodcastModel();
+            $years = $podcastModel->getYears($this->podcast->id);
+            $seasons = $podcastModel->getSeasons($this->podcast->id);
 
             $episodesNavigation = [];
             $activeQuery = null;
@@ -155,7 +156,7 @@ class Podcast extends BaseController
                 'podcast' => $this->podcast,
                 'episodesNav' => $episodesNavigation,
                 'activeQuery' => $activeQuery,
-                'episodes' => $episodeModel->getPodcastEpisodes(
+                'episodes' => (new EpisodeModel())->getPodcastEpisodes(
                     $this->podcast->id,
                     $this->podcast->type,
                     $yearQuery,
@@ -164,20 +165,20 @@ class Podcast extends BaseController
                 'persons' => $persons,
             ];
 
-            $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+            $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
                 $this->podcast->id,
             );
 
             // if user is logged in then send to the authenticated episodes view
             if (can_user_interact()) {
-                return view('podcast/episodes_authenticated', $data, [
+                $found = view('podcast/episodes_authenticated', $data, [
                     'cache' => $secondsToNextUnpublishedEpisode
                         ? $secondsToNextUnpublishedEpisode
                         : DECADE,
-                    'cache_name' => $cacheName . '_authenticated',
+                    'cache_name' => $cacheName,
                 ]);
             } else {
-                return view('podcast/episodes', $data, [
+                $found = view('podcast/episodes', $data, [
                     'cache' => $secondsToNextUnpublishedEpisode
                         ? $secondsToNextUnpublishedEpisode
                         : DECADE,
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index e7fdac994e..d0cdf64296 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -385,7 +385,7 @@ class Episode extends Entity
         }
 
         if (empty($this->persons)) {
-            $this->persons = (new EpisodePersonModel())->getPersonsByEpisodeId(
+            $this->persons = (new EpisodePersonModel())->getEpisodePersons(
                 $this->podcast_id,
                 $this->id,
             );
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 82922b2dd5..8bce314e5a 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -232,7 +232,7 @@ class Podcast extends Entity
         }
 
         if (empty($this->persons)) {
-            $this->persons = (new PodcastPersonModel())->getPersonsByPodcastId(
+            $this->persons = (new PodcastPersonModel())->getPodcastPersons(
                 $this->id,
             );
         }
diff --git a/app/Language/en/Install.php b/app/Language/en/Install.php
index 5a5af49e41..b4428f8d66 100644
--- a/app/Language/en/Install.php
+++ b/app/Language/en/Install.php
@@ -39,7 +39,7 @@ return [
         'cacheHandlerOptions' => [
             'file' => 'File',
             'redis' => 'Redis',
-            'memcached' => 'Memcached',
+            'predis' => 'Predis',
         ],
         'next' => 'Next',
         'submit' => 'Finish install',
diff --git a/app/Language/en/Validation.php b/app/Language/en/Validation.php
index 4fb156366d..fb27793e14 100644
--- a/app/Language/en/Validation.php
+++ b/app/Language/en/Validation.php
@@ -7,8 +7,6 @@
  */
 
 return [
-    'not_in_protected_slugs' =>
-        'The {field} field conflicts with one of the gateway routes (admin, auth or install).',
     'min_dims' =>
         '{field} is either not an image, or it is not wide or tall enough.',
     'is_image_squared' =>
diff --git a/app/Language/fr/Install.php b/app/Language/fr/Install.php
index e721ebb441..9264032032 100644
--- a/app/Language/fr/Install.php
+++ b/app/Language/fr/Install.php
@@ -39,7 +39,7 @@ return [
         'cacheHandlerOptions' => [
             'file' => 'Fichiers',
             'redis' => 'Redis',
-            'memcached' => 'Memcached',
+            'predis' => 'Predis',
         ],
         'next' => 'Suivant',
         'submit' => 'Terminer l’installation',
diff --git a/app/Language/fr/Validation.php b/app/Language/fr/Validation.php
index d9ae5dc517..519fbe9a03 100644
--- a/app/Language/fr/Validation.php
+++ b/app/Language/fr/Validation.php
@@ -7,8 +7,6 @@
  */
 
 return [
-    'not_in_protected_slugs' =>
-        'Le champ {field} est en conflit avec une des routes (admin, auth ou install).',
     'min_dims' =>
         '{field} n’est pas une image ou n’a pas la taille minimale requise.',
     'is_image_squared' =>
diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php
index 6764f6ed04..6fc150c470 100644
--- a/app/Models/CategoryModel.php
+++ b/app/Models/CategoryModel.php
@@ -34,21 +34,24 @@ class CategoryModel extends Model
 
     public function getCategoryOptions()
     {
-        if (!($options = cache('category_options'))) {
+        $locale = service('request')->getLocale();
+        $cacheName = "category_options_{$locale}";
+
+        if (!($options = cache($cacheName))) {
             $categories = $this->findAll();
 
             $options = array_reduce(
                 $categories,
                 function ($result, $category) {
                     $result[$category->id] = lang(
-                        'Podcast.category_options.' . $category->code
+                        'Podcast.category_options.' . $category->code,
                     );
                     return $result;
                 },
-                []
+                [],
             );
 
-            cache()->save('category_options', $options, DECADE);
+            cache()->save($cacheName, $options, DECADE);
         }
 
         return $options;
@@ -64,7 +67,7 @@ class CategoryModel extends Model
      */
     public function setPodcastCategories($podcastId, $categories)
     {
-        cache()->delete("podcasts{$podcastId}_categories");
+        cache()->delete("podcast#{$podcastId}_categories");
 
         // Remove already previously set categories to overwrite them
         $this->db
@@ -83,7 +86,7 @@ class CategoryModel extends Model
 
                     return $result;
                 },
-                []
+                [],
             );
 
             // Set podcast categories
@@ -103,20 +106,17 @@ class CategoryModel extends Model
      */
     public function getPodcastCategories($podcastId)
     {
-        if (!($categories = cache("podcasts{$podcastId}_categories"))) {
+        $cacheName = "podcast#{$podcastId}_categories";
+        if (!($categories = cache($cacheName))) {
             $categories = $this->select('categories.*')
                 ->join(
                     'podcasts_categories',
-                    'podcasts_categories.category_id = categories.id'
+                    'podcasts_categories.category_id = categories.id',
                 )
                 ->where('podcasts_categories.podcast_id', $podcastId)
                 ->findAll();
 
-            cache()->save(
-                "podcasts{$podcastId}_categories",
-                $categories,
-                DECADE
-            );
+            cache()->save($cacheName, $categories, DECADE);
         }
 
         return $categories;
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 7fd8c5b200..9360348286 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -113,7 +113,8 @@ class EpisodeModel extends Model
      */
     public function getEpisodeBySlug($podcastId, $episodeSlug)
     {
-        if (!($found = cache("podcast@{$podcastId}_episode@{$episodeSlug}"))) {
+        $cacheName = "podcast#{$podcastId}_episode@{$episodeSlug}";
+        if (!($found = cache($cacheName))) {
             $builder = $this->select('episodes.*')
                 ->where('slug', $episodeSlug)
                 ->where('`published_at` <= NOW()', null, false);
@@ -130,11 +131,7 @@ class EpisodeModel extends Model
 
             $found = $builder->first();
 
-            cache()->save(
-                "podcast{$podcastId}_episode@{$episodeSlug}",
-                $found,
-                DECADE,
-            );
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -142,14 +139,15 @@ class EpisodeModel extends Model
 
     public function getEpisodeById($episodeId)
     {
-        if (!($found = cache("podcast_episode{$episodeId}"))) {
+        $cacheName = "podcast_episode#{$episodeId}";
+        if (!($found = cache($cacheName))) {
             $builder = $this->where([
                 'id' => $episodeId,
             ]);
 
             $found = $builder->first();
 
-            cache()->save("podcast_episode{$episodeId}", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -157,7 +155,8 @@ class EpisodeModel extends Model
 
     public function getPublishedEpisodeById($episodeId, $podcastId = null)
     {
-        if (!($found = cache("podcast{$podcastId}_episode{$episodeId}"))) {
+        $cacheName = "podcast_episode#{$episodeId}_published";
+        if (!($found = cache($cacheName))) {
             $builder = $this->where([
                 'id' => $episodeId,
             ])->where('`published_at` <= NOW()', null, false);
@@ -168,11 +167,7 @@ class EpisodeModel extends Model
 
             $found = $builder->first();
 
-            cache()->save(
-                "podcast{$podcastId}_episode{$episodeId}",
-                $found,
-                DECADE,
-            );
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -195,7 +190,7 @@ class EpisodeModel extends Model
         $cacheName = implode(
             '_',
             array_filter([
-                "podcast{$podcastId}",
+                "podcast#{$podcastId}",
                 $year,
                 $season ? 'season' . $season : null,
                 'episodes',
@@ -243,106 +238,6 @@ class EpisodeModel extends Model
         return $found;
     }
 
-    public function getYears(int $podcastId): array
-    {
-        if (!($found = cache("podcast{$podcastId}_years"))) {
-            $found = $this->select(
-                'YEAR(published_at) as year, count(*) as number_of_episodes',
-            )
-                ->where([
-                    'podcast_id' => $podcastId,
-                    'season_number' => null,
-                    $this->deletedField => null,
-                ])
-                ->where('`published_at` <= NOW()', null, false)
-                ->groupBy('year')
-                ->orderBy('year', 'DESC')
-                ->get()
-                ->getResultArray();
-
-            $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
-                $podcastId,
-            );
-
-            cache()->save(
-                "podcast{$podcastId}_years",
-                $found,
-                $secondsToNextUnpublishedEpisode
-                    ? $secondsToNextUnpublishedEpisode
-                    : DECADE,
-            );
-        }
-
-        return $found;
-    }
-
-    public function getSeasons(int $podcastId): array
-    {
-        if (!($found = cache("podcast{$podcastId}_seasons"))) {
-            $found = $this->select(
-                'season_number, count(*) as number_of_episodes',
-            )
-                ->where([
-                    'podcast_id' => $podcastId,
-                    'season_number is not' => null,
-                    $this->deletedField => null,
-                ])
-                ->where('`published_at` <= NOW()', null, false)
-                ->groupBy('season_number')
-                ->orderBy('season_number', 'ASC')
-                ->get()
-                ->getResultArray();
-
-            $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
-                $podcastId,
-            );
-
-            cache()->save(
-                "podcast{$podcastId}_seasons",
-                $found,
-                $secondsToNextUnpublishedEpisode
-                    ? $secondsToNextUnpublishedEpisode
-                    : DECADE,
-            );
-        }
-
-        return $found;
-    }
-
-    /**
-     * Returns the default query for displaying the episode list on the podcast page
-     *
-     * @param int $podcastId
-     *
-     * @return array|null
-     */
-    public function getDefaultQuery(int $podcastId)
-    {
-        if (!($defaultQuery = cache("podcast{$podcastId}_defaultQuery"))) {
-            $seasons = $this->getSeasons($podcastId);
-
-            if (!empty($seasons)) {
-                // get latest season
-                $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;
-                }
-            }
-
-            cache()->save(
-                "podcast{$podcastId}_defaultQuery",
-                $defaultQuery,
-                DECADE,
-            );
-        }
-        return $defaultQuery;
-    }
-
     /**
      * Returns the timestamp difference in seconds between the next episode to publish and the current timestamp
      * Returns false if there's no episode to publish
@@ -387,70 +282,37 @@ class EpisodeModel extends Model
         );
 
         // delete cache for rss feed
-        cache()->delete("podcast{$episode->podcast_id}_feed");
-        foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) {
-            cache()->delete(
-                "podcast{$episode->podcast_id}_feed_{$service['slug']}",
-            );
-        }
+        cache()->deleteMatching("podcast#{$episode->podcast_id}_feed*");
 
         // delete model requests cache
-        cache()->delete("podcast{$episode->podcast_id}_episodes");
+        cache()->delete("podcast#{$episode->podcast_id}_episodes");
 
+        cache()->deleteMatching("podcast_episode#{$episode->id}*");
         cache()->delete(
-            "podcast{$episode->podcast_id}_episode@{$episode->slug}",
+            "podcast#{$episode->podcast_id}_episode@{$episode->slug}",
         );
 
-        cache()->delete("podcast_episode{$episode->id}");
-
-        // delete episode lists cache per year / season for a podcast
-        // and localized pages
-        $episodeModel = new EpisodeModel();
-        $years = $episodeModel->getYears($episode->podcast_id);
-        $seasons = $episodeModel->getSeasons($episode->podcast_id);
-        $supportedLocales = config('App')->supportedLocales;
-
-        foreach ($supportedLocales as $locale) {
-            cache()->delete(
-                "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}",
-            );
-            cache()->delete("credits_{$locale}");
-        }
+        cache()->deleteMatching(
+            "page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*",
+        );
+        cache()->deleteMatching('page_credits_*');
 
-        foreach ($years as $year) {
-            cache()->delete(
-                "podcast{$episode->podcast_id}_year{$year['year']}_episodes",
+        if ($episode->season_number) {
+            cache()->deleteMatching("podcast#{$episode->podcast_id}_season*");
+            cache()->deleteMatching(
+                "page_podcast#{$episode->podcast_id}_season*",
             );
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$episode->podcast_id}_year{$year['year']}_{$locale}",
-                );
-            }
-        }
-
-        foreach ($seasons as $season) {
-            cache()->delete(
-                "podcast{$episode->podcast_id}_season{$season['season_number']}_episodes",
+        } else {
+            cache()->deleteMatching("podcast#{$episode->podcast_id}_year*");
+            cache()->deleteMatching(
+                "page_podcast#{$episode->podcast_id}_year*",
             );
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$episode->podcast_id}_season{$season['season_number']}_{$locale}",
-                );
-            }
-        }
-
-        foreach (array_keys(self::$themes) as $themeKey) {
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$episode->podcast_id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}",
-                );
-            }
         }
 
         // delete query cache
-        cache()->delete("podcast{$episode->podcast_id}_defaultQuery");
-        cache()->delete("podcast{$episode->podcast_id}_years");
-        cache()->delete("podcast{$episode->podcast_id}_seasons");
+        cache()->delete("podcast#{$episode->podcast_id}_defaultQuery");
+        cache()->delete("podcast#{$episode->podcast_id}_years");
+        cache()->delete("podcast#{$episode->podcast_id}_seasons");
 
         return $data;
     }
diff --git a/app/Models/EpisodePersonModel.php b/app/Models/EpisodePersonModel.php
index 1ed80d1e06..8df81e10e5 100644
--- a/app/Models/EpisodePersonModel.php
+++ b/app/Models/EpisodePersonModel.php
@@ -38,27 +38,17 @@ class EpisodePersonModel extends Model
     protected $afterInsert = ['clearCache'];
     protected $beforeDelete = ['clearCache'];
 
-    public function getPersonsByEpisodeId($podcastId, $episodeId)
+    public function getEpisodePersons($episodeId)
     {
-        if (
-            !($found = cache(
-                "podcast{$podcastId}_episodes{$episodeId}_persons"
-            ))
-        ) {
+        $cacheName = "podcast_episode#{$episodeId}_persons";
+        if (!($found = cache($cacheName))) {
             $found = $this->select('episodes_persons.*')
                 ->where('episode_id', $episodeId)
-                ->join(
-                    'persons',
-                    'person_id=persons.id'
-                )
+                ->join('persons', 'person_id=persons.id')
                 ->orderby('full_name')
                 ->findAll();
 
-            cache()->save(
-                "podcast{$podcastId}_episodes{$episodeId}_persons",
-                $found,
-                DECADE
-            );
+            cache()->save($cacheName, $found, DECADE);
         }
         return $found;
     }
@@ -81,11 +71,9 @@ class EpisodePersonModel extends Model
     ) {
         if (!empty($persons)) {
             $this->clearCache([
-                'id' => [
-                    'podcast_id' => $podcastId,
-                    'episode_id' => $episodeId,
-                ],
+                'episode_id' => $episodeId,
             ]);
+
             $data = [];
             foreach ($persons as $person) {
                 if ($groups_roles) {
@@ -126,23 +114,17 @@ class EpisodePersonModel extends Model
 
     protected function clearCache(array $data)
     {
-        $podcastId = null;
         $episodeId = null;
-        if (
-            isset($data['id']['podcast_id']) &&
-            isset($data['id']['episode_id'])
-        ) {
-            $podcastId = $data['id']['podcast_id'];
-            $episodeId = $data['id']['episode_id'];
+        if (isset($data['episode_id'])) {
+            $episodeId = $data['episode_id'];
         } else {
-            $episodePerson = (new EpisodePersonModel())->find(
-                is_array($data['id']) ? $data['id']['id'] : $data['id']
+            $person = (new EpisodePersonModel())->find(
+                is_array($data['id']) ? $data['id']['id'] : $data['id'],
             );
-            $podcastId = $episodePerson->podcast_id;
-            $episodeId = $episodePerson->episode_id;
+            $episodeId = $person->episode_id;
         }
 
-        cache()->delete("podcast{$podcastId}_episodes{$episodeId}_persons");
+        cache()->delete("podcast_episode#{$episodeId}_persons");
         (new EpisodeModel())->clearCache(['id' => $episodeId]);
 
         return $data;
diff --git a/app/Models/PageModel.php b/app/Models/PageModel.php
index d3740ead5d..c045ca0c70 100644
--- a/app/Models/PageModel.php
+++ b/app/Models/PageModel.php
@@ -25,7 +25,7 @@ class PageModel extends Model
     protected $validationRules = [
         'title' => 'required',
         'slug' =>
-            'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]|is_unique[pages.slug,id,{id}]|not_in_protected_slugs',
+            'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]|is_unique[pages.slug,id,{id}]',
         'content' => 'required',
     ];
     protected $validationMessages = [];
@@ -37,46 +37,8 @@ class PageModel extends Model
 
     protected function clearCache(array $data)
     {
-        $page = (new PageModel())->find(
-            is_array($data['id']) ? $data['id'][0] : $data['id'],
-        );
-
-        // delete page cache
-        cache()->delete(md5($page->link));
-
-        // Clear the cache of all podcast and episode pages
-        $allPodcasts = (new PodcastModel())->findAll();
-
-        foreach ($allPodcasts as $podcast) {
-            // delete localized podcast and episode page cache
-            $episodeModel = new EpisodeModel();
-            $years = $episodeModel->getYears($podcast->id);
-            $seasons = $episodeModel->getSeasons($podcast->id);
-            $supportedLocales = config('App')->supportedLocales;
-
-            foreach ($years as $year) {
-                foreach ($supportedLocales as $locale) {
-                    cache()->delete(
-                        "page_podcast{$podcast->id}_year{$year['year']}_{$locale}",
-                    );
-                }
-            }
-            foreach ($seasons as $season) {
-                foreach ($supportedLocales as $locale) {
-                    cache()->delete(
-                        "page_podcast{$podcast->id}_season{$season['season_number']}_{$locale}",
-                    );
-                }
-            }
-
-            foreach ($podcast->episodes as $episode) {
-                foreach ($supportedLocales as $locale) {
-                    cache()->delete(
-                        "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}",
-                    );
-                }
-            }
-        }
+        // Clear the cache of all pages
+        cache()->deleteMatching('page*');
 
         return $data;
     }
diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php
index e1812cf47e..fc19b3988d 100644
--- a/app/Models/PersonModel.php
+++ b/app/Models/PersonModel.php
@@ -48,9 +48,11 @@ class PersonModel extends Model
 
     public function getPersonById($personId)
     {
-        if (!($found = cache("person{$personId}"))) {
+        $cacheName = "person#{$personId}";
+        if (!($found = cache($cacheName))) {
             $found = $this->find($personId);
-            cache()->save("person{$personId}", $found, DECADE);
+
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -99,7 +101,8 @@ class PersonModel extends Model
     {
         $options = [];
         $locale = service('request')->getLocale();
-        if (!($options = cache("taxonomy_options_{$locale}"))) {
+        $cacheName = "taxonomy_options_{$locale}";
+        if (!($options = cache($cacheName))) {
             foreach (lang('PersonsTaxonomy.persons') as $group_key => $group) {
                 foreach ($group['roles'] as $role_key => $role) {
                     $options[
@@ -108,7 +111,7 @@ class PersonModel extends Model
                 }
             }
 
-            cache()->save("taxonomy_options_{$locale}", $options, DECADE);
+            cache()->save($cacheName, $options, DECADE);
         }
 
         return $options;
@@ -116,19 +119,15 @@ class PersonModel extends Model
 
     protected function clearCache(array $data)
     {
-        $person = (new PersonModel())->getPersonById(
+        $person = (new PersonModel())->find(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
         );
 
         cache()->delete('person_options');
-        cache()->delete("person{$person->id}");
-        cache()->delete("user{$person->created_by}_persons");
+        cache()->delete("person#{$person->id}");
 
-        $supportedLocales = config('App')->supportedLocales;
-        // clear cache for every credit page
-        foreach ($supportedLocales as $locale) {
-            cache()->delete("credit_{$locale}");
-        }
+        // clear cache for every credits page
+        cache()->deleteMatching('page_credits_*');
 
         return $data;
     }
diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php
index a5666e0c09..45d84bb915 100644
--- a/app/Models/PlatformModel.php
+++ b/app/Models/PlatformModel.php
@@ -45,9 +45,10 @@ class PlatformModel extends Model
 
     public function getPlatform($slug)
     {
-        if (!($found = cache("platform_$slug"))) {
+        $cacheName = "platform@{$slug}";
+        if (!($found = cache($cacheName))) {
             $found = $this->where('slug', $slug)->first();
-            cache()->save("platform_$slug", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
         return $found;
     }
@@ -72,7 +73,9 @@ class PlatformModel extends Model
     public function getPlatformsWithLinks($podcastId, $platformType)
     {
         if (
-            !($found = cache("podcast{$podcastId}_platforms_{$platformType}"))
+            !($found = cache(
+                "podcast#{$podcastId}_platforms_{$platformType}_withLinks",
+            ))
         ) {
             $found = $this->select(
                 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player',
@@ -86,7 +89,7 @@ class PlatformModel extends Model
                 ->findAll();
 
             cache()->save(
-                "podcast{$podcastId}_platforms_{$platformType}",
+                "podcast#{$podcastId}_platforms_{$platformType}_withLinks",
                 $found,
                 DECADE,
             );
@@ -97,11 +100,8 @@ class PlatformModel extends Model
 
     public function getPodcastPlatforms($podcastId, $platformType)
     {
-        if (
-            !($found = cache(
-                "podcast{$podcastId}_podcastPlatforms_{$platformType}",
-            ))
-        ) {
+        $cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
+        if (!($found = cache($cacheName))) {
             $found = $this->select(
                 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player',
             )
@@ -113,11 +113,7 @@ class PlatformModel extends Model
                 ->where('platforms.type', $platformType)
                 ->findAll();
 
-            cache()->save(
-                "podcast{$podcastId}_podcastPlatforms_{$platformType}",
-                $found,
-                DECADE,
-            );
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -132,12 +128,14 @@ class PlatformModel extends Model
 
         $podcastsPlatformsTable = $this->db->prefixTable('podcasts_platforms');
         $platformsTable = $this->db->prefixTable('platforms');
-        $deleteJoinQuery = <<<EOD
+
+        $deleteJoinQuery = <<<SQL
         DELETE $podcastsPlatformsTable
         FROM $podcastsPlatformsTable
         INNER JOIN $platformsTable ON $platformsTable.slug = $podcastsPlatformsTable.platform_slug
         WHERE `podcast_id` = ? AND `type` = ?
-        EOD;
+        SQL;
+
         $this->db->query($deleteJoinQuery, [$podcastId, $platformType]);
 
         // Set podcastPlatforms
@@ -168,51 +166,9 @@ class PlatformModel extends Model
 
     public function clearCache($podcastId)
     {
-        $podcast = (new PodcastModel())->getPodcastById($podcastId);
+        cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
 
-        foreach (['podcasting', 'social', 'funding'] as $platformType) {
-            cache()->delete("podcast{$podcastId}_platforms_{$platformType}");
-            cache()->delete(
-                "podcast{$podcastId}_podcastPlatforms_{$platformType}",
-            );
-        }
         // delete localized podcast page cache
-        $episodeModel = new EpisodeModel();
-        $years = $episodeModel->getYears($podcastId);
-        $seasons = $episodeModel->getSeasons($podcastId);
-        $supportedLocales = config('App')->supportedLocales;
-
-        foreach ($years as $year) {
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcastId}_year{$year['year']}_{$locale}",
-                );
-            }
-        }
-
-        foreach ($seasons as $season) {
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcastId}_season{$season['season_number']}_{$locale}",
-                );
-            }
-        }
-
-        // clear cache for every localized podcast episode page
-        foreach ($podcast->episodes as $episode) {
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}",
-                );
-                foreach (
-                    array_keys(\App\Models\EpisodeModel::$themes)
-                    as $themeKey
-                ) {
-                    cache()->delete(
-                        "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}",
-                    );
-                }
-            }
-        }
+        cache()->deleteMatching("page_podcast#{$podcastId}*");
     }
 }
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index e533bbb71e..4724385fbe 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -83,7 +83,8 @@ class PodcastModel extends Model
 
     public function getPodcastByName($podcastName)
     {
-        if (!($found = cache("podcast@{$podcastName}"))) {
+        $cacheName = "podcast@{$podcastName}";
+        if (!($found = cache($cacheName))) {
             $found = $this->where('name', $podcastName)->first();
             cache()->save("podcast@{$podcastName}", $found, DECADE);
         }
@@ -93,10 +94,11 @@ class PodcastModel extends Model
 
     public function getPodcastById($podcastId)
     {
-        if (!($found = cache("podcast{$podcastId}"))) {
+        $cacheName = "podcast#{$podcastId}";
+        if (!($found = cache($cacheName))) {
             $found = $this->find($podcastId);
 
-            cache()->save("podcast{$podcastId}", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -111,7 +113,8 @@ class PodcastModel extends Model
      */
     public function getUserPodcasts($userId)
     {
-        if (!($found = cache("user{$userId}_podcasts"))) {
+        $cacheName = "user{$userId}_podcasts";
+        if (!($found = cache($cacheName))) {
             $found = $this->select('podcasts.*')
                 ->join(
                     'podcasts_users',
@@ -120,7 +123,7 @@ class PodcastModel extends Model
                 ->where('podcasts_users.user_id', $userId)
                 ->findAll();
 
-            cache()->save("user{$userId}_podcasts", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
@@ -128,7 +131,7 @@ class PodcastModel extends Model
 
     public function addPodcastContributor($userId, $podcastId, $groupId)
     {
-        cache()->delete("podcast{$podcastId}_contributors");
+        cache()->delete("podcast#{$podcastId}_contributors");
 
         $data = [
             'user_id' => (int) $userId,
@@ -141,7 +144,7 @@ class PodcastModel extends Model
 
     public function updatePodcastContributor($userId, $podcastId, $groupId)
     {
-        cache()->delete("podcast{$podcastId}_contributors");
+        cache()->delete("podcast#{$podcastId}_contributors");
 
         return $this->db
             ->table('podcasts_users')
@@ -154,7 +157,7 @@ class PodcastModel extends Model
 
     public function removePodcastContributor($userId, $podcastId)
     {
-        cache()->delete("podcast{$podcastId}_contributors");
+        cache()->delete("podcast#{$podcastId}_contributors");
 
         return $this->db
             ->table('podcasts_users')
@@ -196,75 +199,122 @@ class PodcastModel extends Model
             : false;
     }
 
-    public function clearCache(array $data)
+    public function getYears(int $podcastId): array
     {
-        $podcast = (new PodcastModel())->getPodcastById(
-            is_array($data['id']) ? $data['id'][0] : $data['id'],
-        );
-        $supportedLocales = config('App')->supportedLocales;
+        $cacheName = "podcast#{$podcastId}_years";
+        if (!($found = cache($cacheName))) {
+            $episodeModel = new EpisodeModel();
+            $found = $episodeModel
+                ->select(
+                    'YEAR(published_at) as year, count(*) as number_of_episodes',
+                )
+                ->where([
+                    'podcast_id' => $podcastId,
+                    'season_number' => null,
+                    $episodeModel->deletedField => null,
+                ])
+                ->where('`published_at` <= NOW()', null, false)
+                ->groupBy('year')
+                ->orderBy('year', 'DESC')
+                ->get()
+                ->getResultArray();
+
+            $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+                $podcastId,
+            );
 
-        // delete cache for rss feed and podcast pages
-        cache()->delete("podcast{$podcast->id}_feed");
-        foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) {
-            cache()->delete("podcast{$podcast->id}_feed_{$service['slug']}");
+            cache()->save(
+                $cacheName,
+                $found,
+                $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+            );
         }
 
-        // delete model requests cache
-        cache()->delete("podcast{$podcast->id}");
-        cache()->delete("podcast@{$podcast->name}");
+        return $found;
+    }
 
-        // clear cache for every localized podcast episode page
-        foreach ($podcast->episodes as $episode) {
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}",
-                );
-                foreach (
-                    array_keys(\App\Models\EpisodeModel::$themes)
-                    as $themeKey
-                ) {
-                    cache()->delete(
-                        "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}",
-                    );
-                }
-            }
-        }
-        // clear cache for every credit page
-        foreach ($supportedLocales as $locale) {
-            cache()->delete("credits_{$locale}");
-        }
+    public function getSeasons(int $podcastId): array
+    {
+        $cacheName = "podcast#{$podcastId}_seasons";
+        if (!($found = cache($cacheName))) {
+            $episodeModel = new EpisodeModel();
+            $found = $episodeModel
+                ->select('season_number, count(*) as number_of_episodes')
+                ->where([
+                    'podcast_id' => $podcastId,
+                    'season_number is not' => null,
+                    $episodeModel->deletedField => null,
+                ])
+                ->where('`published_at` <= NOW()', null, false)
+                ->groupBy('season_number')
+                ->orderBy('season_number', 'ASC')
+                ->get()
+                ->getResultArray();
 
-        // delete episode lists cache per year / season
-        // and localized pages
-        $episodeModel = new EpisodeModel();
-        $years = $episodeModel->getYears($podcast->id);
-        $seasons = $episodeModel->getSeasons($podcast->id);
+            $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+                $podcastId,
+            );
 
-        foreach ($years as $year) {
-            cache()->delete(
-                "podcast{$podcast->id}_year{$year['year']}_episodes",
+            cache()->save(
+                $cacheName,
+                $found,
+                $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
             );
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcast->id}_year{$year['year']}_{$locale}",
-                );
-            }
         }
-        foreach ($seasons as $season) {
-            cache()->delete(
-                "podcast{$podcast->id}_season{$season['season_number']}_episodes",
-            );
-            foreach ($supportedLocales as $locale) {
-                cache()->delete(
-                    "page_podcast{$podcast->id}_season{$season['season_number']}_{$locale}",
-                );
+
+        return $found;
+    }
+
+    /**
+     * Returns the default query for displaying the episode list on the podcast page
+     *
+     * @param int $podcastId
+     *
+     * @return array|null
+     */
+    public function getDefaultQuery(int $podcastId)
+    {
+        $cacheName = "podcast#{$podcastId}_defaultQuery";
+        if (!($defaultQuery = cache($cacheName))) {
+            $seasons = $this->getSeasons($podcastId);
+
+            if (!empty($seasons)) {
+                // get latest season
+                $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;
+                }
             }
+
+            cache()->save($cacheName, $defaultQuery, DECADE);
         }
+        return $defaultQuery;
+    }
+
+    public function clearCache(array $data)
+    {
+        $podcast = (new PodcastModel())->getPodcastById(
+            is_array($data['id']) ? $data['id'][0] : $data['id'],
+        );
+
+        // delete cache all podcast pages
+        cache()->deleteMatching("page_podcast#{$podcast->id}_*");
 
-        // delete query cache
-        cache()->delete("podcast{$podcast->id}_defaultQuery");
-        cache()->delete("podcast{$podcast->id}_years");
-        cache()->delete("podcast{$podcast->id}_seasons");
+        // delete model requests cache, includes feed / query / episode lists, etc.
+        cache()->deleteMatching("podcast#{$podcast->id}*");
+        cache()->delete("podcast@{$podcast->name}");
+
+        // clear cache for every credit page
+        cache()->deleteMatching('page_credits_*');
 
         return $data;
     }
diff --git a/app/Models/PodcastPersonModel.php b/app/Models/PodcastPersonModel.php
index 8268cf0ebd..c111fe98b7 100644
--- a/app/Models/PodcastPersonModel.php
+++ b/app/Models/PodcastPersonModel.php
@@ -37,20 +37,19 @@ class PodcastPersonModel extends Model
     protected $afterInsert = ['clearCache'];
     protected $beforeDelete = ['clearCache'];
 
-    public function getPersonsByPodcastId($podcastId)
+    public function getPodcastPersons($podcastId)
     {
-        if (!($found = cache("podcast{$podcastId}_persons"))) {
+        $cacheName = "podcast#{$podcastId}_persons";
+        if (!($found = cache($cacheName))) {
             $found = $this->select('podcasts_persons.*')
                 ->where('podcast_id', $podcastId)
-                ->join(
-                    'persons',
-                    'person_id=persons.id'
-                )
+                ->join('persons', 'person_id=persons.id')
                 ->orderby('full_name')
                 ->findAll();
 
-            cache()->save("podcast{$podcastId}_persons", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
+
         return $found;
     }
 
@@ -66,7 +65,7 @@ class PodcastPersonModel extends Model
     public function addPodcastPersons($podcastId, $persons, $groups_roles)
     {
         if (!empty($persons)) {
-            $this->clearCache(['id' => ['podcast_id' => $podcastId]]);
+            $this->clearCache(['podcast_id' => $podcastId]);
             $data = [];
             foreach ($persons as $person) {
                 if ($groups_roles) {
@@ -102,16 +101,16 @@ class PodcastPersonModel extends Model
     protected function clearCache(array $data)
     {
         $podcastId = null;
-        if (isset($data['id']['podcast_id'])) {
-            $podcastId = $data['id']['podcast_id'];
+        if (isset($data['podcast_id'])) {
+            $podcastId = $data['podcast_id'];
         } else {
             $person = (new PodcastPersonModel())->find(
-                is_array($data['id']) ? $data['id']['id'] : $data['id']
+                is_array($data['id']) ? $data['id']['id'] : $data['id'],
             );
             $podcastId = $person->podcast_id;
         }
 
-        cache()->delete("podcast{$podcastId}_persons");
+        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 fa55d5927c..102ab7bfaf 100644
--- a/app/Models/SoundbiteModel.php
+++ b/app/Models/SoundbiteModel.php
@@ -56,14 +56,15 @@ class SoundbiteModel extends Model
      */
     public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
     {
-        if (!($found = cache("episode{$episodeId}_soundbites"))) {
+        $cacheName = "podcast_episode#{$episodeId}_soundbites";
+        if (!($found = cache($cacheName))) {
             $found = $this->where([
                 'episode_id' => $episodeId,
                 'podcast_id' => $podcastId,
             ])
                 ->orderBy('start_time')
                 ->findAll();
-            cache()->save("episode{$episodeId}_soundbites", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
         return $found;
     }
@@ -73,25 +74,18 @@ class SoundbiteModel extends Model
         $episode = (new EpisodeModel())->find(
             isset($data['data'])
                 ? $data['data']['episode_id']
-                : $data['id']['episode_id']
+                : $data['id']['episode_id'],
         );
 
-        cache()->delete("episode{$episode->id}_soundbites");
+        cache()->delete("podcast_episode#{$episode->id}_soundbites");
 
         // delete cache for rss feed
-        cache()->delete("podcast{$episode->id}_feed");
-        foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) {
-            cache()->delete(
-                "podcast{$episode->podcast->id}_feed_{$service['slug']}"
-            );
-        }
+        cache()->deleteMatching("podcast#{$episode->podcast_id}_feed*");
+
+        cache()->deleteMatching(
+            "page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*",
+        );
 
-        $supportedLocales = config('App')->supportedLocales;
-        foreach ($supportedLocales as $locale) {
-            cache()->delete(
-                "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}"
-            );
-        }
         return $data;
     }
 }
diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php
index 4a03446129..e726b1324e 100644
--- a/app/Models/UserModel.php
+++ b/app/Models/UserModel.php
@@ -14,7 +14,8 @@ class UserModel extends \Myth\Auth\Models\UserModel
 
     public function getPodcastContributors($podcastId)
     {
-        if (!($found = cache("podcast{$podcastId}_contributors"))) {
+        $cacheName = "podcast#{$podcastId}_contributors";
+        if (!($found = cache($cacheName))) {
             $found = $this->select('users.*, auth_groups.name as podcast_role')
                 ->join('podcasts_users', 'podcasts_users.user_id = users.id')
                 ->join(
@@ -24,7 +25,7 @@ class UserModel extends \Myth\Auth\Models\UserModel
                 ->where('podcasts_users.podcast_id', $podcastId)
                 ->findAll();
 
-            cache()->save("podcast{$podcastId}_contributors", $found, DECADE);
+            cache()->save($cacheName, $found, DECADE);
         }
 
         return $found;
diff --git a/app/Validation/Rules.php b/app/Validation/Rules.php
index 9b8ce5a405..94f117742d 100644
--- a/app/Validation/Rules.php
+++ b/app/Validation/Rules.php
@@ -10,26 +10,6 @@ namespace App\Validation;
 
 class Rules
 {
-    /**
-     * Value should not be within the array of protected slugs (adminGateway, authGateway or installGateway)
-     *
-     * @param string $value
-     *
-     * @return boolean
-     */
-    public function not_in_protected_slugs(string $value = null): bool
-    {
-        $appConfig = config('App');
-        $protectedSlugs = [
-            $appConfig->adminGateway,
-            $appConfig->authGateway,
-            $appConfig->installGateway,
-        ];
-        return !in_array($value, $protectedSlugs, true);
-    }
-
-    //--------------------------------------------------------------------
-
     /**
      * Checks a URL to ensure it's formed correctly.
      *
diff --git a/app/Views/install/cache_config.php b/app/Views/install/cache_config.php
index 44f01018fa..0cb1fc80a8 100644
--- a/app/Views/install/cache_config.php
+++ b/app/Views/install/cache_config.php
@@ -21,7 +21,7 @@
     [
         'file' => lang('Install.form.cacheHandlerOptions.file'),
         'redis' => lang('Install.form.cacheHandlerOptions.redis'),
-        'memcached' => lang('Install.form.cacheHandlerOptions.memcached'),
+        'predis' => lang('Install.form.cacheHandlerOptions.predis'),
     ],
     old('cache_handler', 'file'),
     [
diff --git a/composer.lock b/composer.lock
index e79f6d1b58..ed5ded5457 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": "0d419d654c43fa6e14f8a96b42258a91",
+    "content-hash": "b5d726bdc7252c80c0fd5a6f53de1948",
     "packages": [
         {
             "name": "brick/math",
@@ -68,12 +68,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/codeigniter4/CodeIgniter4.git",
-                "reference": "425bca14c840c08b935d730c73da372f3d4bdef8"
+                "reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/425bca14c840c08b935d730c73da372f3d4bdef8",
-                "reference": "425bca14c840c08b935d730c73da372f3d4bdef8",
+                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/dfbc85af9ef408a6654cce6a462c8fdde3ee2446",
+                "reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446",
                 "shasum": ""
             },
             "require": {
@@ -83,18 +83,18 @@
                 "ext-mbstring": "*",
                 "kint-php/kint": "^3.3",
                 "laminas/laminas-escaper": "^2.6",
-                "php": "^7.3||^8.0",
+                "php": "^7.3 || ^8.0",
                 "psr/log": "^1.1"
             },
             "require-dev": {
                 "codeigniter4/codeigniter4-standard": "^1.0",
                 "fakerphp/faker": "^1.9",
-                "johnkary/phpunit-speedtrap": "^3.3",
                 "mikey179/vfsstream": "^1.6",
-                "phpstan/phpstan": "0.12.82",
+                "nexusphp/tachycardia": "^1.0",
+                "phpstan/phpstan": "0.12.84",
                 "phpunit/phpunit": "^9.1",
                 "predis/predis": "^1.1",
-                "rector/rector": "^0.10",
+                "rector/rector": "0.10.6",
                 "squizlabs/php_codesniffer": "^3.3"
             },
             "suggest": {
@@ -139,7 +139,7 @@
                 "slack": "https://codeigniterchat.slack.com",
                 "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
             },
-            "time": "2021-03-25T19:47:47+00:00"
+            "time": "2021-04-20T08:40:30+00:00"
         },
         {
             "name": "composer/ca-bundle",
@@ -823,16 +823,16 @@
         },
         {
             "name": "league/commonmark",
-            "version": "1.5.7",
+            "version": "1.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54"
+                "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/11df9b36fd4f1d2b727a73bf14931d81373b9a54",
-                "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf",
+                "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf",
                 "shasum": ""
             },
             "require": {
@@ -920,7 +920,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-10-31T13:49:32+00:00"
+            "time": "2021-03-28T18:51:39+00:00"
         },
         {
             "name": "league/html-to-markdown",
@@ -1010,23 +1010,23 @@
         },
         {
             "name": "maxmind-db/reader",
-            "version": "v1.10.0",
+            "version": "v1.10.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
-                "reference": "07f84d969cfc527ce49388558a366ad376f1f35c"
+                "reference": "569bd44d97d30a4ec12c7793a33004a76d4caf18"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/07f84d969cfc527ce49388558a366ad376f1f35c",
-                "reference": "07f84d969cfc527ce49388558a366ad376f1f35c",
+                "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/569bd44d97d30a4ec12c7793a33004a76d4caf18",
+                "reference": "569bd44d97d30a4ec12c7793a33004a76d4caf18",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.2"
             },
             "conflict": {
-                "ext-maxminddb": "<1.10.0,>=2.0.0"
+                "ext-maxminddb": "<1.10.1,>=2.0.0"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "*",
@@ -1069,9 +1069,9 @@
             ],
             "support": {
                 "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
-                "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.10.0"
+                "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.10.1"
             },
-            "time": "2021-02-09T17:52:47+00:00"
+            "time": "2021-04-14T17:49:35+00:00"
         },
         {
             "name": "maxmind/web-service-common",
@@ -1125,20 +1125,20 @@
         },
         {
             "name": "michalsn/codeigniter4-uuid",
-            "version": "v1.0.0-beta2",
+            "version": "v1.0.0-beta3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/michalsn/codeigniter4-uuid.git",
-                "reference": "a5f9751570a3b27e81deaa7548eef507499be888"
+                "reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/a5f9751570a3b27e81deaa7548eef507499be888",
-                "reference": "a5f9751570a3b27e81deaa7548eef507499be888",
+                "url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/568aba8f315199b6cc87e76b8441cd03a2bba5b4",
+                "reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.2",
+                "php": ">=7.3",
                 "ramsey/uuid": "^4.0"
             },
             "require-dev": {
@@ -1172,9 +1172,9 @@
             ],
             "support": {
                 "issues": "https://github.com/michalsn/codeigniter4-uuid/issues",
-                "source": "https://github.com/michalsn/codeigniter4-uuid/tree/develop"
+                "source": "https://github.com/michalsn/codeigniter4-uuid/tree/v1.0.0-beta3"
             },
-            "time": "2020-08-16T07:56:20+00:00"
+            "time": "2021-04-02T11:08:18+00:00"
         },
         {
             "name": "myth/auth",
@@ -1182,12 +1182,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/lonnieezell/myth-auth.git",
-                "reference": "a11dc6369177c932add936f1be3844c30fe45ed4"
+                "reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/a11dc6369177c932add936f1be3844c30fe45ed4",
-                "reference": "a11dc6369177c932add936f1be3844c30fe45ed4",
+                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/eff9805d7f1d27326f14875b53ff4b3d2a6b72ee",
+                "reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee",
                 "shasum": ""
             },
             "require": {
@@ -1248,7 +1248,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2021-03-23T03:55:24+00:00"
+            "time": "2021-04-12T22:34:12+00:00"
         },
         {
             "name": "opawg/user-agents-php",
@@ -1359,16 +1359,16 @@
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "2.0.30",
+            "version": "2.0.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36"
+                "reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/136b9ca7eebef78be14abf90d65c5e57b6bc5d36",
-                "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/233a920cb38636a43b18d428f9a8db1f0a1a08f4",
+                "reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4",
                 "shasum": ""
             },
             "require": {
@@ -1448,7 +1448,7 @@
             ],
             "support": {
                 "issues": "https://github.com/phpseclib/phpseclib/issues",
-                "source": "https://github.com/phpseclib/phpseclib/tree/2.0.30"
+                "source": "https://github.com/phpseclib/phpseclib/tree/2.0.31"
             },
             "funding": [
                 {
@@ -1464,7 +1464,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-12-17T05:42:04+00:00"
+            "time": "2021-04-06T13:56:45+00:00"
         },
         {
             "name": "podlibre/ipcat",
@@ -2761,16 +2761,16 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.5",
+            "version": "9.2.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1"
+                "reference": "f6293e1b30a2354e8428e004689671b83871edde"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1",
-                "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
+                "reference": "f6293e1b30a2354e8428e004689671b83871edde",
                 "shasum": ""
             },
             "require": {
@@ -2826,7 +2826,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
             },
             "funding": [
                 {
@@ -2834,7 +2834,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-11-28T06:44:49+00:00"
+            "time": "2021-03-28T07:26:59+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-- 
GitLab