04.09.2026・TechStuff
04.09.2026・TechStuff

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:

  1. The entity is read with its clone-relevant associations — children, translations, prices; the cascade the definition declares.
  2. IDs are stripped or remapped so the copy is a real, independent entity.
  3. createdAt is reset to now, updatedAt cleared — the clone doesn’t inherit a fake history.
  4. Your overwrites are applied via array_replace_recursive — deep merge, so you can override nested data, not just top-level fields.
  5. 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:

new CloneBehavior([
    'name' => 'Clone',
    'translations' => [
        $deLanguageId => ['name' => 'Kopie'],
    ],
])

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):

POST /api/_action/clone/product/018d2e6f...
{
  "overwrites": { "productNumber": "SW-1001-B", "active": false },
  "cloneChildren": true
}

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:

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.