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
Context
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
- Flip
DOCUMENT_GENERATION_REWORKtodefault: true, the flag becomes the opt-out. - Remove the
@experimentalannotations, the marked classes become the stable public API. - Write the
UPGRADE-6.8.mdentries for the default switch and the stabilized extension surface.
Version 6.9 Major Checklist
- Remove
DOCUMENT_GENERATION_REWORKand every gate (PHP, DI, Twig, admin JS, system config XML). - 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.
- Move the surviving shared classes into the
DocumentV2namespace. - Execute the prepared backfills:
document_filerows (incl. Zugferd normalization), thetype_namecolumns, rule/flow payloads; make thedocument_type_idcolumns nullable. - Drop the
document_base_config.configJSON blob (destructive) and remove theDocumentBaseConfigSyncSubscriber. - Make the v2 branch of the storefront and Store API download routes unconditional.
- Write the
UPGRADE-6.9.mdentries and the destructive migrations (updateDestructive():document_typetables, thedocument_type_idcolumns,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.twigfiles, 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.renderertags, v1 document events, and decorators of the v1DocumentGeneratorare 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.