Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How properly update plugins in Shopware 6?

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?

like image 201
Matheus Gontijo Avatar asked Sep 11 '25 03:09

Matheus Gontijo


2 Answers

About Migrations

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.

About lifecycle events

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());
}
like image 69
dneustadt Avatar answered Sep 13 '25 22:09

dneustadt


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);
        }
    }
like image 21
Alex Avatar answered Sep 13 '25 21:09

Alex