doctrine/orm · error · InvalidArgumentException

Dirty entity can not be scheduled for insertion.

Error message

Dirty entity can not be scheduled for insertion.

What it means

EntityManager::persist() delegates to UnitOfWork::scheduleForInsert(), which refuses entities already scheduled for update: entityUpdates is populated by computeChangeSets() while a flush is in progress, so an entity that is simultaneously 'has pending changes' and 'to be inserted' would produce conflicting SQL plans. Doctrine throws as soon as that combination is detected.

Source

Thrown at src/UnitOfWork.php:1396

            }
        }

        return $sort->sort();
    }

    /**
     * Schedules an entity for insertion into the database.
     * If the entity already has an identifier, it will be added to the identity map.
     *
     * @throws ORMInvalidArgumentException
     * @throws InvalidArgumentException
     */
    public function scheduleForInsert(object $entity): void
    {
        $oid = spl_object_id($entity);

        if (isset($this->entityUpdates[$oid])) {
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
        }

        if (isset($this->entityDeletions[$oid])) {
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
        }

        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
        }

        if (isset($this->entityInsertions[$oid])) {
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
        }

        $this->entityInsertions[$oid] = $entity;

        if (isset($this->entityIdentifiers[$oid])) {
            $this->addToIdentityMap($entity);

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Move the persist() call outside the flush cycle: do it before flush(), or collect the new entities and persist/flush them in a separate, later flush.
  2. Inside onFlush, only persist genuinely new (STATE_NEW) entities; for already-managed entities just mutate them and call $uow->recomputeSingleEntityChangeSet() instead of persist().
  3. Drop redundant persist() calls - an already-managed dirty entity is picked up by the change tracker automatically.
  4. Never call flush() recursively from lifecycle listeners; queue work and flush after the current flush completes.

Example fix

// before (inside an onFlush listener)
public function onFlush(OnFlushEventArgs $args): void
{
    foreach ($uow->getScheduledEntityUpdates() as $entity) {
        $this->stamp($entity);
        $args->getObjectManager()->persist($entity); // InvalidArgumentException: Dirty entity can not be scheduled for insertion.
    }
}

// after
public function onFlush(OnFlushEventArgs $args): void
{
    $em  = $args->getObjectManager();
    $uow = $em->getUnitOfWork();
    foreach ($uow->getScheduledEntityUpdates() as $entity) {
        $this->stamp($entity);
        $uow->recomputeSingleEntityChangeSet($em->getClassMetadata($entity::class), $entity);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Inside flush-time listeners, only persist entities that are safe to insert
$uow  = $em->getUnitOfWork();
$state = $uow->getEntityState($entity);

if ($state === UnitOfWork::STATE_NEW && ! $uow->isEntityScheduled($entity)) {
    $em->persist($entity);
    $uow->computeChangeSet($em->getClassMetadata($entity::class), $entity);
}
// already-managed entities: just mutate; the flush will pick changes up

Prevention

When it happens

Trigger: Calling $em->persist($entity) from code that runs after change sets were computed during a flush - most commonly inside an onFlush listener or a nested flush attempt during preUpdate/postPersist - where the entity is already managed with computed changes (entityUpdates[spl_object_id($entity)] is set). Also reachable by calling $uow->scheduleForInsert() manually.

Common situations: onFlush listeners that persist the very entities they observe (audit loggers, event subscribers); re-entrant flush() inside lifecycle callbacks; generic 'ensure persisted' helper methods invoked during flush.

Related errors


AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21). Data as JSON: /api/errors/99b5fc65a41b7348. Report an issue: GitHub.