Cascading

Cascade operations automatically propagate persist and delete operations from parent entities to their related children, reducing boilerplate code and ensuring related entities stay synchronized.

What are Cascade Operations?

Cascade operations allow you to persist or delete related entities automatically when you persist or delete a parent entity:

class OrderEntity {
    /**
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
     * @Orm\Cascade(operations={"persist", "remove"})
     */
    public EntityCollection $items;
}

// Without cascade, you'd need to persist each item individually
$order = new OrderEntity();
$item1 = new OrderItemEntity();
$item2 = new OrderItemEntity();

$entityManager->persist($order);
$entityManager->persist($item1);  // Without cascade
$entityManager->persist($item2);  // Without cascade
$entityManager->flush();

// With cascade, persisting the order persists all items
$order = new OrderEntity();
$order->items->add($item1);
$order->items->add($item2);

$entityManager->persist($order);  // Items are automatically persisted
$entityManager->flush();

Cascade Persist

Automatically persist related entities when the parent is persisted:

class OrderEntity {
    /**
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
     * @Orm\Cascade(operations={"persist"})
     */
    public EntityCollection $items;

    public function __construct() {
        $this->items = new EntityCollection();
    }
}

// Usage
$order = new OrderEntity();
$order->setCustomerId(123);

$item1 = new OrderItemEntity();
$item1->setProductId(1);
$item1->setQuantity(2);

$item2 = new OrderItemEntity();
$item2->setProductId(2);
$item2->setQuantity(1);

$order->items->add($item1);
$order->items->add($item2);

// Only need to persist the order
$entityManager->persist($order);
$entityManager->flush();  // Both items are saved automatically

Cascade Remove

Automatically delete related entities when the parent is deleted:

class OrderEntity {
    /**
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
     * @Orm\Cascade(operations={"remove"})
     */
    public EntityCollection $items;
}

// Usage
$order = $entityManager->find(OrderEntity::class, 123);

// Deleting the order automatically deletes all items
$entityManager->remove($order);
$entityManager->flush();  // All order items are deleted too

Combining Cascade Operations

You can cascade both persist and remove operations:

class OrderEntity {
    /**
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
     * @Orm\Cascade(operations={"persist", "remove"})
     */
    public EntityCollection $items;
}

// Both operations cascade
$order = new OrderEntity();
$order->items->add(new OrderItemEntity());

$entityManager->persist($order);  // Item is persisted
$entityManager->flush();

$entityManager->remove($order);   // Item is deleted
$entityManager->flush();

When to Use Cascade

Use cascade persist when:

  • Child entities only exist as part of the parent (composition)
  • Creating the parent always creates children (e.g., Order → OrderItems)
  • Children have no meaning without the parent

Use cascade remove when:

  • Deleting the parent should delete all children
  • Children cannot exist without the parent
  • You want to maintain referential integrity at the application level

Don't use cascade when:

  • Related entities are independent (e.g., Product → Category)
  • Related entities are shared across multiple parents
  • You need fine-grained control over persistence

Cascade with ManyToOne

Cascade can also be used on ManyToOne relationships:

class OrderItemEntity {
    /**
     * @Orm\ManyToOne(targetEntity="OrderEntity")
     * @Orm\Cascade(operations={"persist"})
     */
    private OrderEntity $order;
}

// Creating an item automatically creates the order
$item = new OrderItemEntity();
$item->setOrder(new OrderEntity());

$entityManager->persist($item);  // Order is automatically persisted
$entityManager->flush();

This is less common but useful when the child entity controls the parent's lifecycle.

Important Considerations

Cascade is Application-Level

ObjectQuel cascade operations happen in your application code, not at the database level. This means:

  • ObjectQuel loads related entities and explicitly deletes them
  • Lifecycle events (@PreDelete, @PostDelete) fire for cascaded deletes
  • You have full control and visibility over what's being deleted
  • Database foreign key constraints are not required (but can be added)

Performance Impact

Cascade operations can trigger multiple database queries:

// Deleting an order with 100 items triggers:
// 1. SELECT to load the order
// 2. SELECT to load all items
// 3. DELETE for each item (100 queries)
// 4. DELETE for the order

$order = $entityManager->find(OrderEntity::class, 123);
$entityManager->remove($order);
$entityManager->flush();

For large collections, consider batch operations or database-level cascading.

Bidirectional Relationships

Be careful with bidirectional cascade operations to avoid infinite loops:

// DANGEROUS - Cascading both directions
class OrderEntity {
    /**
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
     * @Orm\Cascade(operations={"persist", "remove"})
     */
    public EntityCollection $items;
}

class OrderItemEntity {
    /**
     * @Orm\ManyToOne(targetEntity="OrderEntity")
     * @Orm\Cascade(operations={"remove"})  // Can create circular cascade
     */
    private OrderEntity $order;
}

Typically, only cascade from parent to children, not both directions.

Examples

Blog Post with Comments

class PostEntity {
    /**
     * @Orm\OneToMany(targetEntity="CommentEntity", mappedBy="postId")
     * @Orm\Cascade(operations={"remove"})
     */
    public EntityCollection $comments;
}

// Deleting a post deletes all comments
$post = $entityManager->find(PostEntity::class, 1);
$entityManager->remove($post);
$entityManager->flush();

Shopping Cart with Items

class CartEntity {
    /**
     * @Orm\OneToMany(targetEntity="CartItemEntity", mappedBy="cartId")
     * @Orm\Cascade(operations={"persist", "remove"})
     */
    public EntityCollection $items;
}

// Adding items and saving cart persists everything
$cart = new CartEntity();
$cart->items->add(new CartItemEntity());
$cart->items->add(new CartItemEntity());

$entityManager->persist($cart);
$entityManager->flush();

Best Practices

  • Use cascade for composition relationships (parent owns children)
  • Avoid cascade for association relationships (independent entities)
  • Be cautious with cascade remove - it permanently deletes data
  • Document cascade behavior clearly in entity classes
  • Test cascade operations thoroughly, especially removes
  • Consider performance impact for large collections
  • Use only on the owning side or inverse side, not both