feat(modules): the lifecycle of a module
Installing, switching on and off, updating and uninstalling — in one
service, which is the only place any of it happens. Files on disk remain
somebody else's business: Composer put them there, or an archive, or a
person over FTP. What happens here is the record the site keeps, the
migrations, the hooks of the module and the caches that would otherwise
still describe the previous set of modules.
Every operation answers with the steps it took rather than a boolean.
Installing is half a dozen things in a row and any of them can be the one
that failed; the console prints that list, and the admin panel will show
it.
What it refuses is as much the point as what it does: a module that is not
there, one already installed, an alias another module holds, a version this
site cannot run, a system module, one another installed module needs — and,
before anything is undone, a module whose migrations never said how to be
rolled back. Undoing half a module leaves a schema nobody can describe.
Uninstalling keeps the tables. --purge is what deletes the data, and it is
a separate word on purpose.
Installer gets install() and update($from, $to) back, and none of its
methods are abstract any more: a module overrides what it needs. Tables are
still created by migrations alone. ModuleInstaller is gone, and step 4 of
the site installer calls the installers of the modules itself.
Two things this turned up:
The registry and the state store cache what they read, and the factory
builds them from instances of its own — not the ones in the container. So
installing wrote the file while the migrations ran against a list of
sources the new module was not in yet: the first run reported "0 applied"
with a live migration on disk. Both grew a forget(), called after every
write.
Migrator is final readonly, so it can be neither replaced nor mocked. The
service now depends on MigrationRunnerInterface — run, rollback,
irreversible — which is what .agents/architecture.md asks for anyway and
the only way to test these operations without a database.
service, which is the only place any of it happens. Files on disk remain
somebody else's business: Composer put them there, or an archive, or a
person over FTP. What happens here is the record the site keeps, the
migrations, the hooks of the module and the caches that would otherwise
still describe the previous set of modules.
Every operation answers with the steps it took rather than a boolean.
Installing is half a dozen things in a row and any of them can be the one
that failed; the console prints that list, and the admin panel will show
it.
What it refuses is as much the point as what it does: a module that is not
there, one already installed, an alias another module holds, a version this
site cannot run, a system module, one another installed module needs — and,
before anything is undone, a module whose migrations never said how to be
rolled back. Undoing half a module leaves a schema nobody can describe.
Uninstalling keeps the tables. --purge is what deletes the data, and it is
a separate word on purpose.
Installer gets install() and update($from, $to) back, and none of its
methods are abstract any more: a module overrides what it needs. Tables are
still created by migrations alone. ModuleInstaller is gone, and step 4 of
the site installer calls the installers of the modules itself.
Two things this turned up:
The registry and the state store cache what they read, and the factory
builds them from instances of its own — not the ones in the container. So
installing wrote the file while the migrations ran against a list of
sources the new module was not in yet: the first run reported "0 applied"
with a live migration on disk. Both grew a forget(), called after every
write.
Migrator is final readonly, so it can be neither replaced nor mocked. The
service now depends on MigrationRunnerInterface — run, rollback,
irreversible — which is what .agents/architecture.md asks for anyway and
the only way to test these operations without a database.