`cache:disable <type>` promotes every `cache_types` value from `config.php` into `env.php`, and `cache:enable` does not undo it
Preconditions and environment
- Magento
2.4-developat commit12d212031006ec455fee51c1653a6ac230aeca54, installed with a plainsetup:install, no third-party modules - PHP 8.4.25, MariaDB 11.4
- 15 cache types are declared by the enabled modules on this installation, and
setup:installwrites all 15 intoapp/etc/env.php app/etc/config.phpis the shared, version-controlled deployment configuration file (the oneapp:config:dumpwrites to and the one that is meant to be committed),app/etc/env.phpis the per-environment file that is not.Magento\Framework\App\DeploymentConfig\Reader::load()merges the two witharray_replace_recursive()in the orderconfig.php, thenenv.php, so acache_typesmap placed inconfig.phpis read and honoured normally:cache:statusreports it correctly andMagento\Framework\App\Cache\State::isEnabled()returns the right value for every type.
Steps to reproduce
- Install Magento and confirm
app/etc/env.phpcontains acache_typessection andapp/etc/config.phpdoes not. - Move the whole
cache_typesmap fromapp/etc/env.phpintoapp/etc/config.php, so that the map exists only in the shared file:
php -r '
$env = include "app/etc/env.php";
$cfg = include "app/etc/config.php";
$ct = $env["cache_types"];
unset($env["cache_types"]);
$cfg["cache_types"] = $ct;
file_put_contents("app/etc/env.php", "<?php\nreturn " . var_export($env, true) . ";\n");
file_put_contents("app/etc/config.php", "<?php\nreturn " . var_export($cfg, true) . ";\n");
'- Run
bin/magento cache:statusand confirm all 15 types are reported with their values fromconfig.php. - Run
bin/magento cache:disable full_page. - Inspect the
cache_typessection of both files. - Run
bin/magento cache:enable full_pageand inspect both files again.
Expected result
Step 4 changes the state of one cache type. app/etc/env.php either stays without a cache_types section and the change is refused with an explanation, or it gains exactly the one entry that changed:
'cache_types' => [
'full_page' => 0,
],The remaining 14 types keep being resolved from config.php, so a later edit to config.php still takes effect, and step 6 returns the installation to the state it was in before step 4.
Actual result
Step 4 writes the whole merged 15-entry map into app/etc/env.php:
CMD: bin/magento cache:disable full_page
EXIT: 0
Changed cache status:
full_page: 1 -> 0DIFF env.php cache_types (before -> after):
REMOVED <NO_CACHE_TYPES_SECTION>
ADDED block_html: 1
ADDED collections: 1
ADDED compiled_config: 1
ADDED config: 1
ADDED config_integration: 1
ADDED config_integration_api: 1
ADDED config_webservice: 1
ADDED customer_notification: 1
ADDED db_ddl: 1
ADDED eav: 1
ADDED full_page: 0
ADDED graphql_query_resolver_result: 1
ADDED layout: 1
ADDED reflection: 1
ADDED translate: 1
DIFF config.php cache_types (before -> after):
(no change)14 values that the operator deliberately put in the shared, committed file are now also in the per-environment file, which wins the merge. From this point on, changing any of those 14 values in config.php has no effect on this installation, silently, with no output saying so. A probe reading both layers separately shows the shadowing directly:
PROBE_MERGED_COUNT=15
PROBE_CONFIG_PHP=count=15
PROBE_ENV_PHP=count=15
PROBE_TYPE full_page declared=Y merged=0 isEnabled=0 cfgphp=1 envphp=0Step 6 does not undo it. cache:enable full_page flips the one value back and leaves all 15 entries in env.php:
CMD: bin/magento cache:enable full_page
EXIT: 0
Changed cache status:
full_page: 0 -> 1
Cleaned cache types:
full_pageDIFF env.php cache_types (after disable -> after enable):
CHANGED full_page: 0 -> 1
DIFF config.php cache_types (after disable -> after enable):
(no change)The only way back to the previous state is to edit app/etc/env.php by hand and delete the section. cache:disable with no arguments followed by cache:enable with no arguments behaves the same way: all 15 entries stay in env.php afterwards.
Additional information
The mechanism is that the write is not scoped to what changed. Magento\Framework\App\Cache\Manager::setEnabled() collects the types it actually touched into $changedStatusTypes, and then calls a single $this->cacheState->persist() that ignores that list:
if ($isUpdated) {
$this->cacheState->persist();
}
return $changedStatusTypes;Magento\Framework\App\Cache\State::persist() (lib/internal/Magento/Framework/App/Cache/State.php:105) writes the entire in-memory status map, which is the merged view of both files, into env.php:
public function persist(): void
{
$this->load();
$this->writer->saveConfig([ConfigFilePool::APP_ENV => [self::CACHE_KEY => $this->statuses]]);
}So every value that arrived from config.php is written out to env.php as a side effect of changing an unrelated type.
A proposal that keeps the current CLI behaviour intact: give StateInterface a way to persist only the types whose status changed, and have Manager::setEnabled() pass $changedStatusTypes to it. Writer::saveConfig() already merges with array_replace_recursive() against the current contents of the target file, so persisting a one-key cache_types array produces the correct env.php for the classic case where the whole map already lives there, and leaves the other types resolving from config.php in the split case. State::persist() can stay as it is for backward compatibility with any caller that relies on it.
This matters beyond the split layout, because it is a one-way door: the promotion happens on any environment where somebody runs a single cache:disable, it is not visible in the command output, and nothing short of hand-editing env.php reverses it.
Release note
Fixed an issue where disabling or enabling a single cache type wrote the complete cache type map into app/etc/env.php, permanently overriding values configured in app/etc/config.php.
Triage and priority
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
Source: magento/magento2