Description
Bug Report
Q | A |
---|---|
BC Break | no |
Version | 2.8.2 |
Summary
Persisting changes to multiple embedded collections within the same embedded document fails for collections whose field names share a common prefix. The collection with the longer name that starts with the shorter name is ignored during persistence due to incorrect path filtering in CollectionPersister::excludeSubPaths
.
Current behavior
When an embedded document (e.g., VariantVariant
embedded within Product
) contains two or more EmbedMany
collections whose field names share a common prefix (e.g., supplier_factory_codes
and supplier_factory_codes_v2
), and both collections are modified (e.g., by replacing their instances) within the same UnitOfWork cycle, only the changes to the collection with the shorter field name prefix (e.g., supplier_factory_codes
) are persisted to MongoDB upon calling $documentManager->flush()
. Changes to the collection with the longer, prefixed name (e.g., supplier_factory_codes_v2
) are silently ignored.
This behavior was also observed with other collection pairs like options
and optionsList
within the same embedded document.
How to reproduce
-
Define Mappings:
- Create a top-level document mapping (e.g.,
Product
) that embeds a collection of documents (e.g.,variants
embeddingVariantVariant
).<mapped-superclass name="Product" ...> ... <embed-many field="variants" target-document="VariantVariant" collection-class="VariantVariants" /> ... </mapped-superclass>
- Define the embedded document mapping (
VariantVariant
) containing at least twoEmbedMany
collections where one field name is a prefix of the other.<embedded-document name="VariantVariant" > ... <embed-many field="supplier_factory_codes" field-name="supplierFactoryCodes" target-document="SupplierFactoryCode" collection-class="SupplierFactoryCodes" /> <embed-many field="supplier_factory_codes_v2" field-name="supplierFactoryCodesV2" target-document="SupplierFactoryCode" collection-class="SupplierFactoryCodes" /> ... </embedded-document>
- Create a top-level document mapping (e.g.,
-
Execute Code:
- Load a
Product
entity containing at least oneVariantVariant
. - Get a specific
VariantVariant
instance from thevariants
collection. - Replace the instance of both collections within that
VariantVariant
. Example:$variant = $product->getVariants()->first(); // Get an embedded variant $newCodes = new SupplierFactoryCodes(/* ... some data ... */); $newCodesV2 = new SupplierFactoryCodes(/* ... some different data ... */); // Replace both collection instances $variant->setSupplierFactoryCodes($newCodes); $variant->setSupplierFactoryCodesV2($newCodesV2); // Persist changes $documentManager->flush();
- Load a
-
Observe Result: Check the MongoDB document for the updated variant. Only the
supplierFactoryCodes
field will reflect the changes from$newCodes
. ThesupplierFactoryCodesV2
field will remain unchanged (or as it was before the flush). -
Identify Bug Location: The issue stems from
Doctrine\ODM\MongoDB\Persisters\CollectionPersister::excludeSubPaths
.- This method receives the paths to be persisted, e.g.,
['variants.0.supplier_factory_codes', 'variants.0.supplier_factory_codes_v2']
(simplified example). - It sorts the paths:
['variants.0.supplier_factory_codes', 'variants.0.supplier_factory_codes_v2']
. - It iterates and compares the current path with the last added path using
strpos($paths[$i], $lastUniquePath) === 0
. - In the example,
$lastUniquePath
is'variants.0.supplier_factory_codes'
. - When
$paths[$i]
is'variants.0.supplier_factory_codes_v2'
,strpos
returns0
because the string starts with the$lastUniquePath
. - The
continue;
statement is executed, incorrectly discarding the path'variants.0.supplier_factory_codes_v2'
as if it were a sub-path of the first, thus preventing its persistence.
- This method receives the paths to be persisted, e.g.,
Expected behavior
All modified embedded collections within the same embedded document should be persisted correctly during flush()
, regardless of whether their field names share common prefixes.
The CollectionPersister::excludeSubPaths
method should correctly differentiate between true sub-document paths (e.g., address.street
being a sub-path of address
) and sibling fields that happen to share a prefix (e.g., supplierFactoryCodes
and supplierFactoryCodesV2
).