Skip to content

fix: keep the configuration a save replaces - #905

Merged
blaipr merged 1 commit into
mainfrom
fix/the-config-backup-download-has-a-backup-to-offer
Sep 3, 2026
Merged

fix: keep the configuration a save replaces#905
blaipr merged 1 commit into
mainfrom
fix/the-config-backup-download-has-a-backup-to-offer

Conversation

@blaipr

@blaipr blaipr commented Sep 3, 2026

Copy link
Copy Markdown
Member

ConfigBackupService::backup() has existed since this rewrite was imported and is called from
nowhere in src/. So config_backup was never written, and the "Download config backup" link the
Information page renders
— beside a date, unconditionally — answered "Unable to retrieve the
configuration"
every time, on every installation.

The tell was already in the test suite: every integration test around that download seeds the
parameter by hand with #[InjectConfigParam(['config_backup' => …])]. That is the shape of a
feature nothing produces.

It also matters more than it did last week: config.xml is now replaced atomically rather than
truncated in place, and that PR noted there was nothing to fall back on if it were lost. This is the
fallback.

Where the call goes, and why not where it belongs

On paper this belongs in ConfigFile::save() — the one place the file is written. It cannot go
there. ConfigBackup needs ConfigService, which extends Service and needs Application, which
needs ConfigFileService:

ConfigFile → ConfigBackup → Config → Application → ConfigFile

Only a lazy proxy breaks that, on the object the container builds while booting and every request
depends on — the exact place this repo has had a total outage from a DI mistake before. Not worth it
for this.

ConfigTrait::saveConfig() is the shared path all nine configuration screens go through, it runs
per-request long after boot, and a controller taking ConfigBackupService is already proven —
DownloadConfigBackupController does it. So the backup is taken there, and the nine SaveControllers
pass the service in.

Making it a required parameter of saveConfig() rather than an optional one is deliberate: an
optional collaborator that a caller forgets is silently no backup, which is the failure this PR is
fixing. The signature change means every call site had to be updated to compile.

It stores the previous configuration, not the new one

ConfigFileService::getConfigData() answers a clone, and save() has not run when the backup is
taken — so what reaches backup() is still the stored configuration. Ordering is the only thing
that makes that true, so it is asserted directly rather than assumed.

backup() logs and swallows its own failures, so a database that cannot take it does not stop an
administrator saving configuration.

Verified

  • The real container resolves ConfigBackupService, and all three constructor parameters of a
    patched controller are required — so php-di wires them rather than silently leaving a default
    null, which is this codebase's recorded trap.
  • Three tests: the backup is taken; it is taken before the save; and a demo instance, which
    refuses the save, stores nothing. Mutation-verified — removing the call fails the first two and
    leaves the demo one passing, which is right, since it asserts an absence.

ConfigBackupService::backup() has existed since this rewrite was imported and is called
from nowhere in src/, so config_backup was never written and the 'Download config backup'
link the Information page renders — beside a date, unconditionally — answered 'Unable to
retrieve the configuration' every time, on every installation. Every integration test
around that download seeds the parameter by hand with #[InjectConfigParam], which is the
shape of a feature nothing produces.

On paper the call belongs in ConfigFile::save(). It cannot go there: ConfigBackup needs
ConfigService, which needs Application, which needs ConfigFileService — a cycle only a
lazy proxy breaks, on the object the container builds while booting. ConfigTrait::saveConfig()
is the shared path all nine configuration screens go through, runs per-request long after
boot, and a controller taking ConfigBackupService is already proven by
DownloadConfigBackupController.

Required rather than optional, because an optional collaborator a caller forgets is
silently no backup — the failure being fixed.

getConfigData() answers a clone and save() has not run when the backup is taken, so what
is stored is the previous configuration. backup() logs and swallows its own failures, so a
database that cannot take it does not stop an administrator saving.
@blaipr
blaipr merged commit 074059a into main Sep 3, 2026
8 checks passed
@blaipr
blaipr deleted the fix/the-config-backup-download-has-a-backup-to-offer branch September 3, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant