Shopware 6 Hidden Gems #16: CloneBehavior — entity duplication with overrides, one call
Fabian Blechschmidt
You know the admin’s „duplicate“ button on products. Now the ticket says: „We need that, but from code — duplicate a product per sales channel, with adjusted name and its own product number.“ Or: „Campaign landing pages: clone this CMS page 20 times with different headlines.“
I’ve watched this get implemented as: load entity with all associations, walk the struct, null out the IDs, fix the foreign keys, re-insert, discover you forgot the media relations, repeat. A hundred lines of brittle traversal code.
The whole thing is one method call. It’s just that neither the method nor its options are documented.
The gem
Every EntityRepository has a clone() method (Framework/DataAbstractionLayer/EntityRepository.php:175):
use Shopware\Core\Framework\DataAbstractionLayer\Write\CloneBehavior;
$this->productRepository->clone(
$originalId,
$context,
$newId, // optional — random UUID if null
new CloneBehavior(
overwrites: [
'name' => 'Summer Edition',
'productNumber' => 'SW-1001-SUMMER',
'active' => false,
],
cloneChildren: true
)
);
That’s the entire „duplicate with adjustments“ feature. What happens inside (VersionManager.php:232-286) is exactly the traversal you didn’t want to write, done right:
The entity is read with its clone-relevant associations — children, translations, prices; the cascade the definition declares.
IDs are stripped or remapped so the copy is a real, independent entity.
createdAt is reset to now, updatedAt cleared — the clone doesn’t inherit a fake history.
Your overwrites are applied via array_replace_recursive — deep merge, so you can override nested data, not just top-level fields.
The insert runs in SYSTEM_SCOPE, so even WriteProtected fields (part 14 of this series!) clone correctly.
The two options are the whole story
overwrites is what elevates this from „copy“ to „templating“. Deep merge means things like:
work as you’d hope. Clone-as-template: keep one master entity, stamp out configured copies.
cloneChildren (default true) decides whether cascade children come along. For a product that means variants and prices; for a CMS page its sections and blocks. false gives you a flat copy of the root — useful when the children are exactly what you want to replace afterwards.
It’s an API endpoint too
The admin’s duplicate button doesn’t use secret magic — it uses a generic clone route that exists for every entity (Framework/Api/Controller/ApiController.php:76-95):
Requires the entity’s create ACL privilege, returns the new ID. Which means external systems — PIM, campaign tooling, your operations scripts — can duplicate entities with overrides without any custom endpoint on the shop side. I checked the docs for this route and found nothing; it’s been sitting there the whole time.
Fine print
Three things to know before you build on it:
Overwrites use property names (productNumber), like normal repository writes — not storage names. The merge happens on the serialized entity data before insert.
Unique constraints are your job. Clone a product without overwriting productNumber and MySQL will remind you. The admin UI forces you to enter a new number for the same reason.
What gets cloned is definition-driven — associations marked CascadeDelete are treated as children. If your custom entity’s children don’t clone, check your definition’s flags, not the clone call.
TL;DR
$repository->clone($id, $context, $newId, new CloneBehavior($overwrites, $cloneChildren)) duplicates any entity with its child associations, resets timestamps, deep-merges your overrides, and handles write-protected fields — and POST /api/_action/clone/{entity}/{id} exposes the same thing over the Admin API for every entity, gated by the create ACL. Delete your hand-rolled duplication code; the core’s version has seen more edge cases than yours.
Next up: system:dump and system:restore — the built-in database snapshot pair with a smart ignore list.
Cloned something exotic with it? Drop me an email!
Found in Shopware 6.7.0.0: Framework/DataAbstractionLayer/Write/CloneBehavior.php, EntityRepository::clone() at EntityRepository.php:175-190, clone pipeline in VersionManager.php:232-286 (deep-merge at line 272, SYSTEM_SCOPE insert at 276), API route in Framework/Api/Controller/ApiController.php:76-95.