A Shopware 6 major version upgrade is not the same problem as migrating off Shopware 5. Shopware 5 is dead platform, full rebuild, no way around it. A major version upgrade inside Shopware 6 is supposed to be the easy path: composer update, run the migrations, done.
That’s true if the shop stayed current. It stops being true the moment a shop has drifted, and drift is exactly what I found when I took over a Shopware 6.3 project earlier this year. This is what that Shopware 6 upgrade actually involved: a manual migration off an outdated project template, a collision inside Shopware’s own migration history, and the script I wrote to get past it.
Why “just run the upgrade” stops working
I’ve already written about migrating off Shopware 5, which is a different kind of project: new platform, new templating, new everything. A major version upgrade within Shopware 6 is supposed to be smaller in scope, and on a shop that’s stayed current, it usually is. Bump the composer constraint, run the migrations, fix whatever the changelog flagged as breaking, done in an afternoon.
It stops being small the moment a shop has skipped enough tooling changes that the upgrade path itself isn’t clean anymore. That’s a structural problem, not a version-number problem, and it doesn’t show up until you actually start the upgrade. You find out the hard way, usually mid-deploy, on a shop that looked perfectly healthy right up until then.
The shop I took over
I picked up this project mid-state, with no documentation of what previous developers had run or why. That’s a common enough starting point for agency and freelance work: a shop changes hands, the handover notes are thin or nonexistent, and you inherit whatever decisions got made along the way. It’s exactly the kind of gap that lets a Shopware migration fail quietly, or half-run, without anyone noticing until a later upgrade forces the issue.
What I found on inspection here: the shop was on Shopware 6.3, and it was still running the old shopware/production project template, from before Shopware moved to Symfony Flex.
That detail matters more than a version number does. The production template and the Flex-based skeleton aren’t just cosmetically different. Bin scripts, bundle registration, and the overall project config are laid out differently between the two. A shop on the old template isn’t just “a bit behind.” It’s on a structure that newer major versions don’t expect anymore.
Migrating the production template to Flex, by hand
Flex landed in Shopware 6.4.17.0, released in October 2022, and became the default starting with 6.5. By the time I took this project on, a shop still on the pre-Flex production template had already been behind the standard path for years.
Symfony Flex changes more than a folder or two. It controls how composer.json declares recipes, how config/packages and config/bundles.php get assembled, and how bin/console and the rest of the bin scripts get wired up. On a shop that adopted Flex from the start, none of that is visible day to day. On a shop that never made the switch, every one of those pieces is laid out the old way, and a newer major version expects the new way.
There was no documented route for a shop this far off the beaten track. So the move happened by hand: moving bin scripts, rebuilding bundle configuration, restructuring the project to match what Flex expects. I want to be precise about what this was and wasn’t. It wasn’t a case of composer recipes:update failing or being broken. It was manual transition work, on a path Shopware’s own tooling doesn’t document for a shop that’s already this far behind current.
If you’ve read my post on how I approach Shopware 6 plugin development, the underlying lesson is the same one here at the project level: the structural decisions made early on a Shopware build compound every year they go unaddressed. A plugin that overrides instead of decorates breaks on the next update. A project that never moved to Flex breaks on the next major version.
When the core migrations collided
With the project restructured onto Flex, the next step was catching the shop up on Shopware’s own core migrations. This is normally the boring part. It wasn’t here.
Some of Shopware’s core migrations had already been partially executed before I took the project over. I don’t know why, and I’m not going to speculate publicly about a client’s prior development history. What I do know is what happened when I tried to bring the shop current: re-running those migrations collided with schema changes that were already sitting in the database.
Shopware’s migration runner marks a migration as executed only once it finishes successfully, and it re-throws on failure. There’s no built-in way to reconcile a migration that already changed the schema but never got marked complete in the migration table.
That leaves two bad options and no good one. Run it again, blind, and it fails against columns or tables that are already there. Skip it because the table says it’s pending, and you might be skipping a migration that’s genuinely never run, which just moves the same problem further down the upgrade path. Neither is safe on its own, and on a shop with an unclear history, you can’t tell which one you’re looking at without checking.
The script that checked schema state before touching it
The fix was to stop trusting the migration table and start checking the database directly. I wrote a script that, for every pending core migration, inspected the actual schema, tables, columns, indexes, whatever that specific migration was supposed to create or alter, before deciding whether to run it, skip it, or flag it for a manual look.
That one change turned a blocked upgrade into a working one. It’s also the piece of this story that generalizes past this one shop: any Shopware 6 major version upgrade on an inherited project, or any shop where you can’t fully vouch for the deployment history, needs the same discipline. Don’t run migrations against unknown schema state. Verify first, execute second.
The bigger point, and the one I’d make to Shopware directly, is that this shouldn’t have to be a custom script at all. A migration runner that checks schema state before applying a change, rather than trusting its own table, is safer by default on every shop, not just the ones with a messy history. Idempotent migrations are a solved problem in plenty of other frameworks. It’s a reasonable thing to ask for here too.
What this means if you’re planning a Shopware 6 major version upgrade
A few things I’d tell anyone about to do this on a shop they didn’t build from scratch.
- Audit migration history before you touch anything. Compare what the migration table says ran against what the schema actually has. A mismatch is the warning sign, not the upgrade failing halfway through.
- Don’t assume an inherited shop’s tooling is current. Check for the Flex migration specifically if the shop predates 6.5. It’s easy to miss because the shop still runs fine on the old structure right up until you try to move past it.
- Test the upgrade path against a schema-verified clone, not a fresh install. A clean Shopware install will never reproduce a collision that only exists because of your shop’s specific history.
- When something looks like a core bug rather than your own configuration, say so specifically. I’ve had the same experience with a Shopware date filter bug that turned out to live in Shopware’s own core: the details that separate “our setup is wrong” from “this is a core issue” are usually narrow, and they’re worth pinning down before you spend more time on it.
- Budget the structural work separately from the version bump. If a shop needs the Flex migration, a schema audit, or both, quote and schedule that as its own piece of work. Folding it into “the upgrade” as if it’s the same size as a clean shop’s upgrade is how timelines slip.
Getting Shopware’s core team involved
I wrote this migration story up publicly, the same one behind a LinkedIn post about being upfront with clients that ended up doing better than I expected. A Shopware core developer saw it and engaged directly, asking about the specifics of the upgrade. I offered to send over the exact migrations and the exact errors.
That’s the part worth repeating to anyone hitting something similar: a vague “the upgrade broke” doesn’t get anyone’s attention. Being able to hand over precise migrations and precise errors does.
If you’re staring down a Shopware 6 major version upgrade on a shop with an uncertain history, and you’d rather not find out the hard way where it collides, get in touch. This is exactly the kind of upgrade work I take on.
Frequently asked questions
What’s the difference between a Shopware 5 migration and a Shopware 6 major version upgrade? A Shopware 5 to 6 migration is a full platform rebuild. A Shopware 6 major version upgrade stays on the same platform, but can still carry real risk if the shop has drifted far behind or has an unclear migration history.
Is a Shopware 6 major version upgrade just composer update? Only if the shop stayed current on tooling and every migration has a known, verified history. On a shop that’s fallen behind, that’s the easy 10% of the work.
What is Symfony Flex and why does it matter here? Flex is the recipe-based skeleton Shopware moved to, replacing the older production template. It landed in 6.4.17.0 and became default in 6.5. A shop still on the old template needs that structure moved by hand before later major versions upgrade cleanly.
What happens if core migrations were partially executed before I took over a shop? Shopware only marks a migration executed after it fully succeeds. A partial run leaves the schema changed but the migration still flagged as pending, so a blind re-run collides with what’s already there. Check actual schema state before executing.
How do I know if my shop has migration history problems before I upgrade? Compare the migration table against the real schema. Any mismatch, especially on an inherited shop, means audit before you run anything else.
Should I get a Shopware core developer involved? If you can hand over something specific, yes. Exact migrations and exact errors get a useful response. A vague bug report doesn’t.