Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: Migration strategy from document generation v1 to v2
date: 2026-08-05
area: after-sales
tags: [core, documents]
---

# Migration strategy from document generation v1 to v2

::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2026-08-05-document-generation-v1-to-v2-migration-strategy.md)
:::

## Context

[2026-03-17-refactor-of-document-generation.md](https://github.com/shopware/shopware/blob/trunk/adr/2026-03-17-refactor-of-document-generation.md)
decided to rewrite the document generation system, this document outlines the concrete migration strategy.
Both implementations will coexist throughout the 6.7 and 6.8 release lines. The new implementation persists into the same
`document` table and both render the same Twig document templates. The feature flag `DOCUMENT_GENERATION_REWORK` opts
into version 2.

## Decision Roadmap

| Phase | Version | Action |
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Phase 1 | 6.7 | Opt-in: v2 is available behind the feature flag (default off). Version 1 is fully deprecated for removal in 6.9. Compatibility gaps are closed and the backfill process is prepared. |
| Phase 2 | 6.8 | Opt-out: v2 becomes the default (flag flips to `default: true`). The `@experimental` annotations are removed, the marked surface becomes the stable public API. |
| Phase 3 | 6.9 | Version 1 and the feature flag are completely removed. Backfills are executed. |
| Phase 4 | post-6.9 | Destructive schema drops are executed to finalize the database cleanup. |

### Core Principles

- When the feature flag is off, the system runs purely on v1. The v1 code paths take no dependency on v2.
- Turning the flag on means all Shopware-driven surfaces use v2, including the admin UI, Flow Builder actions, mail attachments, bulk edits, and customer-facing download routes.
- The v1 APIs stay functional in both flag states and remain a public contract until their removal in 6.9.
- Data compatibility strictly flows in one direction, meaning v2 can read v1 data.

### Compatibility model

| | v1 document | v2 document |
| ----------- | ------------------ | ----------- |
| v1 Surfaces | Native | Best effort |
| v2 Surfaces | Read-time fallback | Native |

#### Read-time fallback

A `document` missing `document_file` rows exposes virtual formats derived from v1 fields. The `documentMediaFileId` maps
to the format matching the file extension, while the `documentA11yMediaFileId` maps to HTML. Extensions outside the format
registry stay as download-only files, exactly like legacy Zugferd types.

#### Best effort compatibility

A `document` row has two media slots, `documentMediaFileId` and `documentA11yMediaFileId`, and v1 selects between them
purely by file extension, there is no hard PDF assumption. The v2 persister fills both from its generated formats: the
primary slot takes the PDF if present, otherwise the single generated format. The a11y slot takes the HTML. So a v2
document stays usable in v1 surfaces after the flag is switched off whenever at least one format lands in a slot.

#### No regeneration by v1

The read path for v1 auto-generates missing media for non-static documents. For documents that already carry `document_file`
rows, it skips this and fails gracefully instead. Otherwise, switching back would produce a divergent v1 rendering from the
nearly empty v2 `document.config`. This guard is the single deliberate exception to v1 taking no dependency on v2 and will
be removed alongside v1.

#### Legacy Zugferd types

The `zugferd_invoice`, `zugferd_embedded_invoice`, and the four storno or credit-note variants are displayed exactly as
they are in the v2 UI. They appear under their legacy type name and are available for download only.

#### Idempotent backfill

A migration script creates the missing `document_file` rows for v1 documents and normalizes legacy Zugferd types into a
base type plus format. For example, a `zugferd_invoice` becomes a standard `invoice` with a `zugferd_xml` file. This is
prepared during 6.7 but only relied upon in 6.9, once no v1 writer exists anymore. At that point, a catch-up run covers any
documents generated by v1 in between. This process never touches the actual media files.

### Routing and Flags

The v2 admin API routes are available regardless of the flag state. The flag only switches the Shopware-driven surfaces.
Entry points for v2 are annotated `@experimental stableVersion:v6.8.0 feature:DOCUMENT_GENERATION_REWORK` and may change
until v2 becomes the default in 6.8.

For customer-facing downloads, the storefront and Store API routes gain a flag-gated branch. This branch resolves files
through the new document file structure first before falling back to legacy media fields. The URLs do not change and the
routes survive the removal of v1 in 6.9.

### Public Extension Surface

The subset of v2 classes that becomes public API is marked `@experimental stableVersion:v6.8.0
feature:DOCUMENT_GENERATION_REWORK` from the start. This includes abstract extension base classes, render pipeline structs,
configuration DTOs, file entity classes, domain exceptions, and events. Everything that stays internal carries plain
`@internal` and is never marked experimental. In 6.8, the `@experimental` annotations are removed and the marked classes
become the stable public API, while orchestration internals stay restricted.
The `DocumentV2` namespace is permanent, and reused v1 classes (`DocumentEntity`, `DocumentDefinition`, `DocumentCollection`,
`ReferenceInvoiceLoader`, the `DocumentBaseConfig` aggregates) move into it with the removal of v1 in 6.9.

### Deprecations and Entity Removal

Everything slated for removal in 6.9 is deprecated now, during 6.7, with `@deprecated tag:v6.9.0`. This covers the v1
domain, legacy parts of document and mail actions, admin components, and legacy Twig branches.

Document types and formats are now code-registered strings rather than database entities. The legacy foreign keys currently
live across three tables: `document`, `document_base_config`, and `document_base_config_sales_channel`.

#### During 6.7

A nullable `type_name` column is added to all three affected tables. Version 2 writes both the string and the legacy
foreign key, while v1 continues writing only the foreign key. The legacy entities `document_type` and
`document_type_translation` are deprecated with `reason:remove-entity` for removal in 6.9.

#### During 6.9

The `type_name` columns are backfilled from the foreign keys and the legacy `document_type_id` columns are made nullable.
The entity classes, dependency injection registrations, and foreign key fields are removed. Persisted merchant data is migrated
from IDs to technical names.

## Version 6.8 Major Checklist

1. Flip `DOCUMENT_GENERATION_REWORK` to `default: true`, the flag becomes the opt-out.
2. Remove the `@experimental` annotations, the marked classes become the stable public API.
3. Write the `UPGRADE-6.8.md` entries for the default switch and the stabilized extension surface.

## Version 6.9 Major Checklist

1. Remove `DOCUMENT_GENERATION_REWORK` and every gate (PHP, DI, Twig, admin JS, system config XML).
2. Delete the v1 domain, v1 admin components, v1 flow/mail branches, v1-only Twig branches, and the v1 entries in the PHPStan tagged-service contracts.
3. Move the surviving shared classes into the `DocumentV2` namespace.
4. Execute the prepared backfills: `document_file` rows (incl. Zugferd normalization), the `type_name` columns, rule/flow payloads; make the `document_type_id` columns nullable.
5. Drop the `document_base_config.config` JSON blob (destructive) and remove the `DocumentBaseConfigSyncSubscriber`.
6. Make the v2 branch of the storefront and Store API download routes unconditional.
7. Write the `UPGRADE-6.9.md` entries and the destructive migrations (`updateDestructive()`: `document_type` tables, the `document_type_id` columns, `document.document_media_file_id`, `document.document_a11y_media_file_id`).

## Consequences

Merchants have the freedom to switch between v1 and v2 freely during the 6.7 and 6.8 lifecycles. The only caveat is that v1
surfaces represent at most two formats per document and default to PDF, so a v2 document generated without a PDF is
reachable in v1 only by an explicit file-type request while the flag is off. Already generated documents remain fully
accessible after 6.9 without any need for regeneration, as the backfills simply add the required metadata.

### For extension developers

- Twig template overrides keep working: v2 renders the same `@Framework/documents/*.html.twig` files, so
presentation only extensions are largely unaffected.
- Code extension points are not carried over: custom document types or formats registered through the v1
`document.renderer` / `document_type.renderer` tags, v1 document events, and decorators of the v1 `DocumentGenerator`
are never invoked by the new pipeline. v2 discovers types, formats, and data exclusively through its own tagged
services (`shopware.document_v2.{type,provider,renderer}`) or the app manifest.

To stay compatible, such extensions must be ported to v2 (a data provider, a renderer, and a type, or an app manifest
entry). During the transition an extension may register its v2 and v1 variants side by side to work in either flag
state. Step-by-step guidance lives in `UPGRADE-6.7.md` and `UPGRADE-6.8.md`.

### For merchants and partners

- **6.7:** opt in to v2 and test it (in staging). Confirm your document related extensions still work under v2.
- **6.8:** v2 is the default. Before upgrading, verify every document related extension is v2 ready. If one is not, opt-out, upgrade safely, and migrate later.
- **6.9:** v1 and the flag are gone, opting out is no longer possible, so everything must be on v2.

The extra major cycle adds time flexibility without significantly changing the overall workload: because v2 becomes the default in 6.8, an extension that is
not v2 ready works only for merchants who explicitly opt out. Extension authors should therefore be v2 ready by 6.8, or
coordinate opt-out with their merchants, and fully migrated by 6.9.