From 65d79d4c9350665fedbf434799fed335de64688e Mon Sep 17 00:00:00 2001
From: Philipp <admin@philipp.info>
Date: Tue, 3 Jan 2023 14:18:53 +0100
Subject: [PATCH] Introduce ISetConfigValuesTransactional for transactional
 config behaviour

---
 src/Console/Maintenance.php                   |  10 +-
 src/Console/Relocate.php                      |   7 +-
 .../Config/Capability/IManageConfigValues.php |  29 ++-
 .../ISetConfigValuesTransactional.php         |  84 +++++++++
 src/Core/Config/Model/Config.php              |  43 +++--
 src/Core/Config/Model/TransactionalConfig.php |  89 +++++++++
 src/Core/Config/ValueObject/Cache.php         |  67 +++++++
 src/Core/Update.php                           |  28 +--
 src/Database/DBStructure.php                  |   7 +-
 src/Module/Admin/Site.php                     | 176 +++++++++---------
 tests/src/Core/Config/Cache/CacheTest.php     |  48 +++++
 .../Config/Cache/ConfigFileLoaderTest.php     |  34 ++--
 .../Core/Config/TransactionalConfigTest.php   | 110 +++++++++++
 update.php                                    |   6 +-
 14 files changed, 588 insertions(+), 150 deletions(-)
 create mode 100644 src/Core/Config/Capability/ISetConfigValuesTransactional.php
 create mode 100644 src/Core/Config/Model/TransactionalConfig.php
 create mode 100644 tests/src/Core/Config/TransactionalConfigTest.php

diff --git a/src/Console/Maintenance.php b/src/Console/Maintenance.php
index bd3aef7c29..6a11eb2bb5 100644
--- a/src/Console/Maintenance.php
+++ b/src/Console/Maintenance.php
@@ -100,17 +100,19 @@ HELP;
 
 		$enabled = intval($this->getArgument(0));
 
-		$this->config->set('system', 'maintenance', $enabled, false);
+		$transactionConfig = $this->config->transactional();
+
+		$transactionConfig->set('system', 'maintenance', $enabled);
 
 		$reason = $this->getArgument(1);
 
 		if ($enabled && $this->getArgument(1)) {
-			$this->config->set('system', 'maintenance_reason', $this->getArgument(1), false);
+			$transactionConfig->set('system', 'maintenance_reason', $this->getArgument(1));
 		} else {
-			$this->config->set('system', 'maintenance_reason', '', false);
+			$transactionConfig->delete('system', 'maintenance_reason');
 		}
 
-		$this->config->save();
+		$transactionConfig->save();
 
 		if ($enabled) {
 			$mode_str = "maintenance mode";
diff --git a/src/Console/Relocate.php b/src/Console/Relocate.php
index 8a76c92070..7a2ef1d071 100644
--- a/src/Console/Relocate.php
+++ b/src/Console/Relocate.php
@@ -189,9 +189,10 @@ HELP;
 			return 1;
 		} finally {
 			$this->out('Leaving maintenance mode');
-			$this->config->set('system', 'maintenance', false, false);
-			$this->config->set('system', 'maintenance_reason', '', false);
-			$this->config->save();
+			$this->config->transactional()
+						 ->set('system', 'maintenance', false)
+						 ->delete('system', 'maintenance_reason')
+						 ->save();
 		}
 
 		// send relocate
diff --git a/src/Core/Config/Capability/IManageConfigValues.php b/src/Core/Config/Capability/IManageConfigValues.php
index 88fa96314b..42ebea0004 100644
--- a/src/Core/Config/Capability/IManageConfigValues.php
+++ b/src/Core/Config/Capability/IManageConfigValues.php
@@ -22,6 +22,7 @@
 namespace Friendica\Core\Config\Capability;
 
 use Friendica\Core\Config\Exception\ConfigPersistenceException;
+use Friendica\Core\Config\Util\ConfigFileManager;
 use Friendica\Core\Config\ValueObject\Cache;
 
 /**
@@ -57,6 +58,20 @@ interface IManageConfigValues
 	 */
 	public function get(string $cat, string $key, $default_value = null);
 
+	/**
+	 * Load all configuration values from a given cache and saves it back in the configuration node store
+	 * @see	ConfigFileManager::CONFIG_DATA_FILE
+	 *
+	 * All configuration values of the system are stored in the cache.
+	 *
+	 * @param Cache $cache a new cache
+	 *
+	 * @return void
+	 *
+	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 */
+	public function load(Cache $cache);
+
 	/**
 	 * Sets a configuration value for system config
 	 *
@@ -67,20 +82,21 @@ interface IManageConfigValues
 	 * @param string $cat The category of the configuration value
 	 * @param string $key    The configuration key to set
 	 * @param mixed  $value  The value to store
-	 * @param bool   $autosave If true, implicit save the value
 	 *
 	 * @return bool Operation success
 	 *
 	 * @throws ConfigPersistenceException In case the persistence layer throws errors
 	 */
-	public function set(string $cat, string $key, $value, bool $autosave = true): bool;
+	public function set(string $cat, string $key, $value): bool;
 
 	/**
-	 * Save back the overridden values of the config cache
+	 * Creates a transactional config value store, which is used to set a bunch of values at once
 	 *
-	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 * It relies on the current instance, so after save(), the values of this config class will get altered at once too.
+	 *
+	 * @return ISetConfigValuesTransactional
 	 */
-	public function save();
+	public function transactional(): ISetConfigValuesTransactional;
 
 	/**
 	 * Deletes the given key from the system configuration.
@@ -89,14 +105,13 @@ interface IManageConfigValues
 	 *
 	 * @param string $cat The category of the configuration value
 	 * @param string $key    The configuration key to delete
-	 * @param bool   $autosave If true, implicit save the value
 	 *
 	 * @return bool
 	 *
 	 * @throws ConfigPersistenceException In case the persistence layer throws errors
 	 *
 	 */
-	public function delete(string $cat, string $key, bool $autosave = true): bool;
+	public function delete(string $cat, string $key): bool;
 
 	/**
 	 * Returns the Config Cache
diff --git a/src/Core/Config/Capability/ISetConfigValuesTransactional.php b/src/Core/Config/Capability/ISetConfigValuesTransactional.php
new file mode 100644
index 0000000000..9c58427a04
--- /dev/null
+++ b/src/Core/Config/Capability/ISetConfigValuesTransactional.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Core\Config\Capability;
+
+use Friendica\Core\Config\Exception\ConfigPersistenceException;
+
+/**
+ * Interface for transactional saving of config values
+ * It buffers every set/delete until "save()" is called
+ */
+interface ISetConfigValuesTransactional
+{
+	/**
+	 * Get a particular user's config variable given the category name
+	 * ($cat) and a $key.
+	 *
+	 * Get a particular config value from the given category ($cat)
+	 *
+	 * @param string  $cat        The category of the configuration value
+	 * @param string  $key           The configuration key to query
+	 *
+	 * @return mixed Stored value or null if it does not exist
+	 *
+	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 *
+	 */
+	public function get(string $cat, string $key);
+
+	/**
+	 * Sets a configuration value for system config
+	 *
+	 * Stores a config value ($value) in the category ($cat) under the key ($key)
+	 *
+	 * Note: Please do not store booleans - convert to 0/1 integer values!
+	 *
+	 * @param string $cat The category of the configuration value
+	 * @param string $key    The configuration key to set
+	 * @param mixed  $value  The value to store
+	 *
+	 * @return static the current instance
+	 *
+	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 */
+	public function set(string $cat, string $key, $value): self;
+
+	/**
+	 * Deletes the given key from the system configuration.
+	 *
+	 * @param string $cat The category of the configuration value
+	 * @param string $key The configuration key to delete
+	 *
+	 * @return static the current instance
+	 *
+	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 *
+	 */
+	public function delete(string $cat, string $key): self;
+
+	/**
+	 * Saves the node specific config values
+	 *
+	 * @throws ConfigPersistenceException In case the persistence layer throws errors
+	 */
+	public function save(): void;
+}
diff --git a/src/Core/Config/Model/Config.php b/src/Core/Config/Model/Config.php
index 7829b75ffd..24f5fd3b59 100644
--- a/src/Core/Config/Model/Config.php
+++ b/src/Core/Config/Model/Config.php
@@ -22,6 +22,7 @@
 namespace Friendica\Core\Config\Model;
 
 use Friendica\Core\Config\Capability\IManageConfigValues;
+use Friendica\Core\Config\Capability\ISetConfigValuesTransactional;
 use Friendica\Core\Config\Exception\ConfigFileException;
 use Friendica\Core\Config\Exception\ConfigPersistenceException;
 use Friendica\Core\Config\Util\ConfigFileManager;
@@ -61,8 +62,17 @@ class Config implements IManageConfigValues
 		return $this->configCache;
 	}
 
-	/** {@inheritDoc} */
-	public function save()
+	/**	{@inheritDoc} */
+	public function transactional(): ISetConfigValuesTransactional
+	{
+		return new TransactionalConfig($this);
+	}
+
+	/**
+	 * Saves the current Configuration back into the data config.
+	 * @see ConfigFileManager::CONFIG_DATA_FILE
+	 */
+	protected function save()
 	{
 		try {
 			$this->configFileManager->saveData($this->configCache);
@@ -84,6 +94,13 @@ class Config implements IManageConfigValues
 		$this->configCache = $configCache;
 	}
 
+	/** {@inheritDoc} */
+	public function load(Cache $cache)
+	{
+		$this->configCache = $cache;
+		$this->save();
+	}
+
 	/** {@inheritDoc} */
 	public function get(string $cat, string $key, $default_value = null)
 	{
@@ -91,26 +108,24 @@ class Config implements IManageConfigValues
 	}
 
 	/** {@inheritDoc} */
-	public function set(string $cat, string $key, $value, bool $autosave = true): bool
+	public function set(string $cat, string $key, $value): bool
 	{
-		$stored = $this->configCache->set($cat, $key, $value, Cache::SOURCE_DATA);
-
-		if ($stored && $autosave) {
+		if ($this->configCache->set($cat, $key, $value, Cache::SOURCE_DATA)) {
 			$this->save();
+			return true;
+		} else {
+			return false;
 		}
-
-		return $stored;
 	}
 
 	/** {@inheritDoc} */
-	public function delete(string $cat, string $key, bool $autosave = true): bool
+	public function delete(string $cat, string $key): bool
 	{
-		$removed = $this->configCache->delete($cat, $key);
-
-		if ($removed && $autosave) {
+		if ($this->configCache->delete($cat, $key)) {
 			$this->save();
+			return true;
+		} else {
+			return false;
 		}
-
-		return $removed;
 	}
 }
diff --git a/src/Core/Config/Model/TransactionalConfig.php b/src/Core/Config/Model/TransactionalConfig.php
new file mode 100644
index 0000000000..e9aa71160f
--- /dev/null
+++ b/src/Core/Config/Model/TransactionalConfig.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Core\Config\Model;
+
+use Friendica\Core\Config\Capability\IManageConfigValues;
+use Friendica\Core\Config\Capability\ISetConfigValuesTransactional;
+use Friendica\Core\Config\Exception\ConfigPersistenceException;
+use Friendica\Core\Config\ValueObject\Cache;
+
+/**
+ * config class, which sets values into a temporary buffer until "save()" is called
+ */
+class TransactionalConfig implements ISetConfigValuesTransactional
+{
+	/** @var IManageConfigValues */
+	protected $config;
+	/** @var Cache */
+	protected $cache;
+	/** @var Cache */
+	protected $delCache;
+
+	public function __construct(IManageConfigValues $config)
+	{
+		$this->config   = $config;
+		$this->cache    = new Cache();
+		$this->delCache = new Cache();
+	}
+
+	/** {@inheritDoc} */
+	public function get(string $cat, string $key)
+	{
+		return !$this->delCache->get($cat, $key) ?
+			($this->cache->get($cat, $key) ?? $this->config->get($cat, $key)) :
+			null;
+	}
+
+	/** {@inheritDoc} */
+	public function set(string $cat, string $key, $value): ISetConfigValuesTransactional
+	{
+		$this->cache->set($cat, $key, $value, Cache::SOURCE_DATA);
+
+		return $this;
+	}
+
+
+	/** {@inheritDoc} */
+	public function delete(string $cat, string $key): ISetConfigValuesTransactional
+	{
+		$this->cache->delete($cat, $key);
+		$this->delCache->set($cat, $key, 'deleted');
+
+		return $this;
+	}
+
+	/** {@inheritDoc} */
+	public function save(): void
+	{
+		try {
+			$newCache = $this->config->getCache()->merge($this->cache);
+			$newCache = $newCache->diff($this->delCache);
+			$this->config->load($newCache);
+
+			// flush current cache
+			$this->cache    = new Cache();
+			$this->delCache = new Cache();
+		} catch (\Exception $e) {
+			throw new ConfigPersistenceException('Cannot save config', $e);
+		}
+	}
+}
diff --git a/src/Core/Config/ValueObject/Cache.php b/src/Core/Config/ValueObject/Cache.php
index a074414bfc..305c00d330 100644
--- a/src/Core/Config/ValueObject/Cache.php
+++ b/src/Core/Config/ValueObject/Cache.php
@@ -279,4 +279,71 @@ class Cache
 
 		return $return;
 	}
+
+	/**
+	 * Merges a new Cache into the existing one and returns the merged Cache
+	 *
+	 * @param Cache $cache The cache, which should get merged into this Cache
+	 *
+	 * @return Cache The merged Cache
+	 */
+	public function merge(Cache $cache): Cache
+	{
+		$newConfig = $this->config;
+		$newSource = $this->source;
+
+		$categories = array_keys($cache->config);
+
+		foreach ($categories as $category) {
+			if (is_array($cache->config[$category])) {
+				$keys = array_keys($cache->config[$category]);
+
+				foreach ($keys as $key) {
+					$newConfig[$category][$key] = $cache->config[$category][$key];
+					$newSource[$category][$key] = $cache->source[$category][$key];
+				}
+			}
+		}
+
+		$newCache = new Cache();
+		$newCache->config = $newConfig;
+		$newCache->source = $newSource;
+
+		return $newCache;
+	}
+
+
+	/**
+	 * Diffs a new Cache into the existing one and returns the diffed Cache
+	 *
+	 * @param Cache $cache The cache, which should get deleted for the current Cache
+	 *
+	 * @return Cache The diffed Cache
+	 */
+	public function diff(Cache $cache): Cache
+	{
+		$newConfig = $this->config;
+		$newSource = $this->source;
+
+		$categories = array_keys($cache->config);
+
+		foreach ($categories as $category) {
+			if (is_array($cache->config[$category])) {
+				$keys = array_keys($cache->config[$category]);
+
+				foreach ($keys as $key) {
+					if (!is_null($newConfig[$category][$key] ?? null)) {
+						unset($newConfig[$category][$key]);
+						unset($newSource[$category][$key]);
+					}
+				}
+			}
+		}
+
+		$newCache = new Cache();
+		$newCache->config = $newConfig;
+		$newCache->source = $newSource;
+
+		return $newCache;
+	}
 }
diff --git a/src/Core/Update.php b/src/Core/Update.php
index 9a2ebe1bba..a026457833 100644
--- a/src/Core/Update.php
+++ b/src/Core/Update.php
@@ -160,9 +160,10 @@ class Update
 							Logger::warning('Pre update failed', ['version' => $version]);
 							DI::config()->set('system', 'update', Update::FAILED);
 							DI::lock()->release('dbupdate');
-							DI::config()->set('system', 'maintenance', false, false);
-							DI::config()->delete('system', 'maintenance_reason', false);
-							DI::config()->save();
+							DI::config()->transactional()
+										->set('system', 'maintenance', false)
+										->delete('system', 'maintenance_reason')
+										->save();
 							return $r;
 						} else {
 							Logger::notice('Pre update executed.', ['version' => $version]);
@@ -182,9 +183,10 @@ class Update
 						Logger::error('Update ERROR.', ['from' => $stored, 'to' => $current, 'retval' => $retval]);
 						DI::config()->set('system', 'update', Update::FAILED);
 						DI::lock()->release('dbupdate');
-						DI::config()->set('system', 'maintenance', false, false);
-						DI::config()->delete('system', 'maintenance_reason', false);
-						DI::config()->save();
+						DI::config()->transactional()
+									->set('system', 'maintenance', false)
+									->delete('system', 'maintenance_reason')
+									->save();
 						return $retval;
 					} else {
 						Logger::notice('Database structure update finished.', ['from' => $stored, 'to' => $current]);
@@ -200,9 +202,10 @@ class Update
 							Logger::warning('Post update failed', ['version' => $version]);
 							DI::config()->set('system', 'update', Update::FAILED);
 							DI::lock()->release('dbupdate');
-							DI::config()->set('system', 'maintenance', false, false);
-							DI::config()->delete('system', 'maintenance_reason', false);
-							DI::config()->save();
+							DI::config()->transactional()
+										->set('system', 'maintenance', false)
+										->delete('system', 'maintenance_reason')
+										->save();
 							return $r;
 						} else {
 							DI::config()->set('system', 'build', $version);
@@ -213,9 +216,10 @@ class Update
 					DI::config()->set('system', 'build', $current);
 					DI::config()->set('system', 'update', Update::SUCCESS);
 					DI::lock()->release('dbupdate');
-					DI::config()->set('system', 'maintenance', false, false);
-					DI::config()->delete('system', 'maintenance_reason',  false);
-					DI::config()->save();
+					DI::config()->transactional()
+								->set('system', 'maintenance', false)
+								->delete('system', 'maintenance_reason')
+								->save();
 
 					Logger::notice('Update success.', ['from' => $stored, 'to' => $current]);
 					if ($sendMail) {
diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php
index e3af408b9e..ed2a5e30e7 100644
--- a/src/Database/DBStructure.php
+++ b/src/Database/DBStructure.php
@@ -182,9 +182,10 @@ class DBStructure
 		$status = self::update($verbose, true);
 
 		if ($enable_maintenance_mode) {
-			DI::config()->set('system', 'maintenance', false, false);
-			DI::config()->delete('system', 'maintenance_reason', false);
-			DI::config()->save();
+			DI::config()->transactional()
+						->set('system', 'maintenance', false)
+						->delete('system', 'maintenance_reason')
+						->save();
 		}
 
 		return $status;
diff --git a/src/Module/Admin/Site.php b/src/Module/Admin/Site.php
index f572d3e725..50a7ee8686 100644
--- a/src/Module/Admin/Site.php
+++ b/src/Module/Admin/Site.php
@@ -48,8 +48,6 @@ class Site extends BaseAdmin
 
 		self::checkFormSecurityTokenRedirectOnError('/admin/site', 'admin_site');
 
-		$a = DI::app();
-
 		if (!empty($_POST['republish_directory'])) {
 			Worker::add(Worker::PRIORITY_LOW, 'Directory');
 			return;
@@ -146,9 +144,11 @@ class Site extends BaseAdmin
 		$relay_user_tags   = !empty($_POST['relay_user_tags']);
 		$active_panel      = (!empty($_POST['active_panel'])      ? "#" . trim($_POST['active_panel']) : '');
 
+		$transactionConfig = DI::config()->transactional();
+
 		// Has the directory url changed? If yes, then resubmit the existing profiles there
 		if ($global_directory != DI::config()->get('system', 'directory') && ($global_directory != '')) {
-			DI::config()->set('system', 'directory', $global_directory, false);
+			$transactionConfig->set('system', 'directory', $global_directory);
 			Worker::add(Worker::PRIORITY_LOW, 'Directory');
 		}
 
@@ -194,133 +194,133 @@ class Site extends BaseAdmin
 				);
 			}
 		}
-		DI::config()->set('system', 'ssl_policy'            , $ssl_policy, false);
-		DI::config()->set('system', 'maxloadavg'            , $maxloadavg, false);
-		DI::config()->set('system', 'min_memory'            , $min_memory, false);
-		DI::config()->set('system', 'optimize_tables'       , $optimize_tables, false);
-		DI::config()->set('system', 'contact_discovery'     , $contact_discovery, false);
-		DI::config()->set('system', 'synchronize_directory' , $synchronize_directory, false);
-		DI::config()->set('system', 'poco_requery_days'     , $poco_requery_days, false);
-		DI::config()->set('system', 'poco_discovery'        , $poco_discovery, false);
-		DI::config()->set('system', 'poco_local_search'     , $poco_local_search, false);
-		DI::config()->set('system', 'nodeinfo'              , $nodeinfo, false);
-		DI::config()->set('config', 'sitename'              , $sitename, false);
-		DI::config()->set('config', 'sender_email'          , $sender_email, false);
-		DI::config()->set('system', 'suppress_tags'         , $suppress_tags, false);
-		DI::config()->set('system', 'shortcut_icon'         , $shortcut_icon, false);
-		DI::config()->set('system', 'touch_icon'            , $touch_icon, false);
+		$transactionConfig->set('system', 'ssl_policy'            , $ssl_policy);
+		$transactionConfig->set('system', 'maxloadavg'            , $maxloadavg);
+		$transactionConfig->set('system', 'min_memory'            , $min_memory);
+		$transactionConfig->set('system', 'optimize_tables'       , $optimize_tables);
+		$transactionConfig->set('system', 'contact_discovery'     , $contact_discovery);
+		$transactionConfig->set('system', 'synchronize_directory' , $synchronize_directory);
+		$transactionConfig->set('system', 'poco_requery_days'     , $poco_requery_days);
+		$transactionConfig->set('system', 'poco_discovery'        , $poco_discovery);
+		$transactionConfig->set('system', 'poco_local_search'     , $poco_local_search);
+		$transactionConfig->set('system', 'nodeinfo'              , $nodeinfo);
+		$transactionConfig->set('config', 'sitename'              , $sitename);
+		$transactionConfig->set('config', 'sender_email'          , $sender_email);
+		$transactionConfig->set('system', 'suppress_tags'         , $suppress_tags);
+		$transactionConfig->set('system', 'shortcut_icon'         , $shortcut_icon);
+		$transactionConfig->set('system', 'touch_icon'            , $touch_icon);
 
 		if ($banner == "") {
-			DI::config()->delete('system', 'banner', false);
+			$transactionConfig->delete('system', 'banner');
 		} else {
-			DI::config()->set('system', 'banner', $banner, false);
+			$transactionConfig->set('system', 'banner', $banner);
 		}
 
 		if (empty($email_banner)) {
-			DI::config()->delete('system', 'email_banner', false);
+			$transactionConfig->delete('system', 'email_banner');
 		} else {
-			DI::config()->set('system', 'email_banner', $email_banner, false);
+			$transactionConfig->set('system', 'email_banner', $email_banner);
 		}
 
 		if (empty($additional_info)) {
-			DI::config()->delete('config', 'info', false);
+			$transactionConfig->delete('config', 'info');
 		} else {
-			DI::config()->set('config', 'info', $additional_info, false);
+			$transactionConfig->set('config', 'info', $additional_info);
 		}
-		DI::config()->set('system', 'language', $language, false);
-		DI::config()->set('system', 'theme', $theme, false);
+		$transactionConfig->set('system', 'language', $language);
+		$transactionConfig->set('system', 'theme', $theme);
 		Theme::install($theme);
 
 		if ($theme_mobile == '---') {
-			DI::config()->delete('system', 'mobile-theme', false);
+			$transactionConfig->delete('system', 'mobile-theme');
 		} else {
-			DI::config()->set('system', 'mobile-theme', $theme_mobile, false);
+			$transactionConfig->set('system', 'mobile-theme', $theme_mobile);
 		}
 		if ($singleuser == '---') {
-			DI::config()->delete('system', 'singleuser', false);
+			$transactionConfig->delete('system', 'singleuser');
 		} else {
-			DI::config()->set('system', 'singleuser', $singleuser, false);
+			$transactionConfig->set('system', 'singleuser', $singleuser);
 		}
 		if (preg_match('/\d+(?:\s*[kmg])?/i', $maximagesize)) {
-			DI::config()->set('system', 'maximagesize', $maximagesize, false);
+			$transactionConfig->set('system', 'maximagesize', $maximagesize);
 		} else {
 			DI::sysmsg()->addNotice(DI::l10n()->t('%s is no valid input for maximum image size', $maximagesize));
 		}
-		DI::config()->set('system', 'max_image_length'       , $maximagelength, false);
-		DI::config()->set('system', 'jpeg_quality'           , $jpegimagequality, false);
+		$transactionConfig->set('system', 'max_image_length'       , $maximagelength);
+		$transactionConfig->set('system', 'jpeg_quality'           , $jpegimagequality);
 
-		DI::config()->set('config', 'register_policy'        , $register_policy, false);
-		DI::config()->set('system', 'max_daily_registrations', $daily_registrations, false);
-		DI::config()->set('system', 'account_abandon_days'   , $abandon_days, false);
-		DI::config()->set('config', 'register_text'          , $register_text, false);
-		DI::config()->set('system', 'allowed_sites'          , $allowed_sites, false);
-		DI::config()->set('system', 'allowed_email'          , $allowed_email, false);
-		DI::config()->set('system', 'forbidden_nicknames'    , $forbidden_nicknames, false);
-		DI::config()->set('system', 'system_actor_name'      , $system_actor_name, false);
-		DI::config()->set('system', 'no_oembed_rich_content' , $no_oembed_rich_content, false);
-		DI::config()->set('system', 'allowed_oembed'         , $allowed_oembed, false);
-		DI::config()->set('system', 'block_public'           , $block_public, false);
-		DI::config()->set('system', 'publish_all'            , $force_publish, false);
-		DI::config()->set('system', 'newuser_private'        , $newuser_private, false);
-		DI::config()->set('system', 'enotify_no_content'     , $enotify_no_content, false);
-		DI::config()->set('system', 'disable_embedded'       , $disable_embedded, false);
-		DI::config()->set('system', 'allow_users_remote_self', $allow_users_remote_self, false);
-		DI::config()->set('system', 'explicit_content'       , $explicit_content, false);
-		DI::config()->set('system', 'proxify_content'        , $proxify_content, false);
-		DI::config()->set('system', 'cache_contact_avatar'   , $cache_contact_avatar, false);
-		DI::config()->set('system', 'check_new_version_url'  , $check_new_version_url, false);
+		$transactionConfig->set('config', 'register_policy'        , $register_policy);
+		$transactionConfig->set('system', 'max_daily_registrations', $daily_registrations);
+		$transactionConfig->set('system', 'account_abandon_days'   , $abandon_days);
+		$transactionConfig->set('config', 'register_text'          , $register_text);
+		$transactionConfig->set('system', 'allowed_sites'          , $allowed_sites);
+		$transactionConfig->set('system', 'allowed_email'          , $allowed_email);
+		$transactionConfig->set('system', 'forbidden_nicknames'    , $forbidden_nicknames);
+		$transactionConfig->set('system', 'system_actor_name'      , $system_actor_name);
+		$transactionConfig->set('system', 'no_oembed_rich_content' , $no_oembed_rich_content);
+		$transactionConfig->set('system', 'allowed_oembed'         , $allowed_oembed);
+		$transactionConfig->set('system', 'block_public'           , $block_public);
+		$transactionConfig->set('system', 'publish_all'            , $force_publish);
+		$transactionConfig->set('system', 'newuser_private'        , $newuser_private);
+		$transactionConfig->set('system', 'enotify_no_content'     , $enotify_no_content);
+		$transactionConfig->set('system', 'disable_embedded'       , $disable_embedded);
+		$transactionConfig->set('system', 'allow_users_remote_self', $allow_users_remote_self);
+		$transactionConfig->set('system', 'explicit_content'       , $explicit_content);
+		$transactionConfig->set('system', 'proxify_content'        , $proxify_content);
+		$transactionConfig->set('system', 'cache_contact_avatar'   , $cache_contact_avatar);
+		$transactionConfig->set('system', 'check_new_version_url'  , $check_new_version_url);
 
-		DI::config()->set('system', 'block_extended_register', !$enable_multi_reg, false);
-		DI::config()->set('system', 'no_openid'              , !$enable_openid, false);
-		DI::config()->set('system', 'no_regfullname'         , !$enable_regfullname, false);
-		DI::config()->set('system', 'register_notification'  , $register_notification, false);
-		DI::config()->set('system', 'community_page_style'   , $community_page_style, false);
-		DI::config()->set('system', 'max_author_posts_community_page', $max_author_posts_community_page, false);
-		DI::config()->set('system', 'verifyssl'              , $verifyssl, false);
-		DI::config()->set('system', 'proxyuser'              , $proxyuser, false);
-		DI::config()->set('system', 'proxy'                  , $proxy, false);
-		DI::config()->set('system', 'curl_timeout'           , $timeout, false);
-		DI::config()->set('system', 'imap_disabled'          , !$mail_enabled && function_exists('imap_open'), false);
-		DI::config()->set('system', 'ostatus_disabled'       , !$ostatus_enabled, false);
-		DI::config()->set('system', 'diaspora_enabled'       , $diaspora_enabled, false);
+		$transactionConfig->set('system', 'block_extended_register', !$enable_multi_reg);
+		$transactionConfig->set('system', 'no_openid'              , !$enable_openid);
+		$transactionConfig->set('system', 'no_regfullname'         , !$enable_regfullname);
+		$transactionConfig->set('system', 'register_notification'  , $register_notification);
+		$transactionConfig->set('system', 'community_page_style'   , $community_page_style);
+		$transactionConfig->set('system', 'max_author_posts_community_page', $max_author_posts_community_page);
+		$transactionConfig->set('system', 'verifyssl'              , $verifyssl);
+		$transactionConfig->set('system', 'proxyuser'              , $proxyuser);
+		$transactionConfig->set('system', 'proxy'                  , $proxy);
+		$transactionConfig->set('system', 'curl_timeout'           , $timeout);
+		$transactionConfig->set('system', 'imap_disabled'          , !$mail_enabled && function_exists('imap_open'));
+		$transactionConfig->set('system', 'ostatus_disabled'       , !$ostatus_enabled);
+		$transactionConfig->set('system', 'diaspora_enabled'       , $diaspora_enabled);
 
-		DI::config()->set('config', 'private_addons'         , $private_addons, false);
+		$transactionConfig->set('config', 'private_addons'         , $private_addons);
 
-		DI::config()->set('system', 'force_ssl'              , $force_ssl, false);
-		DI::config()->set('system', 'hide_help'              , !$show_help, false);
+		$transactionConfig->set('system', 'force_ssl'              , $force_ssl);
+		$transactionConfig->set('system', 'hide_help'              , !$show_help);
 
-		DI::config()->set('system', 'dbclean'                , $dbclean, false);
-		DI::config()->set('system', 'dbclean-expire-days'    , $dbclean_expire_days, false);
-		DI::config()->set('system', 'dbclean_expire_conversation', $dbclean_expire_conv, false);
+		$transactionConfig->set('system', 'dbclean'                , $dbclean);
+		$transactionConfig->set('system', 'dbclean-expire-days'    , $dbclean_expire_days);
+		$transactionConfig->set('system', 'dbclean_expire_conversation', $dbclean_expire_conv);
 
 		if ($dbclean_unclaimed == 0) {
 			$dbclean_unclaimed = $dbclean_expire_days;
 		}
 
-		DI::config()->set('system', 'dbclean-expire-unclaimed', $dbclean_unclaimed, false);
+		$transactionConfig->set('system', 'dbclean-expire-unclaimed', $dbclean_unclaimed);
 
-		DI::config()->set('system', 'max_comments', $max_comments, false);
-		DI::config()->set('system', 'max_display_comments', $max_display_comments, false);
+		$transactionConfig->set('system', 'max_comments', $max_comments);
+		$transactionConfig->set('system', 'max_display_comments', $max_display_comments);
 
 		if ($temppath != '') {
 			$temppath = BasePath::getRealPath($temppath);
 		}
 
-		DI::config()->set('system', 'temppath', $temppath, false);
+		$transactionConfig->set('system', 'temppath', $temppath);
 
-		DI::config()->set('system', 'only_tag_search'  , $only_tag_search, false);
-		DI::config()->set('system', 'compute_group_counts', $compute_group_counts, false);
+		$transactionConfig->set('system', 'only_tag_search'  , $only_tag_search);
+		$transactionConfig->set('system', 'compute_group_counts', $compute_group_counts);
 
-		DI::config()->set('system', 'worker_queues'    , $worker_queues, false);
-		DI::config()->set('system', 'worker_fastlane'  , $worker_fastlane, false);
+		$transactionConfig->set('system', 'worker_queues'    , $worker_queues);
+		$transactionConfig->set('system', 'worker_fastlane'  , $worker_fastlane);
 
-		DI::config()->set('system', 'relay_directly'   , $relay_directly, false);
-		DI::config()->set('system', 'relay_scope'      , $relay_scope, false);
-		DI::config()->set('system', 'relay_server_tags', $relay_server_tags, false);
-		DI::config()->set('system', 'relay_deny_tags'  , $relay_deny_tags, false);
-		DI::config()->set('system', 'relay_user_tags'  , $relay_user_tags, false);
+		$transactionConfig->set('system', 'relay_directly'   , $relay_directly);
+		$transactionConfig->set('system', 'relay_scope'      , $relay_scope);
+		$transactionConfig->set('system', 'relay_server_tags', $relay_server_tags);
+		$transactionConfig->set('system', 'relay_deny_tags'  , $relay_deny_tags);
+		$transactionConfig->set('system', 'relay_user_tags'  , $relay_user_tags);
 
-		DI::config()->save();
+		$transactionConfig->save();
 
 		DI::baseUrl()->redirect('admin/site' . $active_panel);
 	}
@@ -334,8 +334,8 @@ class Site extends BaseAdmin
 
 		if (DI::config()->get('system', 'directory_submit_url') &&
 			!DI::config()->get('system', 'directory')) {
-			DI::config()->set('system', 'directory', dirname(DI::config()->get('system', 'directory_submit_url')), false);
-			DI::config()->delete('system', 'directory_submit_url', false);
+			DI::config()->set('system', 'directory', dirname(DI::config()->get('system', 'directory_submit_url')));
+			DI::config()->delete('system', 'directory_submit_url');
 		}
 
 		/* Installed themes */
diff --git a/tests/src/Core/Config/Cache/CacheTest.php b/tests/src/Core/Config/Cache/CacheTest.php
index 8b2c24da3e..9d72774c40 100644
--- a/tests/src/Core/Config/Cache/CacheTest.php
+++ b/tests/src/Core/Config/Cache/CacheTest.php
@@ -358,4 +358,52 @@ class CacheTest extends MockedTest
 		$this->assertEquals(['system' => ['test_2' => 'with_data']], $configCache->getDataBySource(Cache::SOURCE_DATA));
 		$this->assertEquals($data, $configCache->getDataBySource(Cache::SOURCE_FILE));
 	}
+
+	/**
+	 * @dataProvider dataTests
+	 */
+	public function testMerge($data)
+	{
+		$configCache = new Cache();
+		$configCache->load($data, Cache::SOURCE_FILE);
+
+		$configCache->set('system', 'test_2','with_data', Cache::SOURCE_DATA);
+		$configCache->set('config', 'test_override','with_another_data', Cache::SOURCE_DATA);
+
+		$newCache = new Cache();
+		$newCache->set('config', 'test_override','override it again', Cache::SOURCE_DATA);
+		$newCache->set('system', 'test_3','new value', Cache::SOURCE_DATA);
+
+		$mergedCache = $configCache->merge($newCache);
+
+		self::assertEquals('with_data', $mergedCache->get('system', 'test_2'));
+		self::assertEquals('override it again', $mergedCache->get('config', 'test_override'));
+		self::assertEquals('new value', $mergedCache->get('system', 'test_3'));
+	}
+
+	/**
+	 * @dataProvider dataTests
+	 */
+	public function testDiff($data)
+	{
+		$configCache = new Cache();
+		$configCache->load($data, Cache::SOURCE_FILE);
+
+		$configCache->set('system', 'test_2','with_data', Cache::SOURCE_DATA);
+		$configCache->set('config', 'test_override','with_another_data', Cache::SOURCE_DATA);
+
+		$newCache = new Cache();
+		$newCache->set('config', 'test_override','override it again', Cache::SOURCE_DATA);
+		$newCache->set('system', 'test_3','new value', Cache::SOURCE_DATA);
+
+		$mergedCache = $configCache->diff($newCache);
+
+		print_r($mergedCache);
+
+		self::assertEquals('with_data', $mergedCache->get('system', 'test_2'));
+		// existing entry was dropped
+		self::assertNull($mergedCache->get('config', 'test_override'));
+		// the newCache entry wasn't set, because we Diff
+		self::assertNull($mergedCache->get('system', 'test_3'));
+	}
 }
diff --git a/tests/src/Core/Config/Cache/ConfigFileLoaderTest.php b/tests/src/Core/Config/Cache/ConfigFileLoaderTest.php
index e8443611f2..aed55f429e 100644
--- a/tests/src/Core/Config/Cache/ConfigFileLoaderTest.php
+++ b/tests/src/Core/Config/Cache/ConfigFileLoaderTest.php
@@ -21,8 +21,8 @@
 
 namespace Friendica\Test\src\Core\Config\Cache;
 
-use Friendica\Core\Config\Cache;
 use Friendica\Core\Config\Factory\Config;
+use Friendica\Core\Config\ValueObject\Cache;
 use Friendica\Test\MockedTest;
 use Friendica\Test\Util\VFSTrait;
 use Friendica\Core\Config\Util\ConfigFileManager;
@@ -51,7 +51,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -77,7 +77,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 	}
@@ -106,7 +106,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -143,7 +143,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -179,7 +179,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -270,7 +270,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -304,7 +304,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -338,7 +338,7 @@ class ConfigFileLoaderTest extends MockedTest
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR,
 			$this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR
 		);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -354,7 +354,7 @@ class ConfigFileLoaderTest extends MockedTest
 		$this->delConfigFile('local.config.php');
 
 		$configFileLoader = (new Config())->createConfigFileLoader($this->root->url(), ['FRIENDICA_CONFIG_DIR' => '/a/wrong/dir/']);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -380,7 +380,7 @@ class ConfigFileLoaderTest extends MockedTest
 				 ->setContent(file_get_contents($fileDir . 'B.config.php'));
 
 		$configFileLoader = (new Config())->createConfigFileLoader($this->root->url(), ['FRIENDICA_CONFIG_DIR' => $this->root->getChild('config2')->url()]);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
@@ -403,18 +403,18 @@ class ConfigFileLoaderTest extends MockedTest
 				 ->setContent(file_get_contents($fileDir . 'B.config.php'));
 
 		$configFileLoader = (new Config())->createConfigFileLoader($this->root->url(), ['FRIENDICA_CONFIG_DIR' => $this->root->getChild('config2')->url()]);
-		$configCache = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache = new Cache();
 
 		$configFileLoader->setupCache($configCache);
 
 		// overwrite some data and save it back to the config file
-		$configCache->set('system', 'test', 'it', \Friendica\Core\Config\ValueObject\Cache::SOURCE_DATA);
-		$configCache->set('config', 'test', 'it', \Friendica\Core\Config\ValueObject\Cache::SOURCE_DATA);
-		$configCache->set('system', 'test_2', 2, \Friendica\Core\Config\ValueObject\Cache::SOURCE_DATA);
+		$configCache->set('system', 'test', 'it', Cache::SOURCE_DATA);
+		$configCache->set('config', 'test', 'it', Cache::SOURCE_DATA);
+		$configCache->set('system', 'test_2', 2, Cache::SOURCE_DATA);
 		$configFileLoader->saveData($configCache);
 
 		// Reload the configCache with the new values
-		$configCache2 = new \Friendica\Core\Config\ValueObject\Cache();
+		$configCache2 = new Cache();
 		$configFileLoader->setupCache($configCache2);
 
 		self::assertEquals($configCache, $configCache2);
@@ -425,6 +425,6 @@ class ConfigFileLoaderTest extends MockedTest
 			],
 			'config' => [
 				'test' => 'it'
-			]], $configCache2->getDataBySource(\Friendica\Core\Config\ValueObject\Cache::SOURCE_DATA));
+			]], $configCache2->getDataBySource(Cache::SOURCE_DATA));
 	}
 }
diff --git a/tests/src/Core/Config/TransactionalConfigTest.php b/tests/src/Core/Config/TransactionalConfigTest.php
new file mode 100644
index 0000000000..b42fee97c0
--- /dev/null
+++ b/tests/src/Core/Config/TransactionalConfigTest.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Friendica\Test\src\Core\Config;
+use Friendica\Core\Config\Capability\ISetConfigValuesTransactional;
+use Friendica\Core\Config\Model\Config;
+use Friendica\Core\Config\Model\TransactionalConfig;
+use Friendica\Core\Config\Util\ConfigFileManager;
+use Friendica\Core\Config\ValueObject\Cache;
+use Friendica\Test\MockedTest;
+use Friendica\Test\Util\VFSTrait;
+
+class TransactionalConfigTest extends MockedTest
+{
+	use VFSTrait;
+
+	/** @var ConfigFileManager */
+	protected $configFileManager;
+
+	protected function setUp(): void
+	{
+		parent::setUp();
+
+		$this->setUpVfsDir();
+
+		$this->configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/');
+	}
+
+	public function dataTests(): array
+	{
+		return [
+			'default' => [
+				'data' => include dirname(__FILE__, 4) . '/datasets/B.node.config.php',
+			]
+		];
+	}
+
+	public function testInstance()
+	{
+		$config = new Config($this->configFileManager, new Cache());
+		$transactionalConfig = new TransactionalConfig($config);
+
+		self::assertInstanceOf(ISetConfigValuesTransactional::class, $transactionalConfig);
+		self::assertInstanceOf(TransactionalConfig::class, $transactionalConfig);
+	}
+
+	public function testTransactionalConfig()
+	{
+		$config = new Config($this->configFileManager, new Cache());
+		$config->set('config', 'key1', 'value1');
+		$config->set('system', 'key2', 'value2');
+		$config->set('system', 'keyDel', 'valueDel');
+		$config->set('delete', 'keyDel', 'catDel');
+
+		$transactionalConfig = new TransactionalConfig($config);
+		self::assertEquals('value1', $transactionalConfig->get('config', 'key1'));
+		self::assertEquals('value2', $transactionalConfig->get('system', 'key2'));
+		self::assertEquals('valueDel', $transactionalConfig->get('system', 'keyDel'));
+		self::assertEquals('catDel', $transactionalConfig->get('delete', 'keyDel'));
+		// the config file knows it as well immediately
+		$tempData = include $this->root->url() . '/config/' . ConfigFileManager::CONFIG_DATA_FILE;
+		self::assertEquals('value1', $tempData['config']['key1'] ?? null);
+		self::assertEquals('value2', $tempData['system']['key2'] ?? null);
+
+		// new key-value
+		$transactionalConfig->set('transaction', 'key3', 'value3');
+		// overwrite key-value
+		$transactionalConfig->set('config', 'key1', 'changedValue1');
+		// delete key-value
+		$transactionalConfig->delete('system', 'keyDel');
+		// delete last key of category - so the category is gone
+		$transactionalConfig->delete('delete', 'keyDel');
+
+		// The main config still doesn't know about the change
+		self::assertNull($config->get('transaction', 'key3'));
+		self::assertEquals('value1', $config->get('config', 'key1'));
+		self::assertEquals('valueDel', $config->get('system', 'keyDel'));
+		self::assertEquals('catDel', $config->get('delete', 'keyDel'));
+		// but the transaction config of course knows it
+		self::assertEquals('value3', $transactionalConfig->get('transaction', 'key3'));
+		self::assertEquals('changedValue1', $transactionalConfig->get('config', 'key1'));
+		self::assertNull($transactionalConfig->get('system', 'keyDel'));
+		self::assertNull($transactionalConfig->get('delete', 'keyDel'));
+		// The config file still doesn't know it either
+		$tempData = include $this->root->url() . '/config/' . ConfigFileManager::CONFIG_DATA_FILE;
+		self::assertEquals('value1', $tempData['config']['key1'] ?? null);
+		self::assertEquals('value2', $tempData['system']['key2'] ?? null);
+		self::assertEquals('catDel', $tempData['delete']['keyDel'] ?? null);
+		self::assertNull($tempData['transaction']['key3'] ?? null);
+
+		// save it back!
+		$transactionalConfig->save();
+
+		// Now every config and file knows the change
+		self::assertEquals('changedValue1', $config->get('config', 'key1'));
+		self::assertEquals('value3', $config->get('transaction', 'key3'));
+		self::assertNull($config->get('system', 'keyDel'));
+		self::assertNull($config->get('delete', 'keyDel'));
+		self::assertEquals('value3', $transactionalConfig->get('transaction', 'key3'));
+		self::assertEquals('changedValue1', $transactionalConfig->get('config', 'key1'));
+		self::assertNull($transactionalConfig->get('system', 'keyDel'));
+		$tempData = include $this->root->url() . '/config/' . ConfigFileManager::CONFIG_DATA_FILE;
+		self::assertEquals('changedValue1', $tempData['config']['key1'] ?? null);
+		self::assertEquals('value2', $tempData['system']['key2'] ?? null);
+		self::assertEquals('value3', $tempData['transaction']['key3'] ?? null);
+		self::assertNull($tempData['system']['keyDel'] ?? null);
+		self::assertNull($tempData['delete']['keyDel'] ?? null);
+		// the whole category should be gone
+		self::assertNull($tempData['delete'] ?? null);
+	}
+}
diff --git a/update.php b/update.php
index ad33dde012..7ad6e432fe 100644
--- a/update.php
+++ b/update.php
@@ -1184,11 +1184,13 @@ function update_1508()
 {
 	$config = DBA::selectToArray('config');
 
+	$newConfig = DI::config()->transactional();
+
 	foreach ($config as $entry) {
-		DI::config()->set($entry['cat'], $entry['k'], $entry['v'], false);
+		$newConfig->set($entry['cat'], $entry['k'], $entry['v']);
 	}
 
-	DI::config()->save();
+	$newConfig->save();
 
 	DBA::e("DELETE FROM `config`");
 }