Skip to content

CollectionPersister::excludeSubPaths incorrectly filters sibling EmbedMany collections with shared prefix names, preventing persistence #2754

Open
@ricardomoratomateos

Description

@ricardomoratomateos

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

  1. Define Mappings:

    • Create a top-level document mapping (e.g., Product) that embeds a collection of documents (e.g., variants embedding VariantVariant).
      <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 two EmbedMany 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>
  2. Execute Code:

    • Load a Product entity containing at least one VariantVariant.
    • Get a specific VariantVariant instance from the variants 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();
  3. Observe Result: Check the MongoDB document for the updated variant. Only the supplierFactoryCodes field will reflect the changes from $newCodes. The supplierFactoryCodesV2 field will remain unchanged (or as it was before the flush).

  4. 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 returns 0 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.

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

Images

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions