foundry icon indicating copy to clipboard operation
foundry copied to clipboard

Cascade persist order

Open mpiot opened this issue 3 weeks ago • 3 comments

Hi, I just try to upgrade from v1.38 to v2.8, but we are facing an issue.

We have 2 entities: Parent and Child, the Child is cascade persisted from the Parent entity.

In the app, a user create a Parent entity with at least a Child, when we persist the Parent doctrine cascade persist the children.

In v1.38, the ParentFactory allow us access the Child entities in ->afterInstantiate() and ->afterPerist() factory event, but actually we can´t.

The problem is: the Parent entity has a non nullable reference property that is defined from ->getChildren(), but the factory do it:

    1. ParentFactory: ->afterInstantiate
    1. ParentFactory: ->afterPersist
    1. Child: ->afterInstantiate
    1. Child: ->afterPerist

So trying to persist the Parent with a null reference because we cannot process the reference without Children that is persisted after.

class Parent
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\Column]
    private ?string $reference = null;

    #[Assert\Count(min: 1)]
    #[Assert\Valid]
    #[ORM\OneToMany(targetEntity: Child::class, mappedBy: 'parent', cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['id' => 'ASC'])]
    private Collection $children;

    public function __construct()
    {
        $this->children = new ArrayCollection();
    }

    public function getReference(): ?string
    {
        return $this->reference;
    }

    public function setReference(string $reference): static
    {
        $this->reference = $reference;

        return $this;
    }

/**
     * @return ReadableCollection<int, Child>
     */
    public function getChildren(): ReadableCollection
    {
        return $this->children;
    }

    public function addChild(Child $child): static
    {
        if (!$this->children->contains($child)) {
            $this->children[] = $child;
            $child->setParent($this);
        }

        return $this;
    }

    public function removeChild(Child $child): static
    {
        if ($this->children->removeElement($child)) {
            // set the owning side to null (unless already changed)
            if ($child->getParent() === $this) {
                $child->setParent(null);
            }
        }

        return $this;
    }
}
class Child
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: Parent::class, inversedBy: 'children')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Parent $parent = null;

   // Many other scalar properties

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getParent(): ?Parent
    {
        return $this->parent;
    }

    public function setParent(?Parent $parent): static
    {
        $this->parent = $parent;

        return $this;
    }

    // Many getters/setters
}

Previously we can do it in the factory:

final class ParentFactory extends PersistentProxyObjectFactory
{
    protected function defaults(): array
    {
        // In v2.8, ->many() is replaced by ->range()
        // Important: withoutPersisting to avoid the Factory trying persist children, Doctrine cascade it
        return [
            'children' => ChildFactory::new()->withoutPersisting()->many(1, 2),
        ];
    }

    protected function initialize(): static
    {
        // see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
        return $this
            ->afterInstantiate(function (Parent $parent, array $attributes): void {
               // Here the $parent should have children inside, but no more available
               // We call a Service to calculate the reference like in Controllers
            })
        ;
    }

    public static function class(): string
    {
        return Parent::class;
    }
}

mpiot avatar Dec 01 '25 14:12 mpiot

Hello

Could you create a reproducer repository that illustrate the problem, please?

nikophil avatar Dec 03 '25 05:12 nikophil

Hi, I've done a tiny reproducer: https://github.com/mpiot/zenstruck-foundry-issue-1038

I call the Factory in the Homepage and dump & die on it.

mpiot avatar Dec 03 '25 07:12 mpiot

Hi @mpiot

thanks for the reproducer (and sorry for the late reply 😅)

I get the problem now, you're right, the afterInstantiate() callback should see the objects. I'll work on a fix soon

nikophil avatar Dec 08 '25 18:12 nikophil