Published · Europe/Paris
The caret trap: why ^6.5 says yes to Shopware 6.7 — and your plugin still breaks
Picture the audit: you're planning a 6.6→6.7 update window for a client shop. You open
composer.json on the plugin that's given you trouble before, and there it is —
"shopware/core": "^6.5". Green light. The constraint admits the target version, so the
plugin should be fine. You move on to the next shop.
Except a caret constraint answering "yes" is not the same question as "will this actually
work." That gap is the caret trap, and it catches agencies that read composer.json the
way most of us were taught to — as a compatibility statement, when it's really a permission
statement.
What ^6.5 actually promises
Composer's caret operator follows one rule: bump the first non-zero version segment, drop
everything after it, and that's your ceiling. For ^6.5, the first non-zero segment is the
major, 6, so the ceiling becomes 7.0.0.0 — everything from 6.5.0.0 up to (but not
including) 7.0.0.0 is admitted. That means 6.7.0.0 passes. So does 6.6.10.3. The
plugin's own constraint was never written with 6.7 in mind — it just happens to sit inside
a very wide door.
Run the numbers and the trap is obvious once you see it laid out:
^6.5against6.6.10.3→ admitted.^6.5against6.7.0.0→ admitted. This is the one that surprises people: the plugin author wrote^6.5when 6.5 was current, 6.7 didn't exist yet, and the constraint quietly inherited a future it was never tested against.^6.5against7.0.0.0→ correctly rejected. The ceiling does its job at the major boundary — it just doesn't stop you one or two minors early, which is exactly the range most 6.6→6.7-class update windows live in.
Compare that to a tilde constraint on the same plugin. ~6.5 behaves like a caret at the
minor level — but give it one more version segment and it tightens hard: ~6.6.10.0 admits
only 6.6.10.x, nothing past the next patch. Two plugin authors who both meant "pin this
down," using ^ instead of ~, end up with meaningfully different real-world exposure once
a major-adjacent update ships. Most composer.json files in the wild were written once and
never revisited — nobody goes back to tighten a caret before every update window.
Why "constraint admits it" isn't "verified compatible"
This is the honesty line Upgrade Radar draws on every single report it prints, not just the ones that
find a conflict: a constraint check tells you what the plugin author declared, not what the
plugin does. composer update --dry-run against the real dependency tree remains the
definitive gate at update time — no static reading of a lock file replaces actually resolving
the graph. Upgrade Radar exists to be the radar you sweep before that point, across every client shop,
without running composer or touching a shop at all.
The same honesty discipline cuts the other way, too. When a plugin has no shopware/*
constraint at all — no opinion declared, one way or the other — Upgrade Radar marks it UNKNOWN. It
never gets silently folded into "probably fine." A tool that only ever reports COMPATIBLE or
CONFLICT is a tool that's guessing on the gaps; UNKNOWN is the honest third answer, and it's
one agencies can act on (go check that plugin by hand) instead of one that quietly disappears
into a green checklist.
The caret trap at fleet scale
One plugin, one shop, you can eyeball the constraint yourself in thirty seconds. The trap
gets expensive at the scale DACH agencies actually operate at: ten, twenty, fifty client
shops on a maintenance retainer, each with its own plugin set, each with its own mix of
carets, tildes, wildcards, and hyphen ranges that were never written with the next update in
mind. Reading every composer.json by hand before a 6.6→6.7-class window is exactly the
unbillable, error-prone hour-sink agencies already describe in their own update-strategy
write-ups — and it's the reason the underlying gap is still an open feature request against
Shopware itself: nobody has shipped a predictive check, only reactive tools that tell you
what already broke.
Upgrade Radar reads composer.lock and a shop's custom/plugins/custom/static-plugins directories,
evaluates every declared shopware/* constraint against your target version with the same
composer semantics described above — caret, tilde, wildcard, ranges, OR, hyphen, stability
flags, all covered by a table test so a divergence from composer's own rules would fail the
build, not ship quietly — and gives you a verdict per plugin: COMPATIBLE, CONFLICT (with the
excluding constraint quoted), or UNKNOWN. No composer run. No shop access. It runs against a
checkout or a copied composer.lock, offline, in seconds.
Try it on your own lock file
The fastest way to see whether any of your plugins are sitting in a caret trap for your next
update window is to check. The hosted version needs nothing installed — paste a
composer.lock, get the report back, plugin by plugin, with every excluding constraint
quoted so you can see exactly why something landed where it did.