Apparently, there are great limitations to automate plugins' updates. Shopware 6 provides two automated ways to update plugins: "lifecycle methods" AND "migrations". Unfortunately, those have not been enough. πππ It might be something I'm doing incorrectly or missing.
Let's say that I want to install an existing plugin from a third party named MyCustomPlugin
. This plugin is currently on version 2.9.3
. However, the plugin started on 1.0.0
and lots of updates happened since then. For example:
1.0.0 => 1.0.1
1.0.1 => 1.0.2
1.0.2 => 1.0.3
....
....
2.9.1 => 2.9.2
2.9.2 => 2.9.3
Of course, in many of these updates, automated updates were necessary to create directories, perform calculations, call external API's, use existing services from other plugins, create tables, add columns.. etc. Important: not only database queries, but filesystem operations and API calls were necessary.
π¨π¨ LIMITATIONS of "Migrations": as the official docs states, migrations was exclusively created to manage the database. The huge limitation is that it does not provide support to dependency injection. The migration system just passes the "Connection" to update
and updateDestructive
methods and everything you can do is simply perform SQL operations. You can not even use repository services. Too limited. See the following links:
https://developer.shopware.com/docs/guides/plugins/plugins/plugin-fundamentals/database-migrations
https://github.com/shopware/platform/blob/f153cd6973b28d764e0b8b576798aafdbb6d8cc7/src/Core/Framework/Migration/MigrationRuntime.php#L40
π¨π¨ LIMITATIONS of "Plugin lifecycle methods": it provides a much better support to plugin updates. You can use services, you have access to $this->container->get('YourServiceHere')
, but here comes the main problem: when you run plugin:refresh
, Shopware detects a new plugin (even tho it is not installed), and Shopware instantly updates the table plugin.version
to its current version. In our case that would be 2.9.3
. That means, we just added our plugin to the custom/plugins/
directory and Shopware already says it's version 2.9.3
. That way, we can not manage properly the version system during installation (install
method). Remember we had multiple update scripts from version 1.0.0
to version 2.9.3
? Shopware is already pointing out the version as 2.9.3
, and that would completely break any logic to detect what update scripts will be necessary to run. How are we going to run the appropriate update scripts (even if we call them manually on install
method)? There is no way. Also there is a problem of having this plugin being installed several times and having the same operation conflicting against each other, like creating a table or column for instance. That's a problem that doesn't happen in "Migrations", as it tracks all update scripts that were run and don't run them again.
Bottom line: unfortunately, those are not enough ππ Please, let me know if I'm missing something.
ππ I can not use "Migrations" because it does not provide the necessary services to perform a complex update.
ππ I can not use "Plugin lifecycle methods" because during Γ¬nstall
method, it does not provide the correct from/to versions and thus I'm not able to run the appropriate update scripts.
The question is: how to proper automate plugins updates in Shopware 6?
Migrations were deliberately designed to not allow further injection of services and should only serve the purpose of making changes to the database schema that should be interchangeable between all installations of a plugin given a specific version. If you gave developers access to all kinds of services, e.g. the repositories, they may feel inclined to use migrations to persist things such as demo data and the likes, which is not what migrations were designed for (and not just in a Shopware specific sense).
So, in short, if I install your plugin in a specific version and run the migrations, the result of the migrations should be exactly the same (minus randomly generated ids). Anything that could lead to varying results is discouraged.
They're the place where you can and should use public services, e.g. if you wanted to generate dynamic demo data on installation. As you mentioned, unlike migrations, you'll have to handle version specific execution by yourself.
That's why the UpdateContext
class has the method getUpdatePluginVersion
, for the version you're updating to, and the method getCurrentPluginVersion
, for the version you're currently on. Using that information you can essentially build your own migration handling by checking the current version of the plugin and running all your operations down to the earliest version you could update from.
On the initial installation you would obviously run all of these at once. To make sure you don't end up with duplicate data on repeated installations, you'd also have to make sure you delete all the data on uninstallation. I have done this on multiple occasions and if you keep the data well organized on when it is to be persisted and in what order, then this shouldn't be too much of a problem at all.
public function update(UpdateContext $updateContext): void
{
/** @var EntityRepository $repository */
$repository = $this->container->get('some_entity.repository');
$currentVersion = $updateContext->getCurrentPluginVersion();
// persist fixtures added since the current version
if (version_compare($currentVersion, '2.0.0', '<')) {
$fixtures = file_get_contents($this->getPath() . '/Resources/fixtures/update.2.0.0.json');
$repository->upsert(json_decode($fixtures, true), $updateContext->getContext());
}
if (version_compare($currentVersion, '2.1.0', '<')) {
$fixtures = file_get_contents($this->getPath() . '/Resources/fixtures/update.2.1.0.json');
$repository->upsert(json_decode($fixtures, true), $updateContext->getContext());
}
// ...
}
public function install(InstallContext $installContext): void
{
$repository = $this->container->get('some_entity.repository');
// persist all fixtures up to the latest version
$fixtures = file_get_contents($this->getPath() . '/Resources/fixtures/update.2.0.0.json');
$repository->upsert(json_decode($fixtures, true), $installContext->getContext());
$fixtures = file_get_contents($this->getPath() . '/Resources/fixtures/update.2.1.0.json');
$repository->upsert(json_decode($fixtures, true), $installContext->getContext());
// ...
}
public function uninstall(UninstallContext $uninstallContext): void
{
$repository = $this->container->get('some_entity.repository');
// delete all the fixtures up to the latest version
$ids = file_get_contents($this->getPath() . '/Resources/fixtures/ids.json');
$repository->delete(json_decode($ids, true), $uninstallContext->getContext());
}
Adding to dneustadt's answer, this can also be used to pre-configure plugins (more in custom-made plugins, not so much for general purpose plugins for the Shopware store):
public function update(UpdateContext $updateContext): void
{
if (version_compare($updateContext->getCurrentPluginVersion(), '1.0.0', '<')) {
$configService = $this->container->get(SystemConfigService::class);
$configService->set(self::CONFIG_KEY, 'foobar_', self::CHANNEL_SOMETHING);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With