From 40bdfaf4415ff9f46e63435fc5e61049649802f2 Mon Sep 17 00:00:00 2001
From: Moritz Strohm <strohm@data-quest.de>
Date: Tue, 14 May 2024 07:22:47 +0000
Subject: [PATCH] made Stud.IP cache compatible with PSR-6, re #3701

Merge request studip/studip!2570
---
 app/controllers/admin/cache.php               |   6 +-
 app/controllers/file.php                      |  12 +-
 composer.json                                 |   3 +-
 composer.lock                                 |  51 ++++-
 db/migrations/1.154_recalculate_score.php     |   3 +-
 db/migrations/1.224_db_cache_table.php        |   2 +-
 db/migrations/1.318_step_00353_cache.php      |  13 +-
 .../5.5.26_tic3193_no_unlimited_courses.php   |   5 +-
 .../6.0.3_adjust_cache_configuration.php      |  38 ++++
 lib/activities/Activity.php                   |   4 +-
 lib/bootstrap-autoload.php                    |   5 +
 lib/bootstrap-definitions.php                 |   4 +-
 lib/bootstrap.php                             |   4 +-
 lib/classes/MvvPerm.php                       |   4 +-
 lib/classes/Score.class.php                   |   2 +-
 lib/classes/SemClass.class.php                |  10 +-
 lib/classes/SemType.class.php                 |  10 +-
 lib/classes/Seminar.class.php                 |   4 +-
 lib/classes/SimpleORMap.class.php             |  12 +-
 lib/classes/Siteinfo.php                      |   6 +-
 lib/classes/StudipCache.class.php             |  94 --------
 lib/classes/StudipCachedArray.php             |  20 +-
 lib/classes/StudipDbCache.class.php           | 123 -----------
 lib/classes/StudipKing.class.php              |   2 +-
 lib/classes/StudipMemoryCache.class.php       |  84 -------
 lib/classes/UserLookup.class.php              |   2 +-
 lib/classes/assets/SASSCompiler.php           |   4 +-
 lib/classes/cache/Cache.class.php             | 208 ++++++++++++++++++
 lib/classes/cache/DbCache.class.php           | 142 ++++++++++++
 lib/classes/cache/Exception.php               |  27 +++
 .../Factory.class.php}                        |  53 +++--
 .../FileCache.class.php}                      | 191 ++++++++--------
 .../cache/InvalidCacheArgumentException.php   |  28 +++
 lib/classes/cache/Item.class.php              | 163 ++++++++++++++
 .../KeyTrait.php}                             |  10 +-
 .../MemcachedCache.class.php}                 | 110 +++++----
 lib/classes/cache/MemoryCache.class.php       |  98 +++++++++
 .../Proxy.class.php}                          |  89 ++++----
 .../RedisCache.class.php}                     |  95 ++++----
 .../Wrapper.class.php}                        |  76 ++++---
 lib/classes/cas/CAS_PGTStorage_Cache.php      |   4 +-
 lib/cronjobs/purge_cache.class.php            |   4 +-
 lib/functions.php                             |   2 +-
 lib/models/CourseDate.class.php               |   6 +-
 lib/models/OERMaterial.php                    |   4 +-
 lib/models/PersonalNotifications.class.php    |   6 +-
 lib/models/Semester.class.php                 |   6 +-
 lib/models/StudipCacheOperation.php           |   2 +-
 lib/phplib/CT_Cache.class.php                 |   2 +-
 lib/plugins/db/RolePersistence.class.php      |   4 +-
 lib/plugins/engine/PluginRepository.class.php |   2 +-
 lib/raumzeit/SingleDate.class.php             |   4 +-
 tests/functional/_bootstrap.php               |   4 +-
 tests/jsonapi/_bootstrap.php                  |   2 +
 tests/unit/_bootstrap.php                     |   4 +-
 tests/unit/lib/classes/MigrationTest.php      |   5 -
 .../lib/classes/StudipCachedArrayTest.php     |   5 +-
 57 files changed, 1197 insertions(+), 686 deletions(-)
 create mode 100644 db/migrations/6.0.3_adjust_cache_configuration.php
 delete mode 100644 lib/classes/StudipCache.class.php
 delete mode 100644 lib/classes/StudipDbCache.class.php
 delete mode 100644 lib/classes/StudipMemoryCache.class.php
 create mode 100644 lib/classes/cache/Cache.class.php
 create mode 100644 lib/classes/cache/DbCache.class.php
 create mode 100644 lib/classes/cache/Exception.php
 rename lib/classes/{StudipCacheFactory.class.php => cache/Factory.class.php} (81%)
 rename lib/classes/{StudipFileCache.class.php => cache/FileCache.class.php} (55%)
 create mode 100644 lib/classes/cache/InvalidCacheArgumentException.php
 create mode 100644 lib/classes/cache/Item.class.php
 rename lib/classes/{StudipCacheKeyTrait.php => cache/KeyTrait.php} (77%)
 rename lib/classes/{StudipMemcachedCache.php => cache/MemcachedCache.class.php} (51%)
 create mode 100644 lib/classes/cache/MemoryCache.class.php
 rename lib/classes/{StudipCacheProxy.php => cache/Proxy.class.php} (57%)
 rename lib/classes/{StudipRedisCache.class.php => cache/RedisCache.class.php} (73%)
 rename lib/classes/{StudipCacheWrapper.php => cache/Wrapper.class.php} (56%)

diff --git a/app/controllers/admin/cache.php b/app/controllers/admin/cache.php
index 329aab7a968..3e9d970a76d 100644
--- a/app/controllers/admin/cache.php
+++ b/app/controllers/admin/cache.php
@@ -111,7 +111,7 @@ class Admin_CacheController extends AuthenticatedController
         // Store settings to global config.
         if (Config::get()->store('SYSTEMCACHE', $settings)) {
             PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.'));
-            StudipCacheFactory::unconfigure();
+            \Studip\Cache\Factory::unconfigure();
         }
 
         $this->relocate('admin/cache/settings');
@@ -122,7 +122,7 @@ class Admin_CacheController extends AuthenticatedController
      */
     public function flush_action()
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->flush();
 
         PageLayout::postSuccess(_('Die Inhalte des Caches wurden gelöscht.'));
@@ -135,7 +135,7 @@ class Admin_CacheController extends AuthenticatedController
      */
     public function stats_action()
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
 
         $this->stats = $cache->getStats();
     }
diff --git a/app/controllers/file.php b/app/controllers/file.php
index 65344efaa89..bebfe70ebf8 100644
--- a/app/controllers/file.php
+++ b/app/controllers/file.php
@@ -364,7 +364,7 @@ class FileController extends AuthenticatedController
 
             //The file system object is a folder.
             //Calculate the files and the folder size:
-            list($this->folder_size, $this->folder_file_amount) = $this->getFolderSize($this->folder);
+            [$this->folder_size, $this->folder_file_amount] = $this->getFolderSize($this->folder);
             PageLayout::setTitle($this->folder->name);
             $this->render_action('folder_details');
         }
@@ -1128,7 +1128,7 @@ class FileController extends AuthenticatedController
 
                 $this->search_id = md5(json_encode($search_parameters));
 
-                $cache = StudipCacheFactory::getCache();
+                $cache = \Studip\Cache\Factory::getCache();
 
                 $merged_results = LibrarySearchManager::search(
                     $search_parameters,
@@ -1187,7 +1187,7 @@ class FileController extends AuthenticatedController
             $this->search_id = Request::get('search_id');
             $this->page = Request::get('page');
 
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $cache_data = $cache->read($this->search_id);
             $results = $cache_data['results'];
             $this->total_results = count($results);
@@ -1247,7 +1247,7 @@ class FileController extends AuthenticatedController
         }
 
         if ($item_id) {
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $documents = $cache->read($search_id);
             $document = $documents['results'][$item_id];
             if (!($document instanceof LibraryDocument)) {
@@ -1255,7 +1255,7 @@ class FileController extends AuthenticatedController
             }
             $file = LibraryFile::createFromLibraryDocument($document, $folder_id);
         } else {
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $search = $cache->read($search_id);
             if (!$search) {
                 throw new Exception('Search not found in cache!');
@@ -2036,7 +2036,7 @@ class FileController extends AuthenticatedController
                 PageLayout::postMessage($result);
             }
         }
-        list($this->folder_size, $this->folder_file_amount) = $this->getFolderSize($folder);
+        [$this->folder_size, $this->folder_file_amount] = $this->getFolderSize($folder);
     }
 
     public function delete_folder_action($folder_id)
diff --git a/composer.json b/composer.json
index e4d1902d0a5..009a7a07122 100644
--- a/composer.json
+++ b/composer.json
@@ -62,7 +62,8 @@
         "psy/psysh": "^0.12.2",
         "okvpn/clock-lts": "^1.0",
         "vlucas/phpdotenv": "^5.6",
-        "edu-sharing/auth-plugin": "8.0.x-dev"
+        "edu-sharing/auth-plugin": "8.0.x-dev",
+        "psr/cache": "^1.0"
     },
     "replace": {
         "symfony/polyfill-php73": "*",
diff --git a/composer.lock b/composer.lock
index ffde9bab888..b93431af4c4 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": "535218b1c9cca9acd1f2c554c59e5113",
+    "content-hash": "30db4609d0f74dac659c0fcd53db3d0a",
     "packages": [
         {
             "name": "algo26-matthias/idna-convert",
@@ -2889,6 +2889,55 @@
             },
             "time": "2023-02-11T17:10:30+00:00"
         },
+        {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/cache/tree/master"
+            },
+            "time": "2016-08-06T20:24:11+00:00"
+        },
         {
             "name": "psr/clock",
             "version": "1.0.0",
diff --git a/db/migrations/1.154_recalculate_score.php b/db/migrations/1.154_recalculate_score.php
index 158582ee291..5568363ea30 100644
--- a/db/migrations/1.154_recalculate_score.php
+++ b/db/migrations/1.154_recalculate_score.php
@@ -38,7 +38,7 @@ class RecalculateScore extends Migration {
     private static function getScore($user_id)
     {
         $user_id || $user_id = $GLOBALS['user']->id;
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         if ($cache->read("user_score_of_".$user_id)) {
             return $cache->read("user_score_of_".$user_id);
         }
@@ -142,4 +142,3 @@ class RecalculateScore extends Migration {
     }
 
 }
-
diff --git a/db/migrations/1.224_db_cache_table.php b/db/migrations/1.224_db_cache_table.php
index 66227d0b9e2..9abe7e3dc6f 100644
--- a/db/migrations/1.224_db_cache_table.php
+++ b/db/migrations/1.224_db_cache_table.php
@@ -17,7 +17,7 @@ class DbCacheTable extends Migration
                    PRIMARY KEY (cache_key)
                    ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC');
 
-        StudipCacheFactory::getCache()->flush();
+        \Studip\Cache\Factory::getCache()->flush();
     }
 
     public function down()
diff --git a/db/migrations/1.318_step_00353_cache.php b/db/migrations/1.318_step_00353_cache.php
index 901db604523..5959945db68 100644
--- a/db/migrations/1.318_step_00353_cache.php
+++ b/db/migrations/1.318_step_00353_cache.php
@@ -1,5 +1,10 @@
 <?php
 
+use Studip\Cache\DbCache;
+use Studip\Cache\FileCache;
+use Studip\Cache\MemcachedCache;
+use Studip\Cache\RedisCache;
+
 class Step00353Cache extends Migration
 {
     public function description()
@@ -20,10 +25,10 @@ class Step00353Cache extends Migration
             ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC");
 
         $types = [
-            'StudipDbCache',
-            'StudipFileCache',
-            'StudipMemcachedCache',
-            'StudipRedisCache'
+            DbCache::class,
+            FileCache::class,
+            MemcachedCache::class,
+            RedisCache::class,
         ];
 
         // Insert pre-defined cache types in to database
diff --git a/db/migrations/5.5.26_tic3193_no_unlimited_courses.php b/db/migrations/5.5.26_tic3193_no_unlimited_courses.php
index ef45bced3ab..21a80ca39b4 100644
--- a/db/migrations/5.5.26_tic3193_no_unlimited_courses.php
+++ b/db/migrations/5.5.26_tic3193_no_unlimited_courses.php
@@ -12,15 +12,14 @@ final class Tic3193NoUnlimitedCourses extends Migration
         DBManager::get()->exec("
             ALTER TABLE `sem_classes` ADD `unlimited_forbidden` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `is_group`
         ");
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('DB_SEM_CLASSES_ARRAY');
     }
 
     public function down()
     {
         DBManager::get()->exec("ALTER TABLE `sem_classes` DROP `unlimited_forbidden`");
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('DB_SEM_CLASSES_ARRAY');
     }
 }
-
diff --git a/db/migrations/6.0.3_adjust_cache_configuration.php b/db/migrations/6.0.3_adjust_cache_configuration.php
new file mode 100644
index 00000000000..2432e8474ff
--- /dev/null
+++ b/db/migrations/6.0.3_adjust_cache_configuration.php
@@ -0,0 +1,38 @@
+<?php
+return new class extends Migration
+{
+    private const MAPPING = [
+        StudipDbCache::class        => Studip\Cache\DbCache::class,
+        StudipFileCache::class      => Studip\Cache\FileCache::class,
+        StudipMemcachedCache::class => Studip\Cache\MemcachedCache::class,
+        StudipRedisCache::class     => Studip\Cache\RedisCache::class,
+    ];
+
+    public function description()
+    {
+        return 'Replaces the renamed cache classes in system configuration';
+    }
+
+    protected function up()
+    {
+        foreach (self::MAPPING as $old => $new) {
+            self::replaceCache($old, $new);
+        }
+    }
+
+    protected function down()
+    {
+        foreach (self::MAPPING as $old => $new) {
+            self::replaceCache($new, $old);
+        }
+    }
+
+    private function replaceCache(string $old, string $new): void
+    {
+        $query = "UPDATE `config_values`
+                  SET `value` = JSON_REPLACE(`value`, '$.type', ?)
+                  WHERE `field` = 'SYSTEMCACHE'
+                    AND JSON_CONTAINS(`value`, JSON_QUOTE(?), '$.type')";
+        DBManager::get()->execute($query, [$new, $old]);
+    }
+};
diff --git a/lib/activities/Activity.php b/lib/activities/Activity.php
index 3508e5a2170..e1c918d0aa7 100644
--- a/lib/activities/Activity.php
+++ b/lib/activities/Activity.php
@@ -163,7 +163,7 @@ class Activity extends \SimpleORMap
         );
 
         //Expire Cache
-        \StudipCacheFactory::getCache()->expire('activity/oldest_activity');
+        \Studip\Cache\Factory::getCache()->expire('activity/oldest_activity');
     }
 
     /**
@@ -173,7 +173,7 @@ class Activity extends \SimpleORMap
      */
     public static function getOldestActivity()
     {
-        $cache = \StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = 'activity/oldest-activity';
 
         if (!$activity = unserialize($cache->read($cache_key))) {
diff --git a/lib/bootstrap-autoload.php b/lib/bootstrap-autoload.php
index 4e63115f5eb..21219fd325e 100644
--- a/lib/bootstrap-autoload.php
+++ b/lib/bootstrap-autoload.php
@@ -20,6 +20,11 @@ StudipAutoloader::addAutoloadPath('lib/classes/admission');
 StudipAutoloader::addAutoloadPath('lib/classes/admission/userfilter');
 StudipAutoloader::addAutoloadPath('lib/classes/auth_plugins');
 StudipAutoloader::addAutoloadPath('lib/classes/calendar');
+
+StudipAutoloader::addAutoloadPath('lib/classes/cache', 'Studip\\Cache');
+class_alias(\Studip\Cache\Factory::class, 'StudipCacheFactory');
+class_alias(\Studip\Cache\Cache::class, 'StudipCache');
+
 StudipAutoloader::addAutoloadPath('lib/classes/exportdocument');
 StudipAutoloader::addAutoloadPath('lib/classes/forms');
 StudipAutoloader::addAutoloadPath('lib/classes/globalsearch');
diff --git a/lib/bootstrap-definitions.php b/lib/bootstrap-definitions.php
index 17f35d16d64..ee80135be09 100644
--- a/lib/bootstrap-definitions.php
+++ b/lib/bootstrap-definitions.php
@@ -14,8 +14,8 @@ return [
             ),
         ]);
     }),
-    StudipCache::class => DI\factory(function () {
-        return StudipCacheFactory::getCache();
+    \Studip\Cache\Cache::class => DI\factory(function () {
+        return \Studip\Cache\Factory::getCache();
     }),
     StudipPDO::class => DI\factory(function () {
         return DBManager::get();
diff --git a/lib/bootstrap.php b/lib/bootstrap.php
index 364f9d9f69f..9afc16a7a68 100644
--- a/lib/bootstrap.php
+++ b/lib/bootstrap.php
@@ -170,7 +170,7 @@ if (isset($_SERVER['REQUEST_METHOD'])) {
 // bootstrap because the stud.ip cache needs to have a db conenction)
 if ($GLOBALS['CACHING_ENABLE']) {
     $lookup_hash = null;
-    $cached = StudipCacheFactory::getCache()->read('STUDIP#autoloader-classes');
+    $cached = \Studip\Cache\Factory::getCache()->read('STUDIP#autoloader-classes');
     if ($cached) {
         $class_lookup = json_decode($cached, true);
         if (is_array($class_lookup)) {
@@ -182,7 +182,7 @@ if ($GLOBALS['CACHING_ENABLE']) {
     register_shutdown_function(function () use ($lookup_hash) {
         $cached = json_encode(StudipAutoloader::$class_lookup, JSON_UNESCAPED_UNICODE);
         if (md5($cached) !== $lookup_hash) {
-            StudipCacheFactory::getCache()->write(
+            \Studip\Cache\Factory::getCache()->write(
                 'STUDIP#autoloader-classes',
                 $cached,
                 7 * 24 * 60 * 60
diff --git a/lib/classes/MvvPerm.php b/lib/classes/MvvPerm.php
index 93f9f440420..91e1e35b1a5 100644
--- a/lib/classes/MvvPerm.php
+++ b/lib/classes/MvvPerm.php
@@ -563,7 +563,7 @@ class MvvPerm {
     private static function getPrivileges($mvv_table)
     {
         if (self::$privileges === null) {
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             self::$privileges = unserialize($cache->read(MVV::CACHE_KEY . '/privileges'));
         }
 
@@ -576,7 +576,7 @@ class MvvPerm {
                     include $config_file; // Defines $privileges
                     self::$privileges[$mvv_table] = $privileges ?? [];
                 }
-                $cache = StudipCacheFactory::getCache();
+                $cache = \Studip\Cache\Factory::getCache();
                 $cache->write(MVV::CACHE_KEY . '/privileges', serialize(self::$privileges));
             }
         }
diff --git a/lib/classes/Score.class.php b/lib/classes/Score.class.php
index 8f038f9fb94..f7d8ea34271 100644
--- a/lib/classes/Score.class.php
+++ b/lib/classes/Score.class.php
@@ -121,7 +121,7 @@ class Score
     public static function GetMyScore($user_or_id = null)
     {
         $user = $user_or_id ? User::toObject($user_or_id) : User::findCurrent();
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         if ($cache->read("user_score_of_".$user->id)) {
             return $cache->read("user_score_of_".$user->id);
         }
diff --git a/lib/classes/SemClass.class.php b/lib/classes/SemClass.class.php
index 63be9209155..2a038e20787 100644
--- a/lib/classes/SemClass.class.php
+++ b/lib/classes/SemClass.class.php
@@ -389,7 +389,7 @@ class SemClass implements ArrayAccess
                 "chdate = UNIX_TIMESTAMP() " .
             "WHERE id = :id ".
         "");
-        StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+        \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
         return $statement->execute([
             'id' => $this->data['id'],
             'name' => $this->data['name'],
@@ -453,7 +453,7 @@ class SemClass implements ArrayAccess
                 DELETE FROM sem_classes
                 WHERE id = :id
             ");
-            StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+            \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
             return $statement->execute([
                 'id' => $this->data['id']
             ]);
@@ -552,7 +552,7 @@ class SemClass implements ArrayAccess
             $db = DBManager::get();
             self::$sem_classes = [];
 
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $class_array = unserialize($cache->read('DB_SEM_CLASSES_ARRAY'));
             if (!$class_array) {
 
@@ -564,7 +564,7 @@ class SemClass implements ArrayAccess
                     $class_array = $statement->fetchAll(PDO::FETCH_ASSOC);
 
                     if ($class_array) {
-                        $cache = StudipCacheFactory::getCache();
+                        $cache = \Studip\Cache\Factory::getCache();
                         $cache->write('DB_SEM_CLASSES_ARRAY', serialize($class_array));
                     }
                 } catch (PDOException $e) {
@@ -593,7 +593,7 @@ class SemClass implements ArrayAccess
      */
     static public function refreshClasses()
     {
-        StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+        \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
         self::$sem_classes = null;
         return self::getClasses();
     }
diff --git a/lib/classes/SemType.class.php b/lib/classes/SemType.class.php
index 2365c4e8e93..5be1f19cd07 100644
--- a/lib/classes/SemType.class.php
+++ b/lib/classes/SemType.class.php
@@ -68,7 +68,7 @@ class SemType implements ArrayAccess
                 "chdate = UNIX_TIMESTAMP() " .
             "WHERE id = :id ".
         "");
-        StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+        \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
         return $statement->execute([
             'id' => $this->data['id'],
             'name' => $this->data['name'],
@@ -89,7 +89,7 @@ class SemType implements ArrayAccess
                 DELETE FROM sem_types
                 WHERE id = :id
             ");
-            StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+            \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
             return $statement->execute([
                 'id' => $this->data['id']
             ]);
@@ -175,7 +175,7 @@ class SemType implements ArrayAccess
             $db = DBManager::get();
             self::$sem_types = [];
 
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $types_array = unserialize($cache->read('DB_SEM_TYPES_ARRAY'));
             if (!$types_array) {
                 try {
@@ -185,7 +185,7 @@ class SemType implements ArrayAccess
                     $statement->execute();
                     $types_array = $statement->fetchAll(PDO::FETCH_ASSOC);
                     if ($types_array) {
-                        $cache = StudipCacheFactory::getCache();
+                        $cache = \Studip\Cache\Factory::getCache();
                         $cache->write('DB_SEM_TYPES_ARRAY', serialize($types_array));
                     }
                 } catch (PDOException $e) {
@@ -210,7 +210,7 @@ class SemType implements ArrayAccess
 
     static public function refreshTypes() {
         self::$sem_types = null;
-        StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+        \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
         return self::getTypes();
     }
 
diff --git a/lib/classes/Seminar.class.php b/lib/classes/Seminar.class.php
index 054c337d4a1..dda25ee0ffd 100644
--- a/lib/classes/Seminar.class.php
+++ b/lib/classes/Seminar.class.php
@@ -303,7 +303,7 @@ class Seminar
     {
 
         // Caching
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = 'course/undecorated_data/'. $this->id;
 
         if ($filter) {
@@ -745,7 +745,7 @@ class Seminar
         StudipLog::log("SEM_ADD_SINGLEDATE", $this->getId(), $singledate->toString(), 'SingleDateID: '.$singledate->getTerminID());
         // logging <<<<<<
 
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('course/undecorated_data/'. $this->getId());
 
         $this->readSingleDates();
diff --git a/lib/classes/SimpleORMap.class.php b/lib/classes/SimpleORMap.class.php
index 26060860f63..499149c29b4 100644
--- a/lib/classes/SimpleORMap.class.php
+++ b/lib/classes/SimpleORMap.class.php
@@ -277,7 +277,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
                     $relation = $a_config[0] ?? '';
                     $relation_field = $a_config[1] ?? '';
                     if (!$relation) {
-                        list($relation, $relation_field) = explode('_', $a_field);
+                        [$relation, $relation_field] = explode('_', $a_field);
                     }
                     if (!$relation_field || !$relation) {
                         throw new UnexpectedValueException('no relation found for autoget/set additional field: ' . $a_field);
@@ -415,7 +415,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
     public static function tableScheme($db_table)
     {
         if (self::$schemes === null) {
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             self::$schemes = unserialize($cache->read('DB_TABLE_SCHEMES'));
         }
         if (!isset(self::$schemes[$db_table])) {
@@ -434,7 +434,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
             }
             self::$schemes[$db_table]['db_fields'] = $db_fields;
             self::$schemes[$db_table]['pk'] = $pk;
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
             $cache->write('DB_TABLE_SCHEMES', serialize(self::$schemes));
         }
         return isset(self::$schemes[$db_table]);
@@ -446,7 +446,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
      */
     public static function expireTableScheme()
     {
-        StudipCacheFactory::getCache()->expire('DB_TABLE_SCHEMES');
+        \Studip\Cache\Factory::getCache()->expire('DB_TABLE_SCHEMES');
         self::$schemes = null;
         self::$config = [];
     }
@@ -993,7 +993,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
      */
     protected function _getAdditionalValueFromRelation($field)
     {
-        list($relation, $relation_field) = [$this->additional_fields()[$field]['relation'],
+        [$relation, $relation_field] = [$this->additional_fields()[$field]['relation'],
                                                 $this->additional_fields()[$field]['relation_field']];
         if (!array_key_exists($field, $this->additional_data)) {
             $this->_setAdditionalValue($field, $this->getRelationValue($relation, $relation_field));
@@ -1010,7 +1010,7 @@ class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
      */
     protected function _setAdditionalValueFromRelation($field, $value)
     {
-        list($relation, $relation_field) = [$this->additional_fields()[$field]['relation'],
+        [$relation, $relation_field] = [$this->additional_fields()[$field]['relation'],
                 $this->additional_fields()[$field]['relation_field']];
         $this->$relation->$field = $value;
         unset($this->additional_data[$field]);
diff --git a/lib/classes/Siteinfo.php b/lib/classes/Siteinfo.php
index ba7386b8b61..247b836867d 100644
--- a/lib/classes/Siteinfo.php
+++ b/lib/classes/Siteinfo.php
@@ -419,7 +419,7 @@ class SiteinfoMarkupEngine {
     }
 
     function coregroup() {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         if (!($remotefile = $cache->read('coregroup'))) {
             $remotefile = file_get_contents('https://develop.studip.de/studip/extern.php?module=Persons&config_id=8d1dafc3afca2bce6125d57d4119b631&range_id=4498a5bc62d7974d0a0ac3e97aca5296', false, get_default_http_stream_context('https://develop.studip.de'));
             $cache->write('coregroup', $remotefile);
@@ -428,7 +428,7 @@ class SiteinfoMarkupEngine {
     }
 
     function toplist($item) {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         if ($found_in_cache = $cache->read(__METHOD__ . $item)) {
             return $found_in_cache;
         }
@@ -531,7 +531,7 @@ class SiteinfoMarkupEngine {
     }
 
     function indicator($key) {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         if ($found_in_cache = $cache->read(__METHOD__ . $key)) {
             return $found_in_cache;
         }
diff --git a/lib/classes/StudipCache.class.php b/lib/classes/StudipCache.class.php
deleted file mode 100644
index ba929f9bcff..00000000000
--- a/lib/classes/StudipCache.class.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * An interface which has to be implemented by instances returned from
- * StudipCacheFactory#getCache
- *
- * @package    studip
- * @subpackage lib
- *
- * @author     Marco Diedrich (mdiedric@uos)
- * @author     Marcus Lunzenauer (mlunzena@uos.de)
- * @copyright  (c) Authors
- * @since      1.6
- * @license    GPL2 or any later version
- */
-
-interface StudipCache
-{
-    const DEFAULT_EXPIRATION = 12 * 60 * 60; // 12 hours
-
-    /**
-     * Expire item from the cache.
-     *
-     * Example:
-     *
-     *   # expires foo
-     *   $cache->expire('foo');
-     *
-     * @param string $arg a single key
-     */
-    public function expire($arg);
-
-    /**1
-     * Expire all items from the cache.
-     */
-    public function flush();
-
-    /**
-     * Retrieve item from the server.
-     *
-     * Example:
-     *
-     *   # reads foo
-     *   $foo = $cache->reads('foo');
-     *
-     * @param string $arg a single key
-     *
-     * @return mixed    the previously stored data if an item with such a key
-     *                  exists on the server or FALSE on failure.
-     */
-    public function read($arg);
-
-    /**
-     * Store data at the server.
-     *
-     * @param string $name     the item's key.
-     * @param mixed  $content  the item's content (will be serialized if necessary).
-     * @param int    $expires  the item's expiry time in seconds. Optional, defaults to 12h.
-     *
-     * @return bool     returns TRUE on success or FALSE on failure.
-     */
-    public function write($name, $content, $expires = self::DEFAULT_EXPIRATION);
-
-    /**
-     * @return string A translateable display name for this cache class.
-     */
-    public static function getDisplayName(): string;
-
-    /**
-     * Get some statistics from cache, like number of entries, hit rate or
-     * whatever the underlying cache provides.
-     * Results are returned in form of an array like
-     *      "[
-     *          [
-     *              'name' => <displayable name>
-     *              'value' => <value of the current stat>
-     *          ]
-     *      ]"
-     *
-     * @return array
-     */
-    public function getStats(): array;
-
-    /**
-     * Return the Vue component name and props that handle configuration.
-     * The associative array is of the form
-     *  [
-     *      'component' => <Vue component name>,
-     *      'props' => <Properties for component>
-     *  ]
-     *
-     * @return array
-     */
-    public static function getConfig(): array;
-}
diff --git a/lib/classes/StudipCachedArray.php b/lib/classes/StudipCachedArray.php
index 1232d00950d..46723f447fc 100644
--- a/lib/classes/StudipCachedArray.php
+++ b/lib/classes/StudipCachedArray.php
@@ -27,10 +27,10 @@ class StudipCachedArray implements ArrayAccess
      * @param int    $duration Duration in seconds for which the item shall be
      *                         stored
      */
-    public function __construct(string $key, int $duration = StudipCache::DEFAULT_EXPIRATION)
+    public function __construct(string $key, int $duration = \Studip\Cache\Cache::DEFAULT_EXPIRATION)
     {
         $this->key = self::class . "/{$key}";
-        $this->cache = StudipCacheFactory::getCache();
+        $this->cache = \Studip\Cache\Factory::getCache();
         $this->duration = $duration;
         $this->hash = $this->getHash();
 
@@ -116,11 +116,14 @@ class StudipCachedArray implements ArrayAccess
     protected function loadData(string $offset)
     {
         if (!array_key_exists($offset, $this->data)) {
-            $cached = $this->cache->read($this->getCacheKey($offset));
-            $this->data[$offset] = $this->swapNullAndFalse($cached);
+            // Get the cache item from the cache:
+            $item = $this->cache->getItem($this->getCacheKey($offset));
+            if ($item->isHit()) {
+                $this->data[$offset] = $this->swapNullAndFalse($item->get());
+            }
         }
 
-        return $this->data[$offset];
+        return $this->data[$offset] ?? null;
     }
 
     /**
@@ -132,13 +135,12 @@ class StudipCachedArray implements ArrayAccess
      */
     protected function storeData(string $offset): void
     {
-        $data = $this->swapNullAndFalse($this->data[$offset]);
-
-        $this->cache->write(
+        $item = new \Studip\Cache\Item(
             $this->getCacheKey($offset),
-            $data,
+            $this->swapNullAndFalse($this->data[$offset]),
             $this->duration
         );
+        $this->cache->save($item);
     }
 
     /**
diff --git a/lib/classes/StudipDbCache.class.php b/lib/classes/StudipDbCache.class.php
deleted file mode 100644
index 865825e2588..00000000000
--- a/lib/classes/StudipDbCache.class.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-/**
- * StudipCache implementation using database table
- *
- * @package     studip
- * @subpackage  cache
- *
- * @author    Elmar Ludwig <elmar.ludwig@uos.de>
- */
-class StudipDbCache implements StudipCache
-{
-
-    /**
-     * @return string A translateable display name for this cache class.
-     */
-    public static function getDisplayName(): string
-    {
-        return _('Datenbank');
-    }
-
-    /**
-     * Expire item from the cache.
-     *
-     * @param string $arg a single key
-     */
-    public function expire($arg)
-    {
-        $db = DBManager::get();
-
-        $stmt = $db->prepare('DELETE FROM cache WHERE cache_key = ?');
-        $stmt->execute([$arg]);
-    }
-
-    /**
-     * Expire all items from the cache.
-     */
-    public function flush()
-    {
-        $db = DBManager::get();
-
-        $db->exec('TRUNCATE TABLE cache');
-    }
-
-    /**
-     * Delete all expired items from the cache.
-     */
-    public function purge()
-    {
-        $db = DBManager::get();
-
-        $stmt = $db->prepare('DELETE FROM cache WHERE expires < ?');
-        $stmt->execute([time()]);
-    }
-
-    /**
-     * Retrieve item from the server.
-     *
-     * @param string $arg a single key
-     *
-     * @return mixed    the previously stored data if an item with such a key
-     *                  exists on the server or FALSE on failure.
-     */
-    public function read($arg)
-    {
-        $db = DBManager::get();
-
-        $stmt = $db->prepare('SELECT content FROM cache WHERE cache_key = ? AND expires > ?');
-        $stmt->execute([$arg, time()]);
-        $result = $stmt->fetchColumn();
-
-        return $result !== false ? unserialize($result) : false;
-    }
-
-    /**
-     * Store data at the server.
-     *
-     * @param string $name     the item's key.
-     * @param mixed  $content  the item's content (will be serialized if necessary).
-     * @param int    $expired  the item's expiry time in seconds. Optional, defaults to 12h.
-     *
-     * @return bool     returns TRUE on success or FALSE on failure.
-     */
-    public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
-    {
-        $db = DBManager::get();
-
-        $stmt = $db->prepare('REPLACE INTO cache VALUES(?, ?, ?)');
-        return $stmt->execute([$name, serialize($content), time() + $expires]);
-    }
-
-    /**
-     * Return statistics.
-     *
-     * @see StudipCache::getStats()
-     *
-     * @return array|array[]
-     */
-    public function getStats(): array
-    {
-        return [
-            __CLASS__ => [
-                'name' => _('Anzahl Einträge'),
-                'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
-            ]
-        ];
-    }
-
-    /**
-     * Return the Vue component name and props that handle configuration.
-     *
-     * @see StudipCache::getConfig()
-     *
-     * @return array
-     */
-    public static function getConfig(): array
-    {
-        return [
-            'component' => null,
-            'props' => []
-        ];
-    }
-
-}
diff --git a/lib/classes/StudipKing.class.php b/lib/classes/StudipKing.class.php
index 2d1f15ce883..3a61c577f98 100644
--- a/lib/classes/StudipKing.class.php
+++ b/lib/classes/StudipKing.class.php
@@ -63,7 +63,7 @@ class StudipKing {
     private static function get_kings()
     {
         if (self::$kings === null) {
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
 
             # read cache (unserializing a cache miss - FALSE - does not matter)
             $kings = unserialize($cache->read(self::CACHE_KEY));
diff --git a/lib/classes/StudipMemoryCache.class.php b/lib/classes/StudipMemoryCache.class.php
deleted file mode 100644
index d38385a8ec4..00000000000
--- a/lib/classes/StudipMemoryCache.class.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-/**
- * The php memory implementation of the StudipCache interface.
- *
- * @author  Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL2 or any later version
- * @since   Stud.IP 5.0
- */
-class StudipMemoryCache implements StudipCache
-{
-    protected $memory_cache = [];
-
-    /**
-     * Expires just a single key.
-     *
-     * @param  string  the key
-     */
-    public function expire($key)
-    {
-        unset($this->memory_cache[$key]);
-    }
-
-    /**
-     * Expire all items from the cache.
-     */
-    public function flush()
-    {
-        $this->memory_cache = [];
-    }
-
-    /**
-     * Reads just a single key from the cache.
-     *
-     * @param  string  the key
-     *
-     * @return mixed   the corresponding value
-     */
-    public function read($key)
-    {
-        if (!isset($this->memory_cache[$key])) {
-            return false;
-        }
-        if ($this->memory_cache[$key]['expires'] < time()) {
-            $this->expire($key);
-            return false;
-        }
-        return $this->memory_cache[$key]['data'];
-    }
-
-    /**
-     * Store data at the server.
-     *
-     * @param string   the item's key.
-     * @param mixed    the item's content (will be serialized if necessary).
-     * @param int      the item's expiry time in seconds. Defaults to 12h.
-     *
-     * @returns mixed  returns TRUE on success or FALSE on failure.
-     *
-     */
-    public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
-    {
-        $this->memory_cache[$name] = [
-            'expires' => time() + $expires,
-            'data'    => $content,
-        ];
-
-        return true;
-    }
-
-    public static function getDisplayName(): string
-    {
-        return 'Memory cache';
-    }
-
-    public function getStats(): array
-    {
-        return [];
-    }
-
-    public static function getConfig(): array
-    {
-        return [];
-    }
-}
diff --git a/lib/classes/UserLookup.class.php b/lib/classes/UserLookup.class.php
index ddf9276bc0a..bdd9fc368c0 100644
--- a/lib/classes/UserLookup.class.php
+++ b/lib/classes/UserLookup.class.php
@@ -239,7 +239,7 @@ class UserLookup
             return call_user_func(self::$types[$type]['values']);
         }
 
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = "UserLookup/{$type}/values";
         $cached_values = $cache->read($cache_key);
         if ($cached_values) {
diff --git a/lib/classes/assets/SASSCompiler.php b/lib/classes/assets/SASSCompiler.php
index 2dcda2dc50b..0b03a8cd2fa 100644
--- a/lib/classes/assets/SASSCompiler.php
+++ b/lib/classes/assets/SASSCompiler.php
@@ -2,7 +2,7 @@
 namespace Assets;
 
 use Assets;
-use StudipCacheFactory;
+use Studip\Cache\Factory;
 use Studip;
 
 use ScssPhp\ScssPhp\Compiler as ScssCompiler;
@@ -82,7 +82,7 @@ class SASSCompiler implements Compiler
      */
     private function getPrefix()
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = Studip\Cache\Factory::getCache();
 
         $prefix = $cache->read(self::CACHE_KEY);
 
diff --git a/lib/classes/cache/Cache.class.php b/lib/classes/cache/Cache.class.php
new file mode 100644
index 00000000000..366945dbcf6
--- /dev/null
+++ b/lib/classes/cache/Cache.class.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Studip\Cache;
+
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\CacheItemPoolInterface;
+
+/**
+ * An abstract class which has to be extended by instances returned from
+ * \Studip\Cache\Factory#getCache
+ *
+ * @author     Marco Diedrich (mdiedric@uos)
+ * @author     Marcus Lunzenauer (mlunzena@uos.de)
+ * @author     Moritz Strohm <strohm@data-quest.de>
+ * @copyright  (c) Authors
+ * @since      1.6
+ * @license    GPL2 or any later version
+ */
+abstract class Cache implements CacheItemPoolInterface
+{
+    const DEFAULT_EXPIRATION = 12 * 60 * 60; // 12 hours
+
+    /**
+     * @return string A translateable display name for this cache class.
+     */
+    abstract public static function getDisplayName(): string;
+
+    /**
+     * Get some statistics from cache, like number of entries, hit rate or
+     * whatever the underlying cache provides.
+     * Results are returned in form of an array like
+     *      "[
+     *          [
+     *              'name' => <displayable name>
+     *              'value' => <value of the current stat>
+     *          ]
+     *      ]"
+     *
+     * @return array
+     */
+    abstract public function getStats(): array;
+
+    /**
+     * Return the Vue component name and props that handle configuration.
+     * The associative array is of the form
+     *  [
+     *      'component' => <Vue component name>,
+     *      'props' => <Properties for component>
+     *  ]
+     *
+     * @return array
+     */
+    abstract public static function getConfig(): array;
+
+    /**
+     * Expire item from the cache.
+     *
+     * Example:
+     *
+     *   # expires foo
+     *   $cache->expire('foo');
+     *
+     * @param string $arg a single key
+     */
+    abstract public function expire($arg);
+
+    /**
+     * Expire all items from the cache.
+     */
+    abstract public function flush();
+
+    /**
+     * @see CacheItemPoolInterface::getItem
+     */
+    abstract public function getItem($key);
+
+    /**
+     * @see CacheItemPoolInterface::hasItem
+     */
+    abstract public function hasItem($key);
+
+    /**
+     * @var array An array of deferred items that shall be saved only
+     * when commit() is called. This is only used in PSR-6 cache methods.
+     */
+    protected array $deferred_items = [];
+
+    /**
+     * Retrieve item from the server.
+     *
+     * Example:
+     *
+     *   # reads foo
+     *   $foo = $cache->reads('foo');
+     *
+     * @param string $arg a single key
+     *
+     * @return mixed    the previously stored data if an item with such a key
+     *                  exists on the server or FALSE on failure.
+     *
+     * @deprecated To be removed with Stud.IP 7.0.
+     */
+    public function read($arg)
+    {
+        $item = $this->getItem($arg);
+        if ($item->isHit()) {
+            return $item->get();
+        }
+        return false;
+    }
+
+    /**
+     * Store data at the server.
+     *
+     * @param string $name     the item's key.
+     * @param mixed  $content  the item's content (will be serialized if necessary).
+     * @param int    $expires  the item's expiry time in seconds. Optional, defaults to 12h.
+     *
+     * @return bool     returns TRUE on success or FALSE on failure.
+
+     * @deprecated To be removed with Stud.IP 7.0.
+     */
+    public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
+    {
+        $item = new Item($name, $content, $expires);
+
+        return $this->save($item);
+    }
+
+    /**
+     * Calculates the expiration by a cache item. If that cannot be determined,
+     * the default expiration period is returned.
+     *
+     * @param Item $item The item from which to get the expiration time.
+     *
+     * @return int The time from now until the expiration in seconds.
+     */
+    public function getExpiration(CacheItemInterface $item) : int
+    {
+        $expiration = self::DEFAULT_EXPIRATION;
+        if ($item instanceof Item) {
+            $expiration = $item->getExpirationInSeconds();
+        }
+        return $expiration;
+    }
+
+    // PSR-6 CacheItemPoolInterface:
+
+    /**
+     * @see CacheItemPoolInterface::getItems
+     */
+    public function getItems(array $keys = [])
+    {
+        $items = [];
+        foreach ($keys as $key) {
+            $item = $this->getItem($key);
+            if ($item instanceof Item) {
+                $items[] = $item;
+            }
+        }
+        return $items;
+    }
+
+    /**
+     * @see CacheItemPoolInterface::clear
+     */
+    public function clear()
+    {
+        $this->deferred_items = [];
+        $this->flush();
+    }
+
+    /**
+     * @see CacheItemPoolInterface::deleteItem
+     */
+    public function deleteItem($key)
+    {
+        $this->expire($key);
+    }
+
+    /**
+     * @see CacheItemPoolInterface::deleteItems
+     */
+    public function deleteItems(array $keys)
+    {
+        foreach ($keys as $key) {
+            $this->expire($key);
+        }
+    }
+
+    /**
+     * @see CacheItemPoolInterface::saveDeferred
+     */
+    public function saveDeferred(CacheItemInterface $item)
+    {
+        $this->deferred_items[] = $item;
+    }
+
+    /**
+     * @see CacheItemPoolInterface::commit
+     */
+    public function commit()
+    {
+        foreach ($this->deferred_items as $item) {
+            $this->save($item);
+        }
+    }
+}
diff --git a/lib/classes/cache/DbCache.class.php b/lib/classes/cache/DbCache.class.php
new file mode 100644
index 00000000000..3361af05a86
--- /dev/null
+++ b/lib/classes/cache/DbCache.class.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Studip\Cache;
+
+use DBManager;
+
+/**
+ * StudipCache implementation using database table
+ *
+ * @author    Elmar Ludwig <elmar.ludwig@uos.de>
+ */
+class DbCache extends Cache
+{
+    /**
+     * @return string A display name (that can be translated) for this cache class.
+     */
+    public static function getDisplayName(): string
+    {
+        return _('Datenbank');
+    }
+
+    /**
+     * Expire item from the cache.
+     *
+     * @param string $arg a single key
+     */
+    public function expire($arg)
+    {
+        $db = DBManager::get();
+
+        $stmt = $db->prepare('DELETE FROM cache WHERE cache_key = ?');
+        $stmt->execute([$arg]);
+    }
+
+    /**
+     * Expire all items from the cache.
+     */
+    public function flush()
+    {
+        $db = DBManager::get();
+
+        $db->exec('TRUNCATE TABLE cache');
+    }
+
+    /**
+     * Delete all expired items from the cache.
+     */
+    public function purge()
+    {
+        $db = DBManager::get();
+
+        $stmt = $db->prepare('DELETE FROM cache WHERE expires < ?');
+        $stmt->execute([time()]);
+    }
+
+    /**
+     * Return statistics.
+     *
+     * @return array|array[]
+     *@see Cache::getStats()
+     *
+     */
+    public function getStats(): array
+    {
+        return [
+            __CLASS__ => [
+                'name' => _('Anzahl Einträge'),
+                'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
+            ]
+        ];
+    }
+
+    /**
+     * Return the Vue component name and props that handle configuration.
+     *
+     * @return array
+     *@see Cache::getConfig()
+     *
+     */
+    public static function getConfig(): array
+    {
+        return [
+            'component' => null,
+            'props' => []
+        ];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
+    {
+        $query = "SELECT `content`, `expires`
+                  FROM `cache`
+                  WHERE `cache_key` = :key
+                    AND `expires` > UNIX_TIMESTAMP()";
+        $result = DBManager::get()->fetchOne($query, [':key' => $key]);
+
+        $item = new Item($key);
+        if (!empty($result)) {
+            $item->setHit();
+            if ($result['content']) {
+                $item->set(unserialize($result['content']));
+            }
+            if ($result['expires']) {
+                $expiration = new \DateTime();
+                $expiration->setTimestamp($result['expires']);
+                $item->expiresAt($expiration);
+            }
+        }
+        return $item;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
+    {
+        $query = "SELECT 1
+                  FROM `cache`
+                  WHERE `cache_key` = :key
+                    AND `expires` > UNIX_TIMESTAMP()";
+        return (bool) DBManager::get()->fetchColumn($query, [':key' => $key]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function save(\Psr\Cache\CacheItemInterface $item)
+    {
+        $expiration = $this->getExpiration($item);
+        if ($expiration < 1) {
+            // The item would expire immediately.
+            return false;
+        }
+
+        return DBManager::get()->execute(
+            'REPLACE INTO `cache` VALUES (?, ?, ?)',
+            [$item->getKey(), serialize($item->get()), $expiration]
+        );
+    }
+}
diff --git a/lib/classes/cache/Exception.php b/lib/classes/cache/Exception.php
new file mode 100644
index 00000000000..64ee3640873
--- /dev/null
+++ b/lib/classes/cache/Exception.php
@@ -0,0 +1,27 @@
+<?php
+/*
+ * CacheException.class.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2024
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       6.0
+ */
+
+namespace Studip\Cache;
+
+/**
+ * The CacheException class is an implementation of the CacheException interface
+ * of PSR-6 that behaves like a StudipException.
+ */
+class Exception extends \StudipException implements \Psr\Cache\CacheException
+{
+    //Nothing here, since there is nothing to implement.
+}
diff --git a/lib/classes/StudipCacheFactory.class.php b/lib/classes/cache/Factory.class.php
similarity index 81%
rename from lib/classes/StudipCacheFactory.class.php
rename to lib/classes/cache/Factory.class.php
index 77c5973ac1a..b5c835951a6 100644
--- a/lib/classes/StudipCacheFactory.class.php
+++ b/lib/classes/cache/Factory.class.php
@@ -1,4 +1,15 @@
 <?php
+
+namespace Studip\Cache;
+
+use Config;
+use DBSchemaVersion;
+use MessageBox;
+use PageLayout;
+use ReflectionClass;
+use StudipCacheOperation;
+use UnexpectedValueException;
+
 /**
  * This factory retrieves the instance of StudipCache configured for use in
  * this Stud.IP installation.
@@ -12,30 +23,29 @@
  * @since     1.6
  * @license   GPL2 or any later version
  */
-
-class StudipCacheFactory
+class Factory
 {
     /**
      * the default cache class
      *
      * @var string
      */
-    const DEFAULT_CACHE_CLASS = StudipDbCache::class;
+    const DEFAULT_CACHE_CLASS = DbCache::class;
 
     /**
      * singleton instance
      *
-     * @var StudipCache
+     * @var Cache|null
      */
-    private static $cache;
+    private static ?Cache $cache = null;
 
 
     /**
      * config instance
      *
-     * @var Config
+     * @var Config|null
      */
-    private static $config = null;
+    private static ?Config $config = null;
 
 
     /**
@@ -49,7 +59,7 @@ class StudipCacheFactory
      */
     public static function getConfig()
     {
-        return is_null(self::$config) ? Config::getInstance() : self::$config;
+        return self::$config ?? Config::getInstance();
     }
 
 
@@ -58,7 +68,7 @@ class StudipCacheFactory
      *                       determine the class of the implementation of interface
      *                       StudipCache
      */
-    public static function setConfig($config)
+    public static function setConfig(Config $config)
     {
         self::$config = $config;
         self::$cache = NULL;
@@ -77,15 +87,15 @@ class StudipCacheFactory
      *
      * @param bool $apply_proxied_operations Whether or not to apply any
      *                                       proxied (disable this in tests!)
-     * @return StudipCache the cache instance
+     * @return Cache the cache instance
      */
-    public static function getCache($apply_proxied_operations = true)
+    public static function getCache(bool $apply_proxied_operations = true): Cache
     {
-        if (is_null(self::$cache)) {
+        if (self::$cache === null) {
             $proxied = false;
 
             if (!$GLOBALS['CACHING_ENABLE']) {
-                self::$cache = new StudipMemoryCache();
+                self::$cache = new MemoryCache();
 
                 // Proxy cache operations if CACHING_ENABLE is different from the globally set
                 // caching value. This should only be the case in cli mode.
@@ -98,7 +108,7 @@ class StudipCacheFactory
                     $args = self::retrieveConstructorArguments();
 
                     self::$cache = self::instantiateCache($class, $args);
-                } catch (Exception $e) {
+                } catch (\Exception $e) {
                     error_log(__METHOD__ . ': ' . $e->getMessage());
                     PageLayout::addBodyElements(MessageBox::error(__METHOD__ . ': ' . $e->getMessage()));
                     $class = self::DEFAULT_CACHE_CLASS;
@@ -109,7 +119,7 @@ class StudipCacheFactory
             // If proxy should be used, inject it. Otherwise apply pending
             // operations, if any.
             if ($proxied) {
-                self::$cache = new StudipCacheProxy(self::$cache);
+                self::$cache = new Proxy(self::$cache);
             } elseif ($GLOBALS['CACHING_ENABLE'] && $apply_proxied_operations) {
                 // Even if the above condition will try to eliminate most
                 // failures, the following operation still needs to be wrapped
@@ -118,7 +128,7 @@ class StudipCacheFactory
                 // for said operation.
                 try {
                     StudipCacheOperation::apply(self::$cache);
-                } catch (Exception $e) {
+                } catch (\Exception $e) {
                 }
             }
         }
@@ -174,10 +184,11 @@ class StudipCacheFactory
      * memory cache is instantiated, the cache will be wrapped in a wrapper
      * class that uses a memory cache to reduce accesses to the cache.
      *
-     * @param  string $class     the name of the class
-     * @param  array  $arguments an array of arguments to be used by the constructor
+     * @param string $class     the name of the class
+     * @param array  $arguments an array of arguments to be used by the constructor
      *
-     * @return StudipCache  an instance of the specified class
+     * @return Cache  an instance of the specified class
+     * @throws \ReflectionException
      */
     public static function instantiateCache($class, $arguments)
     {
@@ -186,8 +197,8 @@ class StudipCacheFactory
                ? $reflection_class->newInstanceArgs($arguments['config'])
                : $reflection_class->newInstance();
 
-        if ($class !== StudipMemoryCache::class) {
-            return new StudipCacheWrapper($cache);
+        if ($class !== MemoryCache::class) {
+            return new Wrapper($cache);
         }
 
         return $cache;
diff --git a/lib/classes/StudipFileCache.class.php b/lib/classes/cache/FileCache.class.php
similarity index 55%
rename from lib/classes/StudipFileCache.class.php
rename to lib/classes/cache/FileCache.class.php
index 9eae66c1fba..e94395ac461 100644
--- a/lib/classes/StudipFileCache.class.php
+++ b/lib/classes/cache/FileCache.class.php
@@ -1,46 +1,27 @@
 <?php
-# Lifter010: TODO
-// +--------------------------------------------------------------------------+
-// This file is part of Stud.IP
-// StudipFileCache.class.php
-//
-//
-//
-// Copyright (c) 2007 André Noack <noack@data-quest.de>
-// +--------------------------------------------------------------------------+
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or any later version.
-// +--------------------------------------------------------------------------+
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-// +--------------------------------------------------------------------------+
+
+namespace Studip\Cache;
+
+use Config;
+use Exception;
 
 /**
- * StudipCache implementation using files
- *
- * @package     studip
- * @subpackage  cache
+ * Cache implementation using files
  *
  * @author    André Noack <noack@data-quest.de>
- * @version   2
+ * @copyright 2007 André Noack <noack@data-quest.de>
+ * @license GPL2 or any later version
  */
-class StudipFileCache implements StudipCache
+class FileCache extends Cache
 {
-    use StudipCacheKeyTrait;
+    use KeyTrait;
 
     /**
      * full path to cache directory
      *
      * @var string
      */
-    private $dir;
+    private string $dir;
 
     /**
      * @return string A translateable display name for this cache class.
@@ -55,26 +36,28 @@ class StudipFileCache implements StudipCache
      * $CACHING_FILECACHE_PATH or is set to
      * $TMP_PATH/studip_cache
      *
-     * @param string the path to use
-     * @return void
-     * @throws exception if the directory does not exist or could not be
+     * @param string $path the path to use
+     * @throws Exception if the directory does not exist or could not be
      *         created
      */
-    public function __construct($path = '')
+    public function __construct(string $path = '')
     {
         $this->dir = $path
-                  ?: (Config::get()->SYSTEMCACHE['type'] == 'StudipFileCache' ?
-                        Config::get()->SYSTEMCACHE['config']['path'] : '')
+                  ?: (
+                      Config::get()->SYSTEMCACHE['type'] === self::class
+                          ? Config::get()->SYSTEMCACHE['config']['path']
+                          : ''
+                  )
                   ?: $GLOBALS['CACHING_FILECACHE_PATH']
                   ?: ($GLOBALS['TMP_PATH'] . '/' . 'studip_cache');
         $this->dir = rtrim($this->dir, '\\/') . '/';
 
         if (!is_dir($this->dir) && !@mkdir($this->dir, 0700)) {
-            throw new Exception('Could not create directory: ' . $this->dir);
+            throw new \Exception('Could not create directory: ' . $this->dir);
         }
 
         if (!is_writable($this->dir)) {
-            throw new Exception('Can not write to directory: ' . $this->dir);
+            throw new \Exception('Can not write to directory: ' . $this->dir);
         }
     }
 
@@ -91,9 +74,11 @@ class StudipFileCache implements StudipCache
     /**
      * expire cache item
      *
-     * @see StudipCache::expire()
      * @param string $arg
+     *
      * @return void
+     * @throws Exception
+     * @see Cache::expire()
      */
     public function expire($arg)
     {
@@ -112,62 +97,21 @@ class StudipFileCache implements StudipCache
         rmdirr($this->dir);
     }
 
-    /**
-     * retrieve cache item from filesystem
-     * tests first if item is expired
-     *
-     * @see StudipCache::read()
-     * @param string $arg a cache key
-     * @return string|bool
-     */
-    public function read($arg)
-    {
-        $key = $this->getCacheKey($arg);
-
-        if ($file = $this->check($key)){
-            $f = @fopen($file, 'rb');
-            if ($f) {
-                @flock($f, LOCK_SH);
-                $result = stream_get_contents($f);
-                @fclose($f);
-            }
-            return unserialize($result);
-        }
-        return false;
-    }
-
-    /**
-     * store data as cache item in filesystem
-     *
-     * @see StudipCache::write()
-     * @param string $arg a cache key
-     * @param mixed $content data to store
-     * @param int $expire expiry time in seconds, default 12h
-     * @return int|bool the number of bytes that were written to the file,
-     *         or false on failure
-     */
-    public function write($arg, $content, $expire = self::DEFAULT_EXPIRATION)
-    {
-        $key = $this->getCacheKey($arg);
-
-        $this->expire($key);
-        $file = $this->getPathAndFile($key, $expire);
-        return @file_put_contents($file, serialize($content), LOCK_EX);
-    }
-
     /**
      * checks if specified cache item is expired
      * if expired the cache file is deleted
      *
      * @param string $key a cache key to check
-     * @return string|bool the path to the cache file or false if expired
+     *
+     * @return array|bool the path to the cache file or false if expired
+     * @throws Exception
      */
     private function check($key)
     {
         if ($file = $this->getPathAndFile($key)){
-            list($id, $expire) = explode('-', basename($file));
+            [$id, $expire] = explode('-', basename($file));
             if (time() < $expire) {
-                return $file;
+                return [$file, $expire];
             } else {
                 @unlink($file);
             }
@@ -183,16 +127,18 @@ class StudipFileCache implements StudipCache
      * the filename is constructed from the hashed cache key
      * and the timestamp of expiration
      *
-     * @param string $key a cache key
-     * @param int $expire expiry time in seconds
+     * @param string   $key    a cache key
+     * @param int|null $expire expiry time in seconds
+     *
      * @return string|bool full path to cache item or false on failure
+     * @throws Exception
      */
-    private function getPathAndFile($key, $expire = null)
+    private function getPathAndFile(string $key, ?int $expire = null): bool|string
     {
         $id = hash('md5', $key);
         $path = $this->dir . mb_substr($id, 0, 2);
         if (!is_dir($path) && !@mkdir($path, 0700)) {
-            throw new Exception('Could not create directory: ' . $path);
+            throw new \Exception('Could not create directory: ' . $path);
         }
         if (!is_null($expire)){
             return $path . '/' . $id . '-' . (time() + $expire);
@@ -208,16 +154,17 @@ class StudipFileCache implements StudipCache
     /**
      * purges expired entries from the cache directory
      *
-     * @param bool echo messages if set to false
+     * @param bool $be_quiet echo messages if set to false
+     *
      * @return int the number of deleted files
      */
-    public function purge($be_quiet = true)
+    public function purge(bool $be_quiet = true): int
     {
         $now = time();
         $deleted = 0;
         foreach (@glob($this->dir . '*', GLOB_ONLYDIR) as $current_dir){
             foreach (@glob("{$current_dir}/*") as $file){
-                list($id, $expire) = explode('-', basename($file));
+                [$id, $expire] = explode('-', basename($file));
                 if ($expire < $now) {
                     if (@unlink($file)) {
                         ++$deleted;
@@ -225,8 +172,8 @@ class StudipFileCache implements StudipCache
                             echo "File: {$file} deleted.\n";
                         }
                     }
-                } else if (!$be_quiet){
-                    echo "File: {$file} expires on " . strftime('%x %X', $expire) . "\n";
+                } else if (!$be_quiet) {
+                    echo "File: {$file} expires on " . date('Y-m-d H:i:s', $expire) . "\n";
                 }
             }
         }
@@ -243,7 +190,7 @@ class StudipFileCache implements StudipCache
         return [
             __CLASS__ => [
                 'name' => _('Anzahl Einträge'),
-                'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
+                'value' => \DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
             ]
         ];
     }
@@ -273,4 +220,58 @@ class StudipFileCache implements StudipCache
         ];
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
+    {
+        $real_key = $this->getCacheKey($key);
+
+        $item = new \Studip\Cache\Item($key);
+
+        $file_data = $this->check($real_key);
+        if ($file_data) {
+            $file = $file_data[0];
+            $expire = $file_data[1];
+            $f = @fopen($file, 'rb');
+            if ($f) {
+                @flock($f, LOCK_SH);
+                $result = stream_get_contents($f);
+                @fclose($f);
+            }
+            $item->setHit();
+            $item->set(unserialize($result));
+            $expiration = new \DateTime();
+            $expiration->setTimestamp($expire);
+            $item->expiresAt($expiration);
+        }
+        return $item;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
+    {
+        $real_key = $this->getCacheKey($key);
+        $file_data = $this->check($real_key);
+        return $file_data !== false;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function save(\Psr\Cache\CacheItemInterface $item)
+    {
+        $expiration = $this->getExpiration($item);
+        if ($expiration < 1) {
+            //The item would expire immediately.
+            return false;
+        }
+
+        $real_key = $this->getCacheKey($item->getKey());
+        $this->expire($real_key);
+        $file = $this->getPathAndFile($real_key, $expiration);
+        return @file_put_contents($file, serialize($item->get()), LOCK_EX);
+    }
 }
diff --git a/lib/classes/cache/InvalidCacheArgumentException.php b/lib/classes/cache/InvalidCacheArgumentException.php
new file mode 100644
index 00000000000..e85c6b2c138
--- /dev/null
+++ b/lib/classes/cache/InvalidCacheArgumentException.php
@@ -0,0 +1,28 @@
+<?php
+/*
+ * CacheException.class.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2024
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       6.0
+ */
+
+namespace Studip\Cache;
+
+
+/**
+ * The InvalidCacheArgumentException is an implementation of the InvalidArgumentException interface
+ *  of PSR-6 that behaves like a StudipException.
+ */
+class InvalidCacheArgumentException extends \StudipException implements \Psr\Cache\InvalidArgumentException
+{
+    //Nothing here, since there is nothing to implement.
+}
diff --git a/lib/classes/cache/Item.class.php b/lib/classes/cache/Item.class.php
new file mode 100644
index 00000000000..1a09f1ddd47
--- /dev/null
+++ b/lib/classes/cache/Item.class.php
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Item.class.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2024
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       6.0
+ */
+
+namespace Studip\Cache;
+
+use DateInterval;
+use DateTime;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * \Studip\Cache\CacheItem implements the CacheItemInterface of PSR-6. It holds the value and the
+ * key of a cache item and also provides additional methods to get the expiration of the item.
+ */
+class Item implements CacheItemInterface
+{
+    /**
+     * @var string The key of the item in the cache.
+     */
+    protected string $key;
+
+    /**
+     * @var mixed The value of the item.
+     */
+    protected mixed $value;
+
+    /**
+     * @var DateTime|null The expiration as DateTime object or null if the expiration is not defined.
+     */
+    protected ?DateTime $expiration = null;
+
+    /**
+     * @var bool An indicator whether the item has been found in the cache (true) or not (false).
+     */
+    protected bool $cache_hit = false;
+
+    /**
+     * The constructor of \Studip\Cache\CacheItem.
+     *
+     * @param string $key The key of the item in the cache.
+     * @param mixed $value The value of the item.
+     * @param int|null $expiration The expiration of the item in seconds, if applicable.
+     * @param bool $cache_hit Whether the item shall be constructed as cache hit (true) or not (false).
+     *
+     */
+    public function __construct(
+        string $key,
+        mixed $value = null,
+        ?int $expiration = null,
+        bool $cache_hit = false
+    ) {
+        $this->key         = $key;
+        $this->value       = $value;
+        $this->cache_hit   = $cache_hit;
+        $this->expiresAfter($expiration);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function get()
+    {
+        return $this->value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function isHit()
+    {
+        return $this->cache_hit;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function set($value)
+    {
+        $this->value = $value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function expiresAt($expiration)
+    {
+        $this->expiration = $expiration;
+        return $this;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function expiresAfter($time)
+    {
+        $this->expiration = new DateTime();
+        if ($time instanceof DateInterval) {
+            $this->expiration = $this->expiration->add($time);
+        } elseif (is_integer($time)) {
+            $this->expiration->setTimestamp(time() + $time);
+        } else {
+            $this->expiration->setTimestamp(time() + Cache::DEFAULT_EXPIRATION);
+        }
+        return $this;
+    }
+
+    // \Studip\Cache\CacheItem specific methods:
+
+    /**
+     * Sets the item to be a cache hit.
+     *
+     * @return void
+     */
+    public function setHit() : void
+    {
+        $this->cache_hit = true;
+    }
+
+    /**
+     * Returns the expiration, if set.
+     *
+     * @return DateTime|null A DateTime object with the expiration date and time
+     *     or null if the expiration is not defined.
+     */
+    public function getExpiration() : ?DateTime
+    {
+        return $this->expiration;
+    }
+
+    /**
+     * Returns the seconds from the current timestamp until the expiration of the item.
+     *
+     * @return int The seconds until the item expires
+     */
+    public function getExpirationInSeconds() : int
+    {
+        if ($this->expiration) {
+            return $this->expiration->getTimestamp() - time();
+        }
+        return 0;
+    }
+}
diff --git a/lib/classes/StudipCacheKeyTrait.php b/lib/classes/cache/KeyTrait.php
similarity index 77%
rename from lib/classes/StudipCacheKeyTrait.php
rename to lib/classes/cache/KeyTrait.php
index 62eb142f3b0..021aaba3966 100644
--- a/lib/classes/StudipCacheKeyTrait.php
+++ b/lib/classes/cache/KeyTrait.php
@@ -1,4 +1,6 @@
 <?php
+namespace Studip\Cache;
+
 /**
  * Trait for unique cache hashes per key for each system based on db configuration which should
  * be sufficient to eliminate cache mishaps.
@@ -9,9 +11,9 @@
  * @subpackage  cache
  * @since       Stud.IP 5.0
  */
-trait StudipCacheKeyTrait
+trait KeyTrait
 {
-    protected $cache_prefix = null;
+    protected ?string $cache_prefix = null;
 
     /**
      * Returns a prefix cache key based on db configuration.
@@ -19,11 +21,11 @@ trait StudipCacheKeyTrait
      * @param  string $offset
      * @return string
      */
-    protected function getCacheKey($offset)
+    protected function getCacheKey(string $offset): string
     {
         if ($this->cache_prefix === null) {
             $this->cache_prefix = md5("{$GLOBALS['DB_STUDIP_HOST']}|{$GLOBALS['DB_STUDIP_DATABASE']}");
         }
-        return "{$this->cache_prefix}/{$offset}";
+        return "$this->cache_prefix/$offset";
     }
 }
diff --git a/lib/classes/StudipMemcachedCache.php b/lib/classes/cache/MemcachedCache.class.php
similarity index 51%
rename from lib/classes/StudipMemcachedCache.php
rename to lib/classes/cache/MemcachedCache.class.php
index 0e44dd57e1c..5d9033be9b8 100644
--- a/lib/classes/StudipMemcachedCache.php
+++ b/lib/classes/cache/MemcachedCache.class.php
@@ -1,31 +1,23 @@
 <?php
 
-/**
- * Copyright (C) 2007 - Marcus Lunzenauer <mlunzena@uos.de>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- */
+namespace Studip\Cache;
 
+use Memcached;
+use Psr\Cache\CacheItemInterface;
 
 /**
  * Cache implementation using memcached.
  *
- * @package     studip
- * @subpackage  cache
- *
- * @author    mlunzena
+ * @author    Marcus Lunzenauer <mlunzena@uos.de>
  * @copyright (c) Authors
+ * @license GPL2 or any later version
  * @since   5.0
  */
-
-class StudipMemcachedCache implements StudipCache
+class MemcachedCache extends Cache
 {
-    use StudipCacheKeyTrait;
+    use KeyTrait;
 
-    private $memcache;
+    private Memcached $memcache;
 
     /**
      * @return string A translateable display name for this cache class.
@@ -38,18 +30,18 @@ class StudipMemcachedCache implements StudipCache
     public function __construct($servers)
     {
         if (!extension_loaded('memcached')) {
-            throw new Exception('Memcache extension missing.');
+            throw new \Exception('Memcache extension missing.');
         }
 
-        $prefix = Config::get()->STUDIP_INSTALLATION_ID;
-        $this->memcache = new Memcached('studip' . $prefix ? '-' . $prefix : '');
+        $prefix = \Config::get()->STUDIP_INSTALLATION_ID;
+        $this->memcache = new Memcached('studip' . ($prefix ? '-' . $prefix : ''));
 
         if (count($this->memcache->getServerList()) === 0) {
             foreach ($servers as $server) {
                 $status = $this->memcache->addServer($server['hostname'], (int) $server['port']);
 
                 if (!$status) {
-                    throw new Exception("Could not add server: {$server['hostname']} @ port {$server['port']}");
+                    throw new \Exception("Could not add server: {$server['hostname']} @ port {$server['port']}");
                 }
             }
         }
@@ -80,40 +72,6 @@ class StudipMemcachedCache implements StudipCache
         $this->memcache->flush();
     }
 
-    /**
-     * Retrieve item from the server.
-     *
-     * Example:
-     *
-     *   # reads foo
-     *   $foo = $cache->reads('foo');
-     *
-     * @param   string $arg a single key
-     * @returns mixed  the previously stored data if an item with such a key
-     *                 exists on the server or FALSE on failure.
-     */
-    public function read($arg)
-    {
-        $key = $this->getCacheKey($arg);
-        return $this->memcache->get($key);
-    }
-
-    /**
-     * Store data at the server.
-     *
-     * @param string $arg the item's key.
-     * @param string $content the item's content.
-     * @param int $expire the item's expiry time in seconds. Defaults to 12h.
-     *
-     * @returns mixed  returns TRUE on success or FALSE on failure.
-     *
-     */
-    public function write($arg, $content, $expire = self::DEFAULT_EXPIRATION)
-    {
-        $key = $this->getCacheKey($arg);
-        return $this->memcache->set($key, $content, $expire);
-    }
-
     /**
      * Return statistics.
      *
@@ -123,20 +81,19 @@ class StudipMemcachedCache implements StudipCache
      */
     public function getStats(): array
     {
-        $stats = $this->memcache->getStats();
-        return $stats;
+        return $this->memcache->getStats();
     }
 
     /**
      * Return the Vue component name and props that handle configuration.
      *
-     * @see StudipCache::getConfig()
+     * @see Cache::getConfig()
      *
      * @return array
      */
     public static function getConfig(): array
     {
-        $currentCache = Config::get()->SYSTEMCACHE;
+        $currentCache = \Config::get()->SYSTEMCACHE;
 
         // Set default config for this cache
         $currentConfig = [
@@ -154,4 +111,41 @@ class StudipMemcachedCache implements StudipCache
         ];
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
+    {
+        $item = new Item($key);
+        $value = $this->memcache->get($this->getCacheKey($key));
+        if ($this->memcache->getResultCode() !== Memcached::RES_NOTFOUND) {
+            // Set the value, even if it is the boolean value false:
+            $item->setHit();
+            $item->set($value);
+        }
+        return $item;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
+    {
+        return $this->memcache->checkKey($this->getCacheKey($key));
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function save(CacheItemInterface $item)
+    {
+        $expiration = $this->getExpiration($item);
+        if ($expiration < 1) {
+            // The item would expire immediately.
+            return false;
+        }
+
+        $real_key = $this->getCacheKey($item->getKey());
+        return $this->memcache->set($real_key, $item->get(), $expiration);
+    }
 }
diff --git a/lib/classes/cache/MemoryCache.class.php b/lib/classes/cache/MemoryCache.class.php
new file mode 100644
index 00000000000..93f6bbd66aa
--- /dev/null
+++ b/lib/classes/cache/MemoryCache.class.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Studip\Cache;
+
+use DateTime;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * The php memory implementation of the StudipCache interface.
+ *
+ * @author  Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since   Stud.IP 5.0
+ */
+class MemoryCache extends Cache
+{
+    protected array $memory_cache = [];
+
+    /**
+     * Expires just a single key.
+     *
+     * @param  string  the key
+     */
+    public function expire($key)
+    {
+        unset($this->memory_cache[$key]);
+    }
+
+    /**
+     * Expire all items from the cache.
+     */
+    public function flush()
+    {
+        $this->memory_cache = [];
+    }
+
+    public static function getDisplayName(): string
+    {
+        return 'Memory cache';
+    }
+
+    public function getStats(): array
+    {
+        return [];
+    }
+
+    public static function getConfig(): array
+    {
+        return [];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
+    {
+        $item = new Item($key);
+        if (!isset($this->memory_cache[$key])) {
+            return $item;
+        }
+        if ($this->memory_cache[$key]['expires'] < time()) {
+            $this->expire($key);
+            return $item;
+        }
+        $item->setHit();
+        $item->set($this->memory_cache[$key]['data']);
+        if (!empty($this->memory_cache[$key]['expires'])) {
+            $expiration = new DateTime();
+            $expiration->setTimestamp($this->memory_cache[$key]['expires']);
+            $item->expiresAt($expiration);
+        }
+        return $item;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
+    {
+        return isset($this->memory_cache[$key])
+            && $this->memory_cache[$key]['expires'] < time();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function save(CacheItemInterface $item)
+    {
+        $expiration = $this->getExpiration($item);
+
+        $this->memory_cache[$item->getKey()] = [
+            'expires' => $expiration + time(),
+            'data'    => $item->get(),
+        ];
+
+        return true;
+    }
+}
diff --git a/lib/classes/StudipCacheProxy.php b/lib/classes/cache/Proxy.class.php
similarity index 57%
rename from lib/classes/StudipCacheProxy.php
rename to lib/classes/cache/Proxy.class.php
index 686f8129d68..6791fb72c93 100644
--- a/lib/classes/StudipCacheProxy.php
+++ b/lib/classes/cache/Proxy.class.php
@@ -1,4 +1,9 @@
 <?php
+
+namespace Studip\Cache;
+
+use StudipCacheOperation;
+
 /**
  * Proxies a StudipCache and stores the expire operation in the database.
  * These operations are lateron applied to the cache they should have
@@ -8,18 +13,18 @@
  * @license GPL2 or any later version
  * @since   Stud.IP 3.3
  */
-class StudipCacheProxy implements StudipCache
+class Proxy extends Cache
 {
-    protected $actual_cache;
-    protected $proxy_these;
+    protected Cache $actual_cache;
+    protected array $proxy_these;
 
     /**
-     * @param StudipCache $cache       The actual cache object
-     * @param mixed       $proxy_these List of operations to proxy (should be
-     *                                 an array but a space seperated string
-     *                                 is also valid)
+     * @param Cache $cache       The actual cache object
+     * @param mixed $proxy_these List of operations to proxy (should be an
+     *                           array but a space seperated string is also
+     *                           valid)
      */
-    public function __construct(StudipCache $cache, $proxy_these = ['expire'])
+    public function __construct(Cache $cache, $proxy_these = ['expire'])
     {
         if (!is_array($proxy_these)) {
             $proxy_these = words($proxy_these);
@@ -43,7 +48,7 @@ class StudipCacheProxy implements StudipCache
                 $operation = new StudipCacheOperation([$key, 'expire']);
                 $operation->parameters = serialize([]);
                 $operation->store();
-            } catch (Exception $e) {
+            } catch (\Exception $e) {
             }
         }
 
@@ -60,58 +65,58 @@ class StudipCacheProxy implements StudipCache
                 $operation = new StudipCacheOperation(['', 'flush']);
                 $operation->parameters = serialize([]);
                 $operation->store();
-            } catch (Exception $e) {
+            } catch (\Exception $e) {
             }
         }
 
         return $this->actual_cache->flush();
     }
 
-    /**
-     * Reads just a single key from the cache.
-     *
-     * @param  string $key The item's key
-     * @return mixed The corresponding value
-     */
-    public function read($key)
+    public static function getDisplayName(): string
     {
-        return $this->actual_cache->read($key);
+        return static::class;
     }
 
-    /**
-     * Store data at the server.
-     *
-     * @param string $key     The item's key
-     * @param string $content The item's conten
-     * @param int    $expires The item's expiry time in seconds, defaults to 12h
-     * @return bool  Returns TRUE on success or FALSE on failure
-     */
-    public function write($key, $content, $expires = self::DEFAULT_EXPIRATION)
+    public function getStats(): array
     {
-        if (in_array('write', $this->proxy_these)) {
-            try {
-                $operation = new StudipCacheOperation([$key, 'write']);
-                $operation->parameters = serialize([$content, $expires]);
-                $operation->store();
-            } catch (Exception $e) {
-            }
-        }
+        return $this->actual_cache->getStats();
+    }
 
-        return $this->actual_cache->write($key, $content, $expires);
+    public static function getConfig(): array
+    {
+        return [];
     }
 
-    public static function getDisplayName(): string
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
     {
-        return static::class;
+        return $this->actual_cache->getItem($key);
     }
 
-    public function getStats(): array
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
     {
-        return $this->actual_cache->getStats();
+        return $this->actual_cache->hasItem($key);
     }
 
-    public static function getConfig(): array
+    /**
+     * @inheritDoc
+     */
+    public function save(\Psr\Cache\CacheItemInterface $item)
     {
-        return [];
+        if (in_array('save', $this->proxy_these)) {
+            try {
+                $operation = new StudipCacheOperation([$item->getKey(), 'save']);
+                $operation->parameters = serialize([$item]);
+                $operation->store();
+            } catch (\Exception $e) {
+            }
+        }
+
+        return $this->actual_cache->save($item);
     }
 }
diff --git a/lib/classes/StudipRedisCache.class.php b/lib/classes/cache/RedisCache.class.php
similarity index 73%
rename from lib/classes/StudipRedisCache.class.php
rename to lib/classes/cache/RedisCache.class.php
index 7b9570bdd0c..9ea87114622 100644
--- a/lib/classes/StudipRedisCache.class.php
+++ b/lib/classes/cache/RedisCache.class.php
@@ -1,4 +1,15 @@
 <?php
+
+namespace Studip\Cache;
+
+use BadMethodCallException;
+use Config;
+use DateTime;
+use Exception;
+use Psr\Cache\CacheItemInterface;
+use Redis;
+use RedisException;
+
 /**
  * Cache implementation using redis.
  *
@@ -8,9 +19,9 @@
  * @subpackage  cache
  * @since       Stud.IP 5.0
  */
-class StudipRedisCache implements StudipCache
+class RedisCache extends Cache
 {
-    use StudipCacheKeyTrait;
+    use KeyTrait;
 
     private $redis;
 
@@ -28,6 +39,8 @@ class StudipRedisCache implements StudipCache
      * @param string $hostname Hostname of redis server
      * @param int    $port     Port of redis server
      * @param string $auth     Optional auth token/password
+     *
+     * @throws RedisException
      */
     public function __construct($hostname, $port, string $auth = '')
     {
@@ -73,41 +86,6 @@ class StudipRedisCache implements StudipCache
         $this->redis->unlink($key);
     }
 
-    /**
-     * Retrieve item from the server.
-     *
-     * Example:
-     *
-     *   # reads foo
-     *   $foo = $cache->reads('foo');
-     *
-     * @param  string $arg a single key
-     * @return mixed  the previously stored data if an item with such a key
-     *                exists on the server or FALSE on failure.
-     */
-    public function read($arg)
-    {
-        $key = $this->getCacheKey($arg);
-
-        $result = $this->redis->get($key);
-
-        return ($result === null) ? null : unserialize($result);
-    }
-
-    /**
-     * Store data at the server.
-     *
-     * @param string   the item's key.
-     * @param string   the item's content.
-     * @param int      the item's expiry time in seconds. Defaults to 12h.
-     * @return mixed  returns TRUE on success or FALSE on failure.
-     */
-    public function write($name, $content, $expire = self::DEFAULT_EXPIRATION)
-    {
-        $key = $this->getCacheKey($name);
-        return $this->redis->setEx($key, $expire, serialize($content));
-    }
-
     /**
      * Expire all items from the cache.
      */
@@ -174,4 +152,47 @@ class StudipRedisCache implements StudipCache
             'props' => $currentConfig
         ];
     }
+
+    /**
+     * @inheritDoc
+     */
+    public function getItem($key)
+    {
+        $item = new Item($key);
+        $real_key = $this->getCacheKey($key);
+        $result = $this->redis->get($real_key);
+        if ($result === null) {
+            return $item;
+        }
+        $item->setHit();
+        $item->set(unserialize($result));
+        $expiration = new DateTime();
+        $expiration->setTimestamp($this->redis->expiretime($real_key));
+        $item->expiresAt($expiration);
+        return $item;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function hasItem($key)
+    {
+        $real_key = $this->getCacheKey($key);
+        return $this->redis->get($real_key) !== null;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function save(CacheItemInterface $item)
+    {
+        $expiration = $this->getExpiration($item);
+        if ($expiration < 1) {
+            // The item would expire immediately.
+            return false;
+        }
+
+        $real_key = $this->getCacheKey($item->getKey());
+        return $this->redis->setEx($real_key, $expiration, serialize($item->get()));
+    }
 }
diff --git a/lib/classes/StudipCacheWrapper.php b/lib/classes/cache/Wrapper.class.php
similarity index 56%
rename from lib/classes/StudipCacheWrapper.php
rename to lib/classes/cache/Wrapper.class.php
index 6c75c01a120..744893270b1 100644
--- a/lib/classes/StudipCacheWrapper.php
+++ b/lib/classes/cache/Wrapper.class.php
@@ -1,5 +1,9 @@
 <?php
 
+namespace Studip\Cache;
+
+use Psr\Cache\CacheItemInterface;
+
 /**
  * The cache wrapper wraps a memory cache around another cache. This should
  * reduce the accesses to the actual cache.
@@ -8,17 +12,15 @@
  * @license GPL2 or any later version
  * @since Stud.IP 5.4
  */
-class StudipCacheWrapper implements StudipCache
+class Wrapper extends Cache
 {
-    const DEFAULT_MEMORY_EXPIRATION = 60;
-
-    protected $actual_cache;
-    protected $memory_cache;
+    protected Cache $actual_cache;
+    protected MemoryCache $memory_cache;
 
-    public function __construct(StudipCache $actual_cache)
+    public function __construct(Cache $actual_cache)
     {
         $this->actual_cache = $actual_cache;
-        $this->memory_cache = new StudipMemoryCache();
+        $this->memory_cache = new MemoryCache();
     }
 
     /**
@@ -39,47 +41,55 @@ class StudipCacheWrapper implements StudipCache
         $this->actual_cache->flush();
     }
 
+    public static function getDisplayName(): string
+    {
+        return static::class;
+    }
+
+    public function getStats(): array
+    {
+        return $this->actual_cache->getStats();
+    }
+
+    public static function getConfig(): array
+    {
+        return [];
+    }
+
     /**
-     * @inheritdoc
+     * @inheritDoc
      */
-    public function read($arg)
+    public function getItem($key)
     {
-        $cached = $this->memory_cache->read($arg);
-        if ($cached !== false) {
+        $cached = $this->memory_cache->getItem($key);
+        if ($cached->isHit()) {
             return $cached;
         }
 
-        $cached = $this->actual_cache->read($arg);
-        if ($cached !== false) {
-            $this->memory_cache->write($arg, $cached, self::DEFAULT_MEMORY_EXPIRATION);
+        $cached = $this->actual_cache->getItem($key);
+        if ($cached->isHit()) {
+            $this->memory_cache->save($cached);
         }
         return $cached;
     }
 
     /**
-     * @inheritdoc
+     * @inheritDoc
      */
-    public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
+    public function hasItem($key)
     {
-        if ($this->actual_cache->write($name, $content, $expires)) {
-            return $this->memory_cache->write($name, $content, $expires);
-        } else {
-            return false;
-        }
-    }
-
-    public static function getDisplayName(): string
-    {
-        return static::class;
-    }
-
-    public function getStats(): array
-    {
-        return $this->actual_cache->getStats();
+        return $this->actual_cache->hasItem($key);
     }
 
-    public static function getConfig(): array
+    /**
+     * @inheritDoc
+     */
+    public function save(CacheItemInterface $item)
     {
-        return [];
+        if ($this->actual_cache->save($item)) {
+            return $this->memory_cache->save($item);
+        } else {
+            return false;
+        }
     }
 }
diff --git a/lib/classes/cas/CAS_PGTStorage_Cache.php b/lib/classes/cas/CAS_PGTStorage_Cache.php
index 284b59134f3..61ce9fa6ad0 100644
--- a/lib/classes/cas/CAS_PGTStorage_Cache.php
+++ b/lib/classes/cas/CAS_PGTStorage_Cache.php
@@ -43,7 +43,7 @@ class CAS_PGTStorage_Cache extends CAS_PGTStorage_AbstractStorage
      */
     public function write($pgt, $pgt_iou)
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = 'pgtiou/' . $pgt_iou;
         return $cache->write($cache_key, $pgt);
     }
@@ -58,7 +58,7 @@ class CAS_PGTStorage_Cache extends CAS_PGTStorage_AbstractStorage
      */
     public function read($pgt_iou)
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = 'pgtiou/' . $pgt_iou;
         $pgt = $cache->read($cache_key);
         $cache->expire($cache_key);
diff --git a/lib/cronjobs/purge_cache.class.php b/lib/cronjobs/purge_cache.class.php
index 4e5682f2b26..d2e3697db8d 100644
--- a/lib/cronjobs/purge_cache.class.php
+++ b/lib/cronjobs/purge_cache.class.php
@@ -70,7 +70,7 @@ class PurgeCacheJob extends CronJob
      */
     public function setUp()
     {
-        require_once 'lib/classes/StudipFileCache.class.php';
+        require_once 'lib/classes/cache/FileCache.class.php';
     }
 
     /**
@@ -86,7 +86,7 @@ class PurgeCacheJob extends CronJob
      */
     public function execute($last_result, $parameters = [])
     {
-        $cache = new StudipFileCache();
+        $cache = new \Studip\Cache\FileCache();
         $cache->purge(empty($parameters['verbose']));
     }
 }
diff --git a/lib/functions.php b/lib/functions.php
index 0e806aa7f47..7d9f4f2defb 100644
--- a/lib/functions.php
+++ b/lib/functions.php
@@ -636,7 +636,7 @@ function get_users_online($active_time = 5, $name_format = 'full_rev')
  */
 function get_users_online_count($active_time = 10)
 {
-    $cache = StudipCacheFactory::getCache();
+    $cache = \Studip\Cache\Factory::getCache();
     $online_count = $cache->read("online_count/{$active_time}");
     if ($online_count === false) {
         $query = "SELECT COUNT(*) FROM user_online
diff --git a/lib/models/CourseDate.class.php b/lib/models/CourseDate.class.php
index 59fbb9c3a25..eadeb0a020e 100644
--- a/lib/models/CourseDate.class.php
+++ b/lib/models/CourseDate.class.php
@@ -364,7 +364,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
         // load room-booking, if any
         $this->room_booking;
 
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('course/undecorated_data/'. $this->range_id);
         return parent::store();
     }
@@ -376,7 +376,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
      */
     public function delete()
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('course/undecorated_data/'. $this->range_id);
         return parent::delete();
     }
@@ -433,7 +433,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
             $folders = array_merge($folders, $topic->folders->getArrayCopy());
         }
         foreach ($folders as $folder) {
-            list($files, $typed_folders) = array_values(FileManager::getFolderFilesRecursive($folder->getTypedFolder(), $user_id));
+            [$files, $typed_folders] = array_values(FileManager::getFolderFilesRecursive($folder->getTypedFolder(), $user_id));
             foreach ($files as $file) {
                 $all_files[$file->id] = $file;
             }
diff --git a/lib/models/OERMaterial.php b/lib/models/OERMaterial.php
index 6ec7de84485..8411d149d84 100644
--- a/lib/models/OERMaterial.php
+++ b/lib/models/OERMaterial.php
@@ -164,7 +164,7 @@ class OERMaterial extends SimpleORMap
     public static function fetchRemoteSearch($text, $tag = false)
     {
         $cache_name = "oer_remote_searched_for_".md5($text)."_".($tag ? 1 : 0);
-        $already_searched = (bool) StudipCacheFactory::getCache()->read($cache_name);
+        $already_searched = (bool) \Studip\Cache\Factory::getCache()->read($cache_name);
         if (!$already_searched) {
             $hosts = OERHost::findBySQL("index_server = '1' AND allowed_as_index_server = '1' ORDER BY RAND()");
             foreach ($hosts as $host) {
@@ -172,7 +172,7 @@ class OERMaterial extends SimpleORMap
                     $host->fetchRemoteSearch($text, $tag);
                 }
             }
-            StudipCacheFactory::getCache()->write($cache_name, "1", 60);
+            \Studip\Cache\Factory::getCache()->write($cache_name, "1", 60);
         }
     }
 
diff --git a/lib/models/PersonalNotifications.class.php b/lib/models/PersonalNotifications.class.php
index 721038cd63d..12d9c1b14c6 100644
--- a/lib/models/PersonalNotifications.class.php
+++ b/lib/models/PersonalNotifications.class.php
@@ -262,7 +262,7 @@ class PersonalNotifications extends SimpleORMap
      */
     protected static function getCache($user_id)
     {
-        $cache  = StudipCacheFactory::getCache();
+        $cache  = \Studip\Cache\Factory::getCache();
         $hash   = self::getCacheHash($user_id);
         $cached = $cache->read($hash);
 
@@ -281,7 +281,7 @@ class PersonalNotifications extends SimpleORMap
      */
     protected static function setCache($user_id, $items)
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $hash  = self::getCacheHash($user_id);
         $cache->write($hash, serialize($items), self::CACHE_DURATION);
     }
@@ -293,7 +293,7 @@ class PersonalNotifications extends SimpleORMap
      */
     protected static function expireCache($user_id)
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $hash  = self::getCacheHash($user_id);
         $cache->expire($hash);
     }
diff --git a/lib/models/Semester.class.php b/lib/models/Semester.class.php
index 4eefa3b886c..03243a57d04 100644
--- a/lib/models/Semester.class.php
+++ b/lib/models/Semester.class.php
@@ -181,7 +181,7 @@ class Semester extends SimpleORMap
         if (!is_array(self::$semester_cache) || $force_reload) {
             self::$semester_cache = [];
             if (!$force_reload) {
-                $cache = StudipCacheFactory::getCache();
+                $cache = \Studip\Cache\Factory::getCache();
                 $semester_data_array = unserialize($cache->read('DB_SEMESTER_DATA'));
                 if ($semester_data_array) {
                     foreach ($semester_data_array as $semester_data) {
@@ -202,7 +202,7 @@ class Semester extends SimpleORMap
                     }
                     $semester_data[] = $semester->toRawArray();
                 }
-                $cache = StudipCacheFactory::getCache();
+                $cache = \Studip\Cache\Factory::getCache();
                 $cache->write('DB_SEMESTER_DATA', serialize($semester_data));
             }
         }
@@ -471,7 +471,7 @@ class Semester extends SimpleORMap
      */
     public function refreshCache()
     {
-        StudipCacheFactory::getCache()->expire('DB_SEMESTER_DATA');
+        \Studip\Cache\Factory::getCache()->expire('DB_SEMESTER_DATA');
     }
 
     /*
diff --git a/lib/models/StudipCacheOperation.php b/lib/models/StudipCacheOperation.php
index e9c8738371e..d2287c32a5f 100644
--- a/lib/models/StudipCacheOperation.php
+++ b/lib/models/StudipCacheOperation.php
@@ -42,7 +42,7 @@ class StudipCacheOperation extends SimpleORMap
      */
     public static function apply(StudipCache $cache)
     {
-        self::findEachBySQL(function ($item) use ($cache) {
+        self::findEachBySQL(function (StudipCacheOperation $item) use ($cache): void {
             $parameters = unserialize($item->parameters);
             array_unshift($parameters, $item->cache_key);
             call_user_func_array([$cache, $item->operation], $parameters);
diff --git a/lib/phplib/CT_Cache.class.php b/lib/phplib/CT_Cache.class.php
index 311b27cba55..d4eccfa0a27 100644
--- a/lib/phplib/CT_Cache.class.php
+++ b/lib/phplib/CT_Cache.class.php
@@ -14,7 +14,7 @@ class CT_Cache
 
     public function ac_start()
     {
-        $this->cache = StudipCacheFactory::getCache();
+        $this->cache = \Studip\Cache\Factory::getCache();
     }
 
     public function ac_get_lock()
diff --git a/lib/plugins/db/RolePersistence.class.php b/lib/plugins/db/RolePersistence.class.php
index b11fa4ad8fd..df63a773deb 100644
--- a/lib/plugins/db/RolePersistence.class.php
+++ b/lib/plugins/db/RolePersistence.class.php
@@ -29,7 +29,7 @@ class RolePersistence
     {
         if (self::$all_roles === null) {
             // read cache
-            $cache = StudipCacheFactory::getCache();
+            $cache = \Studip\Cache\Factory::getCache();
 
             // cache miss, retrieve from database
             self::$all_roles = $cache->read(self::ROLES_CACHE_KEY);
@@ -675,7 +675,7 @@ class RolePersistence
     public static function expireRolesCache()
     {
         self::$all_roles = null;
-        StudipCacheFactory::getCache()->expire(self::ROLES_CACHE_KEY);
+        \Studip\Cache\Factory::getCache()->expire(self::ROLES_CACHE_KEY);
     }
 
     /**
diff --git a/lib/plugins/engine/PluginRepository.class.php b/lib/plugins/engine/PluginRepository.class.php
index 14415f42bd0..235e9bd97b3 100644
--- a/lib/plugins/engine/PluginRepository.class.php
+++ b/lib/plugins/engine/PluginRepository.class.php
@@ -55,7 +55,7 @@ class PluginRepository
      */
     public function readMetadata($url)
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache_key = 'plugin_metadata/'.$url;
         $metadata = $cache->read($cache_key);
 
diff --git a/lib/raumzeit/SingleDate.class.php b/lib/raumzeit/SingleDate.class.php
index 82a89db6852..1a58695bd28 100644
--- a/lib/raumzeit/SingleDate.class.php
+++ b/lib/raumzeit/SingleDate.class.php
@@ -292,7 +292,7 @@ class SingleDate
 
     function delete()
     {
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('course/undecorated_data/' . $this->range_id);
 
         $this->chdate = time();
@@ -304,7 +304,7 @@ class SingleDate
     function store()
     {
 
-        $cache = StudipCacheFactory::getCache();
+        $cache = \Studip\Cache\Factory::getCache();
         $cache->expire('course/undecorated_data/' . $this->range_id);
 
         $this->chdate = time();
diff --git a/tests/functional/_bootstrap.php b/tests/functional/_bootstrap.php
index 49f332495c8..8a9125b45a4 100644
--- a/tests/functional/_bootstrap.php
+++ b/tests/functional/_bootstrap.php
@@ -30,6 +30,8 @@ StudipAutoloader::register();
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/calendar', 'Studip\\Calendar');
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/classes');
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/classes', 'Studip');
+StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/classes/cache');
+StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/classes/cache', 'Studip');
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/exceptions');
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/exceptions/resources');
 StudipAutoloader::addAutoloadPath($STUDIP_BASE_PATH . '/lib/filesystem');
@@ -74,7 +76,7 @@ if (!class_exists('StudipTestHelper')) {
     {
         static function set_up_tables($tables)
         {
-            $cache = StudipCacheFactory::getCache(false);
+            $cache = \Studip\Cache\Factory::getCache(false);
 
             // second step, expire table scheme
             SimpleORMap::expireTableScheme();
diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php
index 82ae54e514a..baf37409845 100644
--- a/tests/jsonapi/_bootstrap.php
+++ b/tests/jsonapi/_bootstrap.php
@@ -53,6 +53,8 @@ StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/plugins/eng
 
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/calendar');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/calendar', 'Studip\\Calendar');
+StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'lib/classes/cache');
+StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'lib/classes/cache', 'Studip');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/calendar/lib');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/exceptions');
 
diff --git a/tests/unit/_bootstrap.php b/tests/unit/_bootstrap.php
index 3aa1144f668..af956074d85 100644
--- a/tests/unit/_bootstrap.php
+++ b/tests/unit/_bootstrap.php
@@ -54,6 +54,8 @@ StudipAutoloader::addAutoloadPath('lib/models');
 StudipAutoloader::addAutoloadPath('lib/classes');
 StudipAutoloader::addAutoloadPath('lib/classes', 'Studip');
 StudipAutoloader::addAutoloadPath('lib/exTpl', 'exTpl');
+StudipAutoloader::addAutoloadPath('lib/classes/cache');
+StudipAutoloader::addAutoloadPath('lib/classes/cache', 'Studip');
 StudipAutoloader::addAutoloadPath('lib/exceptions');
 StudipAutoloader::addAutoloadPath('lib/classes/sidebar');
 StudipAutoloader::addAutoloadPath('lib/classes/helpbar');
@@ -108,7 +110,7 @@ if (!class_exists('StudipTestHelper')) {
         static function set_up_tables($tables)
         {
             // first step, set fake cache
-            $cache = StudipCacheFactory::getCache(false);
+            $cache = \Studip\Cache\Factory::getCache(false);
 
             // second step, expire table scheme
             SimpleORMap::expireTableScheme();
diff --git a/tests/unit/lib/classes/MigrationTest.php b/tests/unit/lib/classes/MigrationTest.php
index 4a45e71598c..12df1201c7e 100644
--- a/tests/unit/lib/classes/MigrationTest.php
+++ b/tests/unit/lib/classes/MigrationTest.php
@@ -16,11 +16,6 @@ class MigrationTest extends \Codeception\Test\Unit
         $this->before = $GLOBALS['CACHING_ENABLE'] ?? null;
         $GLOBALS['CACHING_ENABLE'] = false;
 
-        require_once 'lib/classes/SimpleORMap.class.php';
-        require_once 'lib/classes/StudipCache.class.php';
-        require_once 'lib/classes/StudipMemoryCache.class.php';
-        require_once 'lib/classes/StudipCacheFactory.class.php';
-
         require_once 'lib/migrations/Migration.php';
         require_once 'lib/migrations/Migrator.php';
         require_once 'lib/migrations/SchemaVersion.php';
diff --git a/tests/unit/lib/classes/StudipCachedArrayTest.php b/tests/unit/lib/classes/StudipCachedArrayTest.php
index c98c1bd6b5c..5fd39f2d7e9 100644
--- a/tests/unit/lib/classes/StudipCachedArrayTest.php
+++ b/tests/unit/lib/classes/StudipCachedArrayTest.php
@@ -1,4 +1,7 @@
 <?php
+
+use Studip\Cache\MemoryCache;
+
 /**
  * StudipCachedArrayTest.php - unit tests for the StudipCachedArray class
  *
@@ -6,7 +9,7 @@
  * @license  GPL2 or any later version
  *
  * @covers StudipCachedArray
- * @uses StudipMemoryCache
+ * @uses   MemoryCache
  */
 
 class StudipCachedArrayTest extends \Codeception\Test\Unit
-- 
GitLab