diff --git a/composer.json b/composer.json
index e89865bfc7dff09b1c0b76487222ebf133671e4b..71e97b073b319fb04195efbeb5e76ed650ef84fa 100644
--- a/composer.json
+++ b/composer.json
@@ -57,6 +57,7 @@
         "symfony/config": "^5.4",
         "symfony/console": "^5.4",
         "symfony/dependency-injection": "^5.4",
+        "symfony/filesystem": "^5.4",
         "symfony/finder": "^5.4",
         "symfony/framework-bundle": "^5.4",
         "symfony/http-foundation": "^5.4",
@@ -94,8 +95,9 @@
     },
     "config": {
         "allow-plugins": {
+            "composer/package-versions-deprecated": true,
             "simplesamlphp/composer-module-installer": true,
-            "composer/package-versions-deprecated": true
+            "muglug/package-versions-56": true
         }
     }
 }
diff --git a/composer.lock b/composer.lock
index 431871eea52d22092f248d2b3ee9751cc73b2d67..72ed91148b8275c93486fa6b0d42ad56c13bb628 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": "9cedbdf88b9c6b9cf915d55ded93758d",
+    "content-hash": "b961c9acd0500cbabf669f97bea560ae",
     "packages": [
         {
             "name": "gettext/gettext",
@@ -3670,16 +3670,16 @@
         },
         {
             "name": "composer/semver",
-            "version": "3.3.0",
+            "version": "3.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b"
+                "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b",
-                "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b",
+                "url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71",
+                "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71",
                 "shasum": ""
             },
             "require": {
@@ -3731,7 +3731,7 @@
             "support": {
                 "irc": "irc://irc.freenode.org/composer",
                 "issues": "https://github.com/composer/semver/issues",
-                "source": "https://github.com/composer/semver/tree/3.3.0"
+                "source": "https://github.com/composer/semver/tree/3.3.1"
             },
             "funding": [
                 {
@@ -3747,7 +3747,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-03-15T08:35:57+00:00"
+            "time": "2022-03-16T11:22:07+00:00"
         },
         {
             "name": "composer/xdebug-handler",
diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php
index 54a6d9edbd0002e4735a6c58cda9942476e1eb6f..1355d8b93dcedcefc23604d198b208de546e1ccc 100644
--- a/lib/SimpleSAML/Configuration.php
+++ b/lib/SimpleSAML/Configuration.php
@@ -4,11 +4,33 @@ declare(strict_types=1);
 
 namespace SimpleSAML;
 
+use Exception;
+use ParseError;
 use SAML2\Constants;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Assert\AssertionFailedException;
 use SimpleSAML\Error;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
+
+use function array_key_exists;
+use function array_keys;
+use function dirname;
+use function implode;
+use function interface_exists;
+use function in_array;
+use function is_array;
+use function is_bool;
+use function is_int;
+use function is_string;
+use function ob_end_clean;
+use function ob_get_length;
+use function ob_start;
+use function preg_match;
+use function preg_replace;
+use function rtrim;
+use function substr;
+use function var_export;
 
 /**
  * Configuration of SimpleSAMLphp
@@ -106,7 +128,8 @@ class Configuration implements Utils\ClearableState
             return self::$loadedConfigs[$filename];
         }
 
-        if (file_exists($filename)) {
+        $fileSystem = new Filesystem();
+        if ($fileSystem->exists($filename)) {
             /** @psalm-var mixed $config */
             $config = 'UNINITIALIZED';
 
@@ -115,7 +138,7 @@ class Configuration implements Utils\ClearableState
             if (interface_exists('Throwable', false)) {
                 try {
                     require($filename);
-                } catch (\ParseError $e) {
+                } catch (ParseError $e) {
                     self::$loadedConfigs[$filename] = self::loadFromArray([], '[ARRAY]', 'simplesaml');
                     throw new Error\ConfigurationError($e->getMessage(), $filename, []);
                 }
@@ -204,7 +227,7 @@ class Configuration implements Utils\ClearableState
     ): void {
         if (!array_key_exists($configSet, self::$configDirs)) {
             if ($configSet !== 'simplesaml') {
-                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
+                throw new Exception('Configuration set \'' . $configSet . '\' not initialized.');
             } else {
                 self::$configDirs['simplesaml'] = dirname(dirname(dirname(__FILE__))) . '/config';
             }
@@ -232,7 +255,7 @@ class Configuration implements Utils\ClearableState
     ): Configuration {
         if (!array_key_exists($configSet, self::$configDirs)) {
             if ($configSet !== 'simplesaml') {
-                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
+                throw new Exception('Configuration set \'' . $configSet . '\' not initialized.');
             } else {
                 $configUtils = new Utils\Config();
                 self::$configDirs['simplesaml'] = $configUtils->getConfigDir();
@@ -262,7 +285,7 @@ class Configuration implements Utils\ClearableState
     ): Configuration {
         if (!array_key_exists($configSet, self::$configDirs)) {
             if ($configSet !== 'simplesaml') {
-                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
+                throw new Exception('Configuration set \'' . $configSet . '\' not initialized.');
             } else {
                 $configUtils = new Utils\Config();
                 self::$configDirs['simplesaml'] = $configUtils->getConfigDir();
@@ -1113,7 +1136,7 @@ class Configuration implements Utils\ClearableState
             case 'saml20-idp-remote:ArtifactResolutionService':
                 return Constants::BINDING_SOAP;
             default:
-                throw new \Exception('Missing default binding for ' . $endpointType . ' in ' . $set);
+                throw new Exception('Missing default binding for ' . $endpointType . ' in ' . $set);
         }
     }
 
@@ -1142,7 +1165,7 @@ class Configuration implements Utils\ClearableState
             // for backwards-compatibility
             $eps = [$eps];
         } elseif (!is_array($eps)) {
-            throw new \Exception($loc . ': Expected array or string.');
+            throw new Exception($loc . ': Expected array or string.');
         }
 
 
@@ -1160,32 +1183,32 @@ class Configuration implements Utils\ClearableState
                     $ep['ResponseLocation'] = $responseLocation;
                 }
             } elseif (!is_array($ep)) {
-                throw new \Exception($iloc . ': Expected a string or an array.');
+                throw new Exception($iloc . ': Expected a string or an array.');
             }
 
             if (!array_key_exists('Location', $ep)) {
-                throw new \Exception($iloc . ': Missing Location.');
+                throw new Exception($iloc . ': Missing Location.');
             }
             if (!is_string($ep['Location'])) {
-                throw new \Exception($iloc . ': Location must be a string.');
+                throw new Exception($iloc . ': Location must be a string.');
             }
 
             if (!array_key_exists('Binding', $ep)) {
-                throw new \Exception($iloc . ': Missing Binding.');
+                throw new Exception($iloc . ': Missing Binding.');
             }
             if (!is_string($ep['Binding'])) {
-                throw new \Exception($iloc . ': Binding must be a string.');
+                throw new Exception($iloc . ': Binding must be a string.');
             }
 
             if (array_key_exists('ResponseLocation', $ep)) {
                 if (!is_string($ep['ResponseLocation'])) {
-                    throw new \Exception($iloc . ': ResponseLocation must be a string.');
+                    throw new Exception($iloc . ': ResponseLocation must be a string.');
                 }
             }
 
             if (array_key_exists('index', $ep)) {
                 if (!is_int($ep['index'])) {
-                    throw new \Exception($iloc . ': index must be an integer.');
+                    throw new Exception($iloc . ': index must be an integer.');
                 }
             }
         }
@@ -1223,7 +1246,7 @@ class Configuration implements Utils\ClearableState
 
         if ($default === self::REQUIRED_OPTION) {
             $loc = $this->location . '[' . var_export($endpointType, true) . ']:';
-            throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
+            throw new Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
         }
 
         return $default;
@@ -1253,7 +1276,7 @@ class Configuration implements Utils\ClearableState
 
         if ($default === self::REQUIRED_OPTION) {
             $loc = $this->location . '[' . var_export($endpointType, true) . ']:';
-            throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
+            throw new Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
         }
 
         return $default;
@@ -1368,7 +1391,7 @@ class Configuration implements Utils\ClearableState
             $data = @file_get_contents($file);
 
             if ($data === false) {
-                throw new \Exception(
+                throw new Exception(
                     $this->location . ': Unable to load certificate/public key from file "' . $file . '".'
                 );
             }
@@ -1376,7 +1399,7 @@ class Configuration implements Utils\ClearableState
             // extract certificate data (if this is a certificate)
             $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m';
             if (!preg_match($pattern, $data, $matches)) {
-                throw new \SimpleSAML\Error\Exception(
+                throw new Error\Exception(
                     $this->location . ': Could not find PEM encoded certificate in "' . $file . '".'
                 );
             }
@@ -1393,7 +1416,7 @@ class Configuration implements Utils\ClearableState
                 ],
             ];
         } elseif ($required === true) {
-            throw new \SimpleSAML\Error\Exception($this->location . ': Missing certificate in metadata.');
+            throw new Error\Exception($this->location . ': Missing certificate in metadata.');
         } else {
             return [];
         }
diff --git a/lib/SimpleSAML/Locale/Localization.php b/lib/SimpleSAML/Locale/Localization.php
index 1dbb5c1f6ac6af03606ea878393755d3840308c8..0f446864fd4819e9af8e9436aa57130d60f92ea4 100644
--- a/lib/SimpleSAML/Locale/Localization.php
+++ b/lib/SimpleSAML/Locale/Localization.php
@@ -10,10 +10,13 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Locale;
 
+use Exception;
 use Gettext\Translations;
 use Gettext\Translator;
 use SimpleSAML\Configuration;
 use SimpleSAML\Logger;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\File\File;
 
 class Localization
 {
@@ -66,6 +69,11 @@ class Localization
      */
     private string $langcode;
 
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    private Filesystem $fileSystem;
+
 
     /**
      * Constructor
@@ -74,6 +82,7 @@ class Localization
      */
     public function __construct(Configuration $configuration)
     {
+        $this->fileSystem = new Filesystem();
         $this->configuration = $configuration;
         /** @var string $locales */
         $locales = $this->configuration->resolvePath('locales');
@@ -186,7 +195,7 @@ class Localization
         // Locale for default language missing even, error out
         $error = "Localization directory '$langPath' missing/broken for langcode '$langcode' and domain '$domain'";
         Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
-        throw new \Exception($error);
+        throw new Exception($error);
     }
 
 
@@ -217,7 +226,7 @@ class Localization
     ): void {
         try {
             $langPath = $this->getLangPath($domain);
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'.";
             Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
             if ($catchException) {
@@ -227,14 +236,18 @@ class Localization
                 throw $e;
             }
         }
-        $poFile = $domain . '.po';
-        $poPath = $langPath . $poFile;
-        if (file_exists($poPath) && is_readable($poPath)) {
-            $translations = Translations::fromPoFile($poPath);
+
+        $file = new File($langPath . $domain . '.po');
+        if ($this->fileSystem->exists($file->getRealPath()) && $file->isReadable()) {
+            $translations = Translations::fromPoFile($file->getRealPath());
             $this->translator->loadTranslations($translations);
         } else {
-            $error = "Localization file '$poFile' not found in '$langPath', falling back to default";
-            Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
+            Logger::debug(sprintf(
+                "%s - Localization file '%s' not found or not readable in '%s', falling back to default",
+                $_SERVER['PHP_SELF'],
+                $file->getfileName(),
+                $langPath,
+            ));
         }
     }
 
diff --git a/lib/SimpleSAML/Logger/FileLoggingHandler.php b/lib/SimpleSAML/Logger/FileLoggingHandler.php
index 179080bb2436ee1750e0c55f7a97a4ade2616aa2..a11eff283a85a2c59b204c69419a7bc5b42b3e1b 100644
--- a/lib/SimpleSAML/Logger/FileLoggingHandler.php
+++ b/lib/SimpleSAML/Logger/FileLoggingHandler.php
@@ -7,6 +7,9 @@ namespace SimpleSAML\Logger;
 use SimpleSAML\Configuration;
 use SimpleSAML\Logger;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
+use Symfony\Component\HttpFoundation\File\File;
 
 /**
  * A logging handler that dumps logs to files.
@@ -45,6 +48,11 @@ class FileLoggingHandler implements LoggingHandlerInterface
     /** @var string */
     protected string $format = "%b %d %H:%M:%S";
 
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    protected Filesystem $fileSystem;
+
 
     /**
      * Build a new logging handler based on files.
@@ -52,6 +60,8 @@ class FileLoggingHandler implements LoggingHandlerInterface
      */
     public function __construct(Configuration $config)
     {
+        $this->fileSystem = new Filesystem();
+
         // get the metadata handler option from the configuration
         $this->logFile = $config->getPathValue('loggingdir', 'log/') .
             $config->getOptionalString('logging.logfile', 'simplesamlphp.log');
@@ -63,17 +73,19 @@ class FileLoggingHandler implements LoggingHandlerInterface
             $config->getOptionalString('logging.processname', 'SimpleSAMLphp')
         );
 
-        if (@file_exists($this->logFile)) {
-            if (!@is_writeable($this->logFile)) {
-                throw new \Exception("Could not write to logfile: " . $this->logFile);
-            }
-        } else {
-            if (!@touch($this->logFile)) {
-                throw new \Exception(
-                    "Could not create logfile: " . $this->logFile .
-                    " The logging directory is not writable for the web server user."
+        $file = new File($this->logFile);
+        // Suppress E_WARNING if not exists
+        if (@$this->fileSystem->exists($this->logFile)) {
+            if (!$file->isWritable()) {
+                throw new CannotWriteFileException(
+                    sprintf("Could not write to logfile: %s", $this->logFile),
                 );
             }
+        } elseif (!$this->fileSystem->touch($this->logFile)) {
+            throw new CannotWriteFileException(sprintf(
+                "The logging directory is not writable for the web server user. Could not create logfile: %s",
+                $this->logFile,
+            ));
         }
 
         $timeUtils = new Utils\Time();
@@ -121,8 +133,20 @@ class FileLoggingHandler implements LoggingHandlerInterface
                 array_push($replacements, date($format));
             }
 
-            $string = str_replace($formats, $replacements, $string);
-            file_put_contents($this->logFile, $string . \PHP_EOL, FILE_APPEND);
+            if (preg_match('/^php:\/\//', $this->logFile)) {
+                // Dirty hack to get unit tests for Windows working.. Symfony doesn't deal well with them.
+                file_put_contents(
+                    $this->logFile,
+                    str_replace($formats, $replacements, $string) . \PHP_EOL,
+                    FILE_APPEND,
+                );
+            } else {
+                $this->fileSystem->appendToFile(
+                    $this->logFile,
+                    str_replace($formats, $replacements, $string) . \PHP_EOL,
+                    false,
+                );
+            }
         }
     }
 }
diff --git a/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php b/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php
index 1fd967955ee9bed379d36fc458829c19198b2d84..37bf8e14f0dbbb0d89f5d35dbdb5bef70c06813c 100644
--- a/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php
+++ b/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace SimpleSAML\Logger;
 
 use SimpleSAML\Configuration;
+use Symfony\Component\Filesystem\Filesystem;
 
 /**
  * A logging handler that outputs all messages to standard error.
@@ -22,12 +23,15 @@ class StandardErrorLoggingHandler extends FileLoggingHandler
      */
     public function __construct(Configuration $config)
     {
+        $this->fileSystem = new Filesystem();
+
         // Remove any non-printable characters before storing
         $this->processname = preg_replace(
             '/[\x00-\x1F\x7F\xA0]/u',
             '',
             $config->getOptionalString('logging.processname', 'SimpleSAMLphp')
         );
+
         $this->logFile = 'php://stderr';
     }
 }
diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php
index 8b1f39b0a256d01c8de964d1ca2852ef815ac1b8..45bae13548aff54c3352b1ed1f1b0a495b1e5990 100644
--- a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php
+++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php
@@ -4,9 +4,13 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Metadata;
 
+use Exception;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Configuration;
 
+use function array_key_exists;
+use function is_array;
+
 /**
  * This file defines a flat file metadata source.
  * Instantiation of session handler objects should be done through
@@ -45,6 +49,8 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource
      */
     protected function __construct(array $config)
     {
+        parent::__construct();
+
         // get the configuration
         $globalConfig = Configuration::getInstance();
 
@@ -79,7 +85,7 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource
     {
         $metadatasetfile = $this->directory . $set . '.php';
 
-        if (!file_exists($metadatasetfile)) {
+        if (!$this->fileSystem->exists($metadatasetfile)) {
             return null;
         }
 
@@ -89,7 +95,7 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource
         include($metadatasetfile);
 
         if (!is_array($metadata)) {
-            throw new \Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
+            throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
         }
 
         return $metadata;
diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php
index 1dcf7d1cf19265f523bbd52617e4dd0a64d6654c..07499485df25cbebbf19531ab0b5402801e21a32 100644
--- a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php
+++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php
@@ -8,6 +8,18 @@ use SimpleSAML\Assert\Assert;
 use SimpleSAML\Configuration;
 use SimpleSAML\Logger;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Exception\IOException;
+use Symfony\Component\Filesystem\Path;
+use Symfony\Component\Finder\Finder;
+use Symfony\Component\HttpFoundation\File\File;
+
+use function array_key_exists;
+use function rawurlencode;
+use function serialize;
+use function strlen;
+use function substr;
+use function unserialize;
+use function var_export;
 
 /**
  * Class for handling metadata files in serialized format.
@@ -42,8 +54,9 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
      */
     public function __construct(array $config)
     {
-        $globalConfig = Configuration::getInstance();
+        parent::__construct();
 
+        $globalConfig = Configuration::getInstance();
         $cfgHelp = Configuration::loadFromArray($config, 'serialize metadata source');
         $this->directory = $cfgHelp->getString('directory');
 
@@ -78,35 +91,22 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
     {
         $ret = [];
 
-        $dh = @opendir($this->directory);
-        if ($dh === false) {
+        $loc = new File($this->directory, false);
+        if (!$this->fileSystem->exists($this->directory) || !$loc->isReadable()) {
             Logger::warning(
                 'Serialize metadata handler: Unable to open directory: ' . var_export($this->directory, true)
             );
             return $ret;
         }
 
-        while (($entry = readdir($dh)) !== false) {
-            if ($entry[0] === '.') {
-                // skip '..', '.' and hidden files
-                continue;
-            }
-
-            $path = $this->directory . '/' . $entry;
+        $finder = new Finder();
+        $finder->directories()->name(sprintf('/%s$/' . self::EXTENSION))->in($this->directory);
 
-            if (!is_dir($path)) {
-                Logger::warning(
-                    'Serialize metadata handler: Metadata directory contained a file where only directories should ' .
-                    'exist: ' . var_export($path, true)
-                );
-                continue;
-            }
-
-            $ret[] = rawurldecode($entry);
+        $ret = [];
+        foreach ($finder as $file) {
+            $ret[] = rawurlencode($file->getPathName());
         }
 
-        closedir($dh);
-
         return $ret;
     }
 
@@ -122,32 +122,23 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
     {
         $ret = [];
 
-        $dir = $this->directory . '/' . rawurlencode($set);
-        if (!is_dir($dir)) {
-            // probably some code asked for a metadata set which wasn't available
-            return $ret;
-        }
-
-        $dh = @opendir($dir);
-        if ($dh === false) {
-            Logger::warning(
-                'Serialize metadata handler: Unable to open directory: ' . var_export($dir, true)
-            );
+        $loc = new File(Path::canonicalize($this->directory . '/' . rawurlencode($set)), false);
+        if (!$this->fileSystem->exists($loc) || !$loc->isReadable()) {
+            Logger::warning(sprintf(
+                'Serialize metadata handler: Unable to open directory: %s',
+                var_export($loc->getPathName(), true),
+            ));
             return $ret;
         }
 
         $extLen = strlen(self::EXTENSION);
 
-        while (($file = readdir($dh)) !== false) {
-            if (strlen($file) <= $extLen) {
-                continue;
-            }
-
-            if (substr($file, -$extLen) !== self::EXTENSION) {
-                continue;
-            }
+        $finder = new Finder();
+        $finder->directories()->name(sprintf('/%s/$', self::EXTENSION))->in($this->directory);
 
-            $entityId = substr($file, 0, -$extLen);
+        $ret = [];
+        foreach ($finder as $file) {
+            $entityId = substr($file->getPathName(), 0, -$extLen);
             $entityId = rawurldecode($entityId);
 
             $md = $this->getMetaData($entityId, $set);
@@ -156,8 +147,6 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
             }
         }
 
-        closedir($dh);
-
         return $ret;
     }
 
@@ -175,17 +164,15 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
     {
         $filePath = $this->getMetadataPath($entityId, $set);
 
-        if (!file_exists($filePath)) {
+        if (!$this->fileSystem->exists($filePath)) {
             return null;
         }
 
-        $data = @file_get_contents($filePath);
-        if ($data === false) {
-            /** @var array $error */
-            $error = error_get_last();
-            Logger::warning(
-                'Error reading file ' . $filePath . ': ' . $error['message']
-            );
+        $file = new File($filePath);
+        try {
+            $data = $file->getContent();
+        } catch (IOException $e) {
+            Logger::warning('Error reading file ' . $filePath . ': ' . $e->getMessage());
             return null;
         }
 
@@ -214,38 +201,37 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
      */
     public function saveMetadata(string $entityId, string $set, array $metadata): bool
     {
-        $filePath = $this->getMetadataPath($entityId, $set);
-        $newPath = $filePath . '.new';
-
-        $dir = dirname($filePath);
-        if (!is_dir($dir)) {
-            Logger::info('Creating directory: ' . $dir);
-            $res = @mkdir($dir, 0777, true);
-            if ($res === false) {
-                /** @var array $error */
-                $error = error_get_last();
-                Logger::error('Failed to create directory ' . $dir . ': ' . $error['message']);
+        $old = new File($this->getMetadataPath($entityId, $set), false);
+        $new = new File($old->getPathName() . '.new', false);
+
+        $loc = new File($old->getPath(), false);
+        if (!$loc->isDir()) {
+            Logger::info('Creating directory: ' . $loc);
+            try {
+                $this->fileSystem->mkdir($loc, 0777);
+            } catch (IOException $e) {
+                Logger::error('Failed to create directory ' . $loc . ': ' . $e->getMessage());
                 return false;
             }
         }
 
         $data = serialize($metadata);
 
-        Logger::debug('Writing: ' . $newPath);
+        Logger::debug('Writing: ' . $new->getPathName());
 
-        $res = file_put_contents($newPath, $data);
-        if ($res === false) {
-            /** @var array $error */
-            $error = error_get_last();
-            Logger::error('Error saving file ' . $newPath . ': ' . $error['message']);
+        try {
+            $this->fileSystem->appendToFile($new->getPathName(), $data);
+        } catch (IOException $e) {
+            Logger::error('Error saving file ' . $new->getPathName() . ': ' . $e->getMessage());
             return false;
         }
 
-        $res = rename($newPath, $filePath);
-        if ($res === false) {
-            /** @var array $error */
-            $error = error_get_last();
-            Logger::error('Error renaming ' . $newPath . ' to ' . $filePath . ': ' . $error['message']);
+        try {
+            $this->fileSystem->rename($new->getPathName(), $old->getPathName(), true);
+        } catch (IOException $e) {
+            Logger::error(
+                sprintf('Error renaming %s to %s: %s', $new->getPathName(), $old->getPathName(), $e->getMessage())
+            );
             return false;
         }
 
@@ -263,7 +249,7 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
     {
         $filePath = $this->getMetadataPath($entityId, $set);
 
-        if (!file_exists($filePath)) {
+        if (!$this->fileSystem->exists($filePath)) {
             Logger::warning(
                 'Attempted to erase nonexistent metadata entry ' .
                 var_export($entityId, true) . ' in set ' . var_export($set, true) . '.'
@@ -271,14 +257,14 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
             return;
         }
 
-        $res = unlink($filePath);
-        if ($res === false) {
-            /** @var array $error */
-            $error = error_get_last();
-            Logger::error(
-                'Failed to delete file ' . $filePath .
-                ': ' . $error['message']
-            );
+        try {
+            $this->fileSystem->remove($filePath);
+        } catch (IOException $e) {
+            Logger::error(sprintf(
+                'Failed to delete file %s: %s',
+                $filePath,
+                $e->getMessage(),
+            ));
         }
     }
 
diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php
index faef115c5ff069dce575b690464018afa936ff4c..9dac4f0c72a4250bcfb4149a9b4cda59f2ad06dd 100644
--- a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php
+++ b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php
@@ -8,6 +8,7 @@ use SimpleSAML\Assert\Assert;
 use SimpleSAML\Error;
 use SimpleSAML\Module;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
 
 /**
  * This abstract class defines an interface for metadata storage sources.
@@ -21,6 +22,21 @@ use SimpleSAML\Utils;
 
 abstract class MetaDataStorageSource
 {
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    protected Filesystem $fileSystem;
+
+
+    /**
+     * This function initializes an XML metadata source.
+     */
+    protected function __construct()
+    {
+        $this->fileSystem = new Filesystem();
+    }
+
+
     /**
      * Parse array with metadata sources.
      *
diff --git a/lib/SimpleSAML/Metadata/SAMLParser.php b/lib/SimpleSAML/Metadata/SAMLParser.php
index 3b79cd0e6835fbf41bf11c84df50996d93f8cd6d..46fa2b25dc4d1fe6418f41dbc54f96b91b0818a7 100644
--- a/lib/SimpleSAML/Metadata/SAMLParser.php
+++ b/lib/SimpleSAML/Metadata/SAMLParser.php
@@ -6,6 +6,7 @@ namespace SimpleSAML\Metadata;
 
 use DOMDocument;
 use DOMElement;
+use Exception;
 use RobRichards\XMLSecLibs\XMLSecurityDSig;
 use RobRichards\XMLSecLibs\XMLSecurityKey;
 use SAML2\Constants;
@@ -38,6 +39,15 @@ use SAML2\XML\shibmd\Scope;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Logger;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\File\File;
+
+use function array_diff;
+use function array_intersect;
+use function array_key_exists;
+use function array_map;
+use function array_merge;
+use function count;
 
 /**
  * This is class for parsing of SAML 2.0 metadata.
@@ -161,6 +171,11 @@ class SAMLParser
      */
     private string $entityDescriptor;
 
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    protected Filesystem $fileSystem;
+
 
     /**
      * This is the constructor for the SAMLParser class.
@@ -177,6 +192,7 @@ class SAMLParser
         array $validators = [],
         array $parentExtensions = []
     ) {
+        $this->fileSystem = new Filesystem();
         $this->spDescriptors = [];
         $this->idpDescriptors = [];
 
@@ -236,8 +252,8 @@ class SAMLParser
 
         try {
             $doc = DOMDocumentFactory::fromString($data);
-        } catch (\Exception $e) {
-            throw new \Exception('Failed to read XML from file: ' . $file);
+        } catch (Exception $e) {
+            throw new Exception('Failed to read XML from file: ' . $file);
         }
 
         return self::parseDocument($doc);
@@ -256,8 +272,8 @@ class SAMLParser
     {
         try {
             $doc = DOMDocumentFactory::fromString($metadata);
-        } catch (\Exception $e) {
-            throw new \Exception('Failed to parse XML string.');
+        } catch (Exception $e) {
+            throw new Exception('Failed to parse XML string.');
         }
 
         return self::parseDocument($doc);
@@ -307,7 +323,7 @@ class SAMLParser
     public static function parseDescriptorsFile(string $file): array
     {
         if (empty($file)) {
-            throw new \Exception('Cannot open file; file name not specified.');
+            throw new Exception('Cannot open file; file name not specified.');
         }
 
         /** @var string $data */
@@ -316,8 +332,8 @@ class SAMLParser
 
         try {
             $doc = DOMDocumentFactory::fromString($data);
-        } catch (\Exception $e) {
-            throw new \Exception('Failed to read XML from file: ' . $file);
+        } catch (Exception $e) {
+            throw new Exception('Failed to read XML from file: ' . $file);
         }
 
         return self::parseDescriptorsElement($doc->documentElement);
@@ -339,8 +355,8 @@ class SAMLParser
     {
         try {
             $doc = DOMDocumentFactory::fromString($string);
-        } catch (\Exception $e) {
-            throw new \Exception('Failed to parse XML string.');
+        } catch (Exception $e) {
+            throw new Exception('Failed to parse XML string.');
         }
 
         return self::parseDescriptorsElement($doc->documentElement);
@@ -361,7 +377,7 @@ class SAMLParser
     public static function parseDescriptorsElement(DOMElement $element = null): array
     {
         if ($element === null) {
-            throw new \Exception('Document was empty.');
+            throw new Exception('Document was empty.');
         }
 
         $xmlUtils = new Utils\XML();
@@ -370,7 +386,7 @@ class SAMLParser
         } elseif ($xmlUtils->isDOMNodeOfType($element, 'EntitiesDescriptor', '@md') === true) {
             return self::processDescriptorsElement(new EntitiesDescriptor($element));
         } else {
-            throw new \Exception('Unexpected root node: [' . $element->namespaceURI . ']:' . $element->localName);
+            throw new Exception('Unexpected root node: [' . $element->namespaceURI . ']:' . $element->localName);
         }
     }
 
@@ -1247,7 +1263,7 @@ class SAMLParser
 
         $xmlUtils = new Utils\XML();
         if ($xmlUtils->isDOMNodeOfType($ed, 'EntityDescriptor', '@md') === false) {
-            throw new \Exception('Expected first element in the metadata document to be an EntityDescriptor element.');
+            throw new Exception('Expected first element in the metadata document to be an EntityDescriptor element.');
         }
 
         return new EntityDescriptor($ed);
@@ -1270,12 +1286,13 @@ class SAMLParser
         foreach ($certificates as $cert) {
             Assert::string($cert);
             $certFile = $configUtils->getCertPath($cert);
-            if (!file_exists($certFile)) {
-                throw new \Exception(
+            if (!$this->fileSystem->exists($certFile)) {
+                throw new Exception(
                     'Could not find certificate file [' . $certFile . '], which is needed to validate signature'
                 );
             }
-            $certData = file_get_contents($certFile);
+            $file = new File($certFile);
+            $certData = $file->getContent();
 
             foreach ($this->validators as $validator) {
                 $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'public']);
@@ -1284,7 +1301,7 @@ class SAMLParser
                     if ($validator->validate($key)) {
                         return true;
                     }
-                } catch (\Exception $e) {
+                } catch (Exception $e) {
                     // this certificate did not sign this element, skip
                 }
             }
diff --git a/lib/SimpleSAML/Metadata/Signer.php b/lib/SimpleSAML/Metadata/Signer.php
index 10dd1208a74805265e7be1ad8d3edcca5225be54..7a35a33dcefc24206c8f1ed2b690c755d45793c1 100644
--- a/lib/SimpleSAML/Metadata/Signer.php
+++ b/lib/SimpleSAML/Metadata/Signer.php
@@ -4,17 +4,26 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Metadata;
 
+use Exception;
 use RobRichards\XMLSecLibs\XMLSecurityKey;
 use RobRichards\XMLSecLibs\XMLSecurityDSig;
 use SAML2\DOMDocumentFactory;
 use SimpleSAML\Configuration;
 use SimpleSAML\Error;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\File\File;
+
+use function array_key_exists;
+use function hash;
+use function in_array;
+use function is_bool;
+use function is_string;
 
 /**
  * This class implements a helper function for signing of metadata.
  *
- * @package SimpleSAMLphp
+ * @package simplesamlphp/simplesamlphp
  */
 
 class Signer
@@ -41,7 +50,7 @@ class Signer
                 !array_key_exists('metadata.sign.privatekey', $entityMetadata)
                 || !array_key_exists('metadata.sign.certificate', $entityMetadata)
             ) {
-                throw new \Exception(
+                throw new Exception(
                     'Missing either the "metadata.sign.privatekey" or the' .
                     ' "metadata.sign.certificate" configuration option in the metadata for' .
                     ' the ' . $type . ' "' . $entityMetadata['entityid'] . '". If one of' .
@@ -66,7 +75,7 @@ class Signer
         $certificate = $config->getOptionalString('metadata.sign.certificate', null);
         if ($privatekey !== null || $certificate !== null) {
             if ($privatekey === null || $certificate === null) {
-                throw new \Exception(
+                throw new Exception(
                     'Missing either the "metadata.sign.privatekey" or the' .
                     ' "metadata.sign.certificate" configuration option in the global' .
                     ' configuration. If one of these options is specified, then the other' .
@@ -92,7 +101,7 @@ class Signer
                 !array_key_exists('privatekey', $entityMetadata)
                 || !array_key_exists('certificate', $entityMetadata)
             ) {
-                throw new \Exception(
+                throw new Exception(
                     'Both the "privatekey" and the "certificate" option must' .
                     ' be set in the metadata for the ' . $type . ' "' .
                     $entityMetadata['entityid'] . '" before it is possible to sign metadata' .
@@ -112,7 +121,7 @@ class Signer
             return $ret;
         }
 
-        throw new \Exception(
+        throw new Exception(
             'Could not find what key & certificate should be used to sign the metadata' .
             ' for the ' . $type . ' "' . $entityMetadata['entityid'] . '".'
         );
@@ -134,7 +143,7 @@ class Signer
         // first check the metadata for the entity
         if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
             if (!is_bool($entityMetadata['metadata.sign.enable'])) {
-                throw new \Exception(
+                throw new Exception(
                     'Invalid value for the "metadata.sign.enable" configuration option for' .
                     ' the ' . $type . ' "' . $entityMetadata['entityid'] . '". This option' .
                     ' should be a boolean.'
@@ -237,27 +246,30 @@ class Signer
         $keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);
 
         $keyFile = $configUtils->getCertPath($keyCertFiles['privatekey']);
-        if (!file_exists($keyFile)) {
-            throw new \Exception(
+        $fileSystem = new Filesystem();
+        if (!$fileSystem->exists($keyFile)) {
+            throw new Exception(
                 'Could not find private key file [' . $keyFile . '], which is needed to sign the metadata'
             );
         }
-        $keyData = file_get_contents($keyFile);
+
+        $key = new File($keyFile);
+        $keyData = $key->getContent();
 
         $certFile = $configUtils->getCertPath($keyCertFiles['certificate']);
-        if (!file_exists($certFile)) {
-            throw new \Exception(
+        $cert = new File($certFile);
+        if (!$fileSystem->exists($certFile)) {
+            throw new Exception(
                 'Could not find certificate file [' . $certFile . '], which is needed to sign the metadata'
             );
         }
-        $certData = file_get_contents($certFile);
-
+        $certData = $cert->getContent();
 
         // convert the metadata to a DOM tree
         try {
             $xml = DOMDocumentFactory::fromString($metadataString);
-        } catch (\Exception $e) {
-            throw new \Exception('Error parsing self-generated metadata.');
+        } catch (Exception $e) {
+            throw new Exception('Error parsing self-generated metadata.');
         }
 
         $signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);
diff --git a/lib/SimpleSAML/Metadata/Sources/MDQ.php b/lib/SimpleSAML/Metadata/Sources/MDQ.php
index 278cc57a29e10dcadb1560dbb7a5df88e7f29d21..7b99ced918bf906908e9200220747eed548cf6d0 100644
--- a/lib/SimpleSAML/Metadata/Sources/MDQ.php
+++ b/lib/SimpleSAML/Metadata/Sources/MDQ.php
@@ -4,21 +4,35 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Metadata\Sources;
 
+use Exception;
 use RobRichards\XMLSecLibs\XMLSecurityDSig;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Configuration;
 use SimpleSAML\Error;
 use SimpleSAML\Logger;
+use SimpleSAML\Metadata\MetaDataStorageSource;
 use SimpleSAML\Metadata\SAMLParser;
 use SimpleSAML\Utils;
+use Symfony\Component\HttpFoundation\File\File;
+
+use function array_key_exists;
+use function error_get_last;
+use function is_array;
+use function serialize;
+use function sha1;
+use function sprintf;
+use function strval;
+use function time;
+use function unserialize;
+use function urlencode;
 
 /**
  * This class implements SAML Metadata Query Protocol
  *
- * @package SimpleSAMLphp
+ * @package simplesamlphp/simplesamlphp
  */
 
-class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
+class MDQ extends MetaDataStorageSource
 {
     /**
      * The URL of MDQ server (url:port)
@@ -34,7 +48,6 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      */
     private ?string $cacheDir;
 
-
     /**
      * The maximum cache length, in seconds.
      *
@@ -42,7 +55,6 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      */
     private int $cacheLength;
 
-
     /**
      * This function initializes the dynamic XML metadata source.
      *
@@ -60,8 +72,10 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      */
     protected function __construct(array $config)
     {
+        parent::__construct($config);
+
         if (!array_key_exists('server', $config)) {
-            throw new \Exception(__CLASS__ . ": the 'server' configuration option is not set.");
+            throw new Exception(__CLASS__ . ": the 'server' configuration option is not set.");
         } else {
             $this->server = $config['server'];
         }
@@ -120,7 +134,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      * @param string $set The metadata set this entity belongs to.
      * @param string $entityId The entity id of this entity.
      *
-     * @return array|NULL  The associative array with the metadata for this entity, or NULL
+     * @return array|null  The associative array with the metadata for this entity, or NULL
      *                     if the entity could not be found.
      * @throws \Exception If an error occurs while loading metadata from cache.
      */
@@ -130,41 +144,47 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
             return null;
         }
 
-        $cachefilename = $this->getCacheFilename($set, $entityId);
-        if (!file_exists($cachefilename)) {
+        $cacheFileName = $this->getCacheFilename($set, $entityId);
+        if (!$this->fileSystem->exists($cacheFileName)) {
             return null;
         }
-        if (!is_readable($cachefilename)) {
-            throw new \Exception(__CLASS__ . ': could not read cache file for entity [' . $cachefilename . ']');
+
+        $file = new File($cacheFileName);
+        if (!$file->isReadable()) {
+            throw new Exception(sprintf('%s: could not read cache file for entity [%s]', strval($file), __CLASS__));
         }
-        Logger::debug(__CLASS__ . ': reading cache [' . $entityId . '] => [' . $cachefilename . ']');
+        Logger::debug(sprintf('%s: reading cache [%s] => [%s]', __CLASS__, $entityId, strval($file)));
 
         /* Ensure that this metadata isn't older that the cachelength option allows. This
          * must be verified based on the file, since this option may be changed after the
          * file is written.
          */
-        $stat = stat($cachefilename);
-        if ($stat['mtime'] + $this->cacheLength <= time()) {
-            Logger::debug(__CLASS__ . ': cache file older that the cachelength option allows.');
+        if (($file->getMtime() + $this->cacheLength) <= time()) {
+            Logger::debug(sprintf('%s: cache file older that the cachelength option allows.', __CLASS__));
             return null;
         }
 
-        $rawData = file_get_contents($cachefilename);
+        $rawData = $file->getContent();
         if (empty($rawData)) {
             /** @var array $error */
             $error = error_get_last();
-            throw new \Exception(
-                __CLASS__ . ': error reading metadata from cache file "' . $cachefilename . '": ' . $error['message']
-            );
+            throw new Exception(sprintf(
+                '%s: error reading metadata from cache file "%s": %s',
+                __CLASS__,
+                strval($file),
+                $error['message'],
+            ));
         }
 
         $data = unserialize($rawData);
         if ($data === false) {
-            throw new \Exception(__CLASS__ . ': error unserializing cached data from file "' . $cachefilename . '".');
+            throw new Exception(
+                sprintf('%s: error unserializing cached data from file "%s".', __CLASS__, strval($file))
+            );
         }
 
         if (!is_array($data)) {
-            throw new \Exception(__CLASS__ . ': Cached metadata from "' . $cachefilename . '" wasn\'t an array.');
+            throw new Exception(sprintf('%s: Cached metadata from "%s" wasn\'t an array.', __CLASS__, strval($file)));
         }
 
         return $data;
@@ -186,12 +206,11 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
             return;
         }
 
-        $cachefilename = $this->getCacheFilename($set, $entityId);
-        if (!is_writable(dirname($cachefilename))) {
-            throw new \Exception(__CLASS__ . ': could not write cache file for entity [' . $cachefilename . ']');
-        }
-        Logger::debug(__CLASS__ . ': Writing cache [' . $entityId . '] => [' . $cachefilename . ']');
-        file_put_contents($cachefilename, serialize($data));
+        $cacheFileName = $this->getCacheFilename($set, $entityId);
+        $file = new File($cacheFileName);
+
+        Logger::debug(sprintf('%s: Writing cache [%s] => [%s]', __CLASS__, $entityId, strval($file)));
+        $this->fileSystem->appendToFile(strval($file), serialize($data), true);
     }
 
 
@@ -201,7 +220,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      * @param \SimpleSAML\Metadata\SAMLParser $entity A SAML2Parser representing an entity.
      * @param string                         $set The metadata set we are looking for.
      *
-     * @return array|NULL  The associative array with the metadata, or NULL if no metadata for
+     * @return array|null  The associative array with the metadata, or NULL if no metadata for
      *                     the given set was found.
      */
     private static function getParsedSet(SAMLParser $entity, string $set): ?array
@@ -214,7 +233,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
             case 'attributeauthority-remote':
                 return $entity->getAttributeAuthorities();
             default:
-                Logger::warning(__CLASS__ . ': unknown metadata set: \'' . $set . '\'.');
+                Logger::warning(sprintf('%s: unknown metadata set: \'%s\'.', __CLASS__, $set));
         }
 
         return null;
@@ -241,12 +260,12 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
      */
     public function getMetaData(string $entityId, string $set): ?array
     {
-        Logger::info(__CLASS__ . ': loading metadata entity [' . $entityId . '] from [' . $set . ']');
+        Logger::info(sprintf('%s: loading metadata entity [%s] from [%s]', __CLASS__, $entityId, $set));
 
         // read from cache if possible
         try {
             $data = $this->getFromCache($set, $entityId);
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             Logger::error($e->getMessage());
             // proceed with fetching metadata even if the cache is broken
             $data = null;
@@ -259,45 +278,49 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource
 
         if (isset($data)) {
             // metadata found in cache and not expired
-            Logger::debug(__CLASS__ . ': using cached metadata for: ' . $entityId . '.');
+            Logger::debug(sprintf('%s: using cached metadata for: %s.', __CLASS__, $entityId));
             return $data;
         }
 
         // look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt
         $mdq_url = $this->server . '/entities/' . urlencode($entityId);
 
-        Logger::debug(__CLASS__ . ': downloading metadata for "' . $entityId . '" from [' . $mdq_url . ']');
+        Logger::debug(sprintf('%s: downloading metadata for "%s" from [%s]', __CLASS__, $entityId, $mdq_url));
         $httpUtils = new Utils\HTTP();
         try {
             $xmldata = $httpUtils->fetch($mdq_url);
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             // Avoid propagating the exception, make sure we can handle the error later
             $xmldata = false;
         }
 
         if (empty($xmldata)) {
             $error = error_get_last();
-            Logger::info('Unable to fetch metadata for "' . $entityId . '" from ' . $mdq_url . ': ' .
-                (is_array($error) ? $error['message'] : 'no error available'));
+            Logger::info(sprintf(
+                'Unable to fetch metadata for "%s" from %s: %s',
+                $entityId,
+                $mdq_url,
+                (is_array($error) ? $error['message'] : 'no error available')
+            ));
             return null;
         }
 
         /** @var string $xmldata */
         $entity = SAMLParser::parseString($xmldata);
-        Logger::debug(__CLASS__ . ': completed parsing of [' . $mdq_url . ']');
+        Logger::debug(sprintf('%s: completed parsing of [%s]', __CLASS__, $mdq_url));
 
         $data = self::getParsedSet($entity, $set);
         if ($data === null) {
-            throw new \Exception(
-                __CLASS__ . ': no metadata for set "' . $set . '" available from "' . $entityId . '".'
+            throw new Exception(
+                sprintf('%s: no metadata for set "%s" available from "%s".', __CLASS__, $set, $entityId)
             );
         }
 
         try {
             $this->writeToCache($set, $entityId, $data);
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             // Proceed without writing to cache
-            Logger::error('Error writing MDQ result to cache: ' . $e->getMessage());
+            Logger::error(sprintf('Error writing MDQ result to cache: %s', $e->getMessage()));
         }
 
         return $data;
diff --git a/lib/SimpleSAML/Module.php b/lib/SimpleSAML/Module.php
index c23e4f6430a83e351564def48a4b84b86157a552..cb3d9be3bf55ca3d65254f17ba0630df9bb13e5a 100644
--- a/lib/SimpleSAML/Module.php
+++ b/lib/SimpleSAML/Module.php
@@ -9,6 +9,9 @@ use SimpleSAML\Assert\Assert;
 use SimpleSAML\Kernel;
 use SimpleSAML\Utils;
 use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Filesystem\Path;
+use Symfony\Component\Finder\Finder;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -16,6 +19,31 @@ use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\ResponseHeaderBag;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
+use function array_filter;
+use function array_key_exists;
+use function class_exists;
+use function count;
+use function dirname;
+use function explode;
+use function function_exists;
+use function in_array;
+use function is_bool;
+use function is_callable;
+use function is_dir;
+use function is_file;
+use function is_null;
+use function is_subclass_of;
+use function mb_strtolower;
+use function mime_content_type;
+use function preg_match;
+use function rtrim;
+use function str_replace;
+use function strlen;
+use function strpos;
+use function strtolower;
+use function strval;
+use function substr;
+
 /**
  * Helper class for accessing information about modules.
  *
@@ -233,11 +261,12 @@ class Module
         }
 
         $path = $moduleDir . $url;
+        $fileSystem = new Filesystem();
 
         if ($path[strlen($path) - 1] === '/') {
             // path ends with a slash - directory reference. Attempt to find index file in directory
             foreach (self::$indexFiles as $if) {
-                if (file_exists($path . $if)) {
+                if ($fileSystem->exists($path . $if)) {
                     $path .= $if;
                     break;
                 }
@@ -251,7 +280,7 @@ class Module
             throw new Error\NotFound('Directory listing not available.');
         }
 
-        if (!file_exists($path)) {
+        if (!$fileSystem->exists($path)) {
             // file not found
             Logger::info('Could not find file \'' . $path . '\'.');
             throw new Error\NotFound('The URL wasn\'t found in the module.');
@@ -373,21 +402,11 @@ class Module
 
         $path = self::getModuleDir('.');
 
-        $dh = scandir($path);
-        if ($dh === false) {
-            throw new Exception('Unable to open module directory "' . $path . '".');
-        }
-
-        foreach ($dh as $f) {
-            if ($f[0] === '.') {
-                continue;
-            }
-
-            if (!is_dir($path . '/' . $f)) {
-                continue;
-            }
+        $finder = new Finder();
+        $finder->directories()->in($path)->depth(0);
 
-            self::$modules[] = $f;
+        foreach ($finder as $module) {
+            self::$modules[] = $module->getFileName();
         }
 
         return self::$modules;
@@ -485,25 +504,21 @@ class Module
             return self::$modules[$module]['hooks'];
         }
 
-        $hook_dir = self::getModuleDir($module) . '/hooks';
-        if (!is_dir($hook_dir)) {
-            return [];
-        }
-
         $hooks = [];
-        $files = scandir($hook_dir);
-        foreach ($files as $file) {
-            if ($file[0] === '.') {
-                continue;
-            }
-
-            if (!preg_match('/^hook_(\w+)\.php$/', $file, $matches)) {
-                continue;
+        $hook_dir = Path::canonicalize(dirname(dirname(dirname(__FILE__))) . '/modules/' . $module . '/hooks');
+        if ((new Filesystem())->exists($hook_dir)) {
+            $finder = new Finder();
+            $finder->files()->in($hook_dir)->depth(0);
+
+            foreach ($finder as $file) {
+                if (preg_match('/^hook_(\w+)\.php$/', $file->getFileName(), $matches)) {
+                    $hook_name = $matches[1];
+                    $hook_func = $module . '_hook_' . $hook_name;
+                    $hooks[$hook_name] = ['file' => Path::canonicalize(strval($file)), 'func' => $hook_func];
+                }
             }
-            $hook_name = $matches[1];
-            $hook_func = $module . '_hook_' . $hook_name;
-            $hooks[$hook_name] = ['file' => $hook_dir . '/' . $file, 'func' => $hook_func];
         }
+
         return $hooks;
     }
 
diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php
index 8b0c421aeea76ac00ea28c2859312987cf8b6c68..a0415a4a0bf5e4daac1a59f30ce3958cf012d193 100644
--- a/lib/SimpleSAML/XHTML/Template.php
+++ b/lib/SimpleSAML/XHTML/Template.php
@@ -10,8 +10,11 @@ declare(strict_types=1);
 
 namespace SimpleSAML\XHTML;
 
+use Exception;
+use InvalidArgumentException;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Configuration;
+use SimpleSAML\Error;
 use SimpleSAML\Locale\Language;
 use SimpleSAML\Locale\Localization;
 use SimpleSAML\Locale\Translate;
@@ -20,12 +23,32 @@ use SimpleSAML\Logger;
 use SimpleSAML\Module;
 use SimpleSAML\Utils;
 use Symfony\Bridge\Twig\Extension\TranslationExtension;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Finder\Finder;
+use Symfony\Component\HttpFoundation\File\File;
 use Symfony\Component\HttpFoundation\Response;
 use Twig\Environment;
+use Twig\Error\RuntimeError;
+use Twig\Extra\Intl\IntlExtension;
 use Twig\Loader\FilesystemLoader;
 use Twig\TwigFilter;
 use Twig\TwigFunction;
 
+use function class_exists;
+use function count;
+use function date;
+use function explode;
+use function hash;
+use function htmlspecialchars;
+use function in_array;
+use function is_null;
+use function key;
+use function ksort;
+use function strripos;
+use function strtolower;
+use function strval;
+use function substr;
+
 /**
  * The content-property is set upstream, but this is not recognized by Psalm
  * @psalm-suppress PropertyNotSetInConstructor
@@ -110,6 +133,11 @@ class Template extends Response
      */
     private array $theme = ['module' => null, 'name' => 'default'];
 
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    private Filesystem $fileSystem;
+
 
     /**
      * Constructor
@@ -150,6 +178,7 @@ class Template extends Response
         $this->twig = $this->setupTwig();
 
         $this->charset = 'UTF-8';
+        $this->fileSystem = new Filesystem();
         parent::__construct();
     }
 
@@ -173,14 +202,15 @@ class Template extends Response
             $path = Module::getModuleUrl($module . '/assets/' . $asset);
         }
 
-        if (!file_exists($file)) {
+        if (!$this->fileSystem->exists($file)) {
             // don't be too harsh if an asset is missing, just pretend it's there...
             return $path;
         }
+        $file = new File($file);
 
         $tag = $this->configuration->getVersion();
         if ($tag === 'master') {
-            $tag = strval(filemtime($file));
+            $tag = strval($file->getMtime());
         }
         $tag = substr(hash('md5', $tag), 0, 5);
 
@@ -240,7 +270,7 @@ class Template extends Response
                 $templateDirs[] = [
                     $this->theme['module'] => TemplateLoader::getModuleTemplateDir($this->theme['module'])
                 ];
-            } catch (\InvalidArgumentException $e) {
+            } catch (InvalidArgumentException $e) {
                 // either the module is not enabled or it has no "templates" directory, ignore
             }
         }
@@ -273,7 +303,7 @@ class Template extends Response
 
         // abort if twig template does not exist
         if (!$loader->exists($this->twig_template)) {
-            throw new \Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.');
+            throw new Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.');
         }
 
         // load extra i18n domains
@@ -294,7 +324,7 @@ class Template extends Response
         $twig = new Environment($loader, $options);
         $twigTranslator = new TwigTranslator([Translate::class, 'translateSingularGettext']);
         $twig->addExtension(new TranslationExtension($twigTranslator));
-        $twig->addExtension(new \Twig\Extra\Intl\IntlExtension());
+        $twig->addExtension(new IntlExtension());
 
         $twig->addFunction(new TwigFunction('moduleURL', [Module::class, 'getModuleURL']));
 
@@ -356,28 +386,30 @@ class Template extends Response
 
         // setup directories & namespaces
         $themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name'];
-        $subdirs = @scandir($themeDir);
-        if (empty($subdirs)) {
+
+        if (!$this->fileSystem->exists($themeDir)) {
             Logger::warning(
-                sprintf(
-                    'Theme directory for theme "%s" (%s) is not readable or is empty.',
-                    $this->theme['name'],
-                    $themeDir
-                )
+                sprintf('Theme directory for theme "%s" (%s) does not exist.', $this->theme['name'], $themeDir),
             );
             return [];
         }
 
-        $themeTemplateDirs = [];
-        foreach ($subdirs as $entry) {
-            // discard anything that's not a directory. Expression is negated to profit from lazy evaluation
-            if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir . '/' . $entry))) {
-                continue;
-            }
+        $finder = new Finder();
+        $finder->directories()->in($themeDir)->depth(0);
+        if (!$finder->hasResults()) {
+            Logger::warning(sprintf(
+                'Theme directory for theme "%s" (%s) is not readable or is empty.',
+                $this->theme['name'],
+                $themeDir,
+            ));
+            return [];
+        }
 
+        $themeTemplateDirs = [];
+        foreach ($finder as $entry) {
             // set correct name for the default namespace
-            $ns = ($entry === 'default') ? FilesystemLoader::MAIN_NAMESPACE : $entry;
-            $themeTemplateDirs[] = [$ns => $themeDir . '/' . $entry];
+            $ns = ($entry->getFileName() === 'default') ? FilesystemLoader::MAIN_NAMESPACE : $entry->getFileName();
+            $themeTemplateDirs[] = [$ns => strval($entry)];
         }
         return $themeTemplateDirs;
     }
@@ -394,15 +426,16 @@ class Template extends Response
     private function getModuleTemplateDir(string $module): string
     {
         if (!Module::isModuleEnabled($module)) {
-            throw new \InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
+            throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
         }
         $moduledir = Module::getModuleDir($module);
         // check if module has a /templates dir, if so, append
-        $templatedir = $moduledir . '/templates';
-        if (!is_dir($templatedir)) {
-            throw new \InvalidArgumentException('The module \'' . $module . '\' has no templates directory.');
+        $templateDir = $moduledir . '/templates';
+        $file = new File($templateDir);
+        if (!$file->isDir()) {
+            throw new InvalidArgumentException('The module \'' . $module . '\' has no templates directory.');
         }
-        return $templatedir;
+        return $templateDir;
     }
 
 
@@ -515,8 +548,8 @@ class Template extends Response
         }
         try {
             return $this->twig->render($this->twig_template, $this->data);
-        } catch (\Twig\Error\RuntimeError $e) {
-            throw new \SimpleSAML\Error\Exception(substr($e->getMessage(), 0, -1) . ' in ' . $this->template, 0, $e);
+        } catch (RuntimeError $e) {
+            throw new Error\Exception(substr($e->getMessage(), 0, -1) . ' in ' . $this->template, 0, $e);
         }
     }
 
diff --git a/lib/SimpleSAML/XML/Signer.php b/lib/SimpleSAML/XML/Signer.php
index 24f62d7c3af2004e852f35b7922a5b3c452d8c40..2ba619b56428ca3694c178414f9b642d98b93959 100644
--- a/lib/SimpleSAML/XML/Signer.php
+++ b/lib/SimpleSAML/XML/Signer.php
@@ -5,7 +5,7 @@
  *
  * This is a helper class for signing XML documents.
  *
- * @package SimpleSAMLphp
+ * @package simplesamlphp/simplesamlphp
  */
 
 declare(strict_types=1);
@@ -15,10 +15,15 @@ namespace SimpleSAML\XML;
 use DOMComment;
 use DOMElement;
 use DOMText;
+use Exception;
 use RobRichards\XMLSecLibs\XMLSecurityDSig;
 use RobRichards\XMLSecLibs\XMLSecurityKey;
 use SimpleSAML\Assert\Assert;
 use SimpleSAML\Utils;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\File\File;
+
+use function array_key_exists;
 
 class Signer
 {
@@ -37,12 +42,16 @@ class Signer
      */
     private string $certificate = '';
 
-
     /**
      * @var array Extra certificates which should be included in the response.
      */
     private array $extraCertificates = [];
 
+    /**
+     * @var \Symfony\Component\Filesystem\Filesystem;
+     */
+    private Filesystem $fileSystem;
+
 
     /**
      * Constructor for the metadata signer.
@@ -62,6 +71,8 @@ class Signer
      */
     public function __construct(array $options = [])
     {
+        $this->fileSystem = new Filesystem();
+
         if (array_key_exists('privatekey', $options)) {
             $pass = null;
             if (array_key_exists('privatekey_pass', $options)) {
@@ -131,12 +142,14 @@ class Signer
             $keyFile = $file;
         }
 
-        if (!file_exists($keyFile)) {
-            throw new \Exception('Could not find private key file "' . $keyFile . '".');
+        if (!$this->fileSystem->exists($keyFile)) {
+            throw new Exception('Could not find private key file "' . $keyFile . '".');
         }
-        $keyData = file_get_contents($keyFile);
+
+        $file = new File($keyFile);
+        $keyData = $file->getContent();
         if ($keyData === false) {
-            throw new \Exception('Unable to read private key file "' . $keyFile . '".');
+            throw new Exception('Unable to read private key file "' . $keyFile . '".');
         }
 
         $privatekey = ['PEM' => $keyData];
@@ -160,7 +173,7 @@ class Signer
     {
         if (!array_key_exists('PEM', $publickey)) {
             // We have a public key with only a fingerprint
-            throw new \Exception('Tried to add a certificate fingerprint in a signature.');
+            throw new Exception('Tried to add a certificate fingerprint in a signature.');
         }
 
         // For now, we only assume that the public key is an X509 certificate
@@ -189,13 +202,14 @@ class Signer
             $certFile = $file;
         }
 
-        if (!file_exists($certFile)) {
-            throw new \Exception('Could not find certificate file "' . $certFile . '".');
+        if (!$this->fileSystem->exists($certFile)) {
+            throw new Exception('Could not find certificate file "' . $certFile . '".');
         }
 
-        $cert = file_get_contents($certFile);
+        $file = new File($certFile);
+        $cert = $file->getContent();
         if ($cert === false) {
-            throw new \Exception('Unable to read certificate file "' . $certFile . '".');
+            throw new Exception('Unable to read certificate file "' . $certFile . '".');
         }
         $this->certificate = $cert;
     }
@@ -232,13 +246,14 @@ class Signer
             $certFile = $file;
         }
 
-        if (!file_exists($certFile)) {
-            throw new \Exception('Could not find extra certificate file "' . $certFile . '".');
+        if (!$this->fileSystem->exists($certFile)) {
+            throw new Exception('Could not find extra certificate file "' . $certFile . '".');
         }
 
-        $certificate = file_get_contents($certFile);
+        $file = new File($certFile);
+        $certificate = $file->getContent();
         if ($certificate === false) {
-            throw new \Exception('Unable to read extra certificate file "' . $certFile . '".');
+            throw new Exception('Unable to read extra certificate file "' . $certFile . '".');
         }
 
         $this->extraCertificates[] = $certificate;
@@ -263,7 +278,7 @@ class Signer
 
         $privateKey = $this->privateKey;
         if ($privateKey === false) {
-            throw new \Exception('Private key not set.');
+            throw new Exception('Private key not set.');
         }
 
 
diff --git a/modules/core/lib/Auth/Process/AttributeMap.php b/modules/core/lib/Auth/Process/AttributeMap.php
index 16cf735ddbc1285e98f175df17d298ea75079ba1..23767bebdf927728387a23d790f7a1b2700a03b8 100644
--- a/modules/core/lib/Auth/Process/AttributeMap.php
+++ b/modules/core/lib/Auth/Process/AttributeMap.php
@@ -9,6 +9,7 @@ use SimpleSAML\Assert\Assert;
 use SimpleSAML\Auth;
 use SimpleSAML\Configuration;
 use SimpleSAML\Module;
+use Symfony\Component\Filesystem\Filesystem;
 
 /**
  * Attribute filter for renaming attributes.
@@ -93,7 +94,8 @@ class AttributeMap extends Auth\ProcessingFilter
             $filePath = $attributenamemapdir . $fileName . '.php';
         }
 
-        if (!file_exists($filePath)) {
+        $fileSystem = new Filesystem();
+        if (!$fileSystem->exists($filePath)) {
             throw new Exception('Could not find attribute map file: ' . $filePath);
         }
 
diff --git a/tests/lib/SimpleSAML/ModuleTest.php b/tests/lib/SimpleSAML/ModuleTest.php
index 966f9c75f74cddfee3555f8a0ad90b5ac922cbe6..c5bb51e3cf96dd47216477e8abf03edddedb9664 100644
--- a/tests/lib/SimpleSAML/ModuleTest.php
+++ b/tests/lib/SimpleSAML/ModuleTest.php
@@ -8,6 +8,7 @@ use Exception;
 use PHPUnit\Framework\TestCase;
 use SimpleSAML\Configuration;
 use SimpleSAML\Module;
+use Symfony\Component\Filesystem\Path;
 
 /**
  * @covers \SimpleSAML\Module
@@ -31,8 +32,8 @@ class ModuleTest extends TestCase
     {
         // test for the most basic functionality
         $this->assertEquals(
-            dirname(dirname(dirname(dirname(__FILE__)))) . '/modules/module',
-            Module::getModuleDir('module')
+            Path::canonicalize(dirname(dirname(dirname(dirname(__FILE__)))) . '/modules/module'),
+            Path::canonicalize(Module::getModuleDir('module')),
         );
     }
 
@@ -121,7 +122,7 @@ class ModuleTest extends TestCase
         $hooks = Module::getModuleHooks('cron');
         $this->assertArrayHasKey('configpage', $hooks);
         $this->assertEquals('cron_hook_configpage', $hooks['configpage']['func']);
-        $expectedFile = dirname(__DIR__, 3) . '/modules/cron/hooks/hook_configpage.php';
+        $expectedFile = Path::canonicalize(dirname(__DIR__, 3) . '/modules/cron/hooks/hook_configpage.php');
         $this->assertEquals($expectedFile, $hooks['configpage']['file']);
     }