Cascading
Cascade operations automatically propagate persist and remove operations from a parent entity to its related children, reducing boilerplate and keeping related entities synchronized.
What are Cascade Operations?
Without cascade, you must explicitly persist every entity you want saved. With cascade configured on a relationship, ObjectQuel handles children automatically when you act on the parent:
class OrderEntity {
/**
* @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
* @Orm\Cascade(operations={"persist", "remove"})
*/
public EntityCollection $items;
}
// Without cascade: every entity must be persisted individually
$order = new OrderEntity();
$item1 = new OrderItemEntity();
$item2 = new OrderItemEntity();
$entityManager->persist($order);
$entityManager->persist($item1); // required without cascade
$entityManager->persist($item2); // required without cascade
$entityManager->flush();
// With cascade: persisting the order is enough
$order = new OrderEntity();
$order->items->add(new OrderItemEntity());
$order->items->add(new OrderItemEntity());
$entityManager->persist($order); // items are persisted automatically
$entityManager->flush();
Cascade Persist
When cascade persist is set, any child entity added to the collection is automatically persisted along with the parent. This is useful for composition relationships where children are created as part of building the parent.
class OrderEntity {
/**
* @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
* @Orm\Cascade(operations={"persist"})
*/
public EntityCollection $items;
public function __construct() {
$this->items = new EntityCollection();
}
}
$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);
$entityManager->persist($order);
$entityManager->flush(); // both items are saved automatically
Cascade Remove
When cascade remove is set, deleting the parent automatically deletes all children in the collection. Without this, you would need to remove each child manually before removing the parent, or rely on database-level foreign key constraints.
class OrderEntity {
/**
* @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
* @Orm\Cascade(operations={"remove"})
*/
public EntityCollection $items;
}
$order = $entityManager->find(OrderEntity::class, 123);
$entityManager->remove($order);
$entityManager->flush(); // all associated OrderItems are deleted too
Combining Cascade Operations
You can specify both persist and remove together. This is the most common configuration for composition relationships, where the parent fully owns the children's lifecycle:
class OrderEntity {
/**
* @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="orderId")
* @Orm\Cascade(operations={"persist", "remove"})
*/
public EntityCollection $items;
}
// persist cascades on save...
$order = new OrderEntity();
$order->items->add(new OrderItemEntity());
$entityManager->persist($order);
$entityManager->flush();
// ...and remove cascades on delete
$entityManager->remove($order);
$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 which children are persisted or removed
Cascade with ManyToOne
Cascade can also be applied to ManyToOne relationships, though this is uncommon. It makes the child entity responsible for persisting the parent — useful when you are building child-first and the parent may not yet exist:
class OrderItemEntity {
/**
* @Orm\ManyToOne(targetEntity="OrderEntity")
* @Orm\Cascade(operations={"persist"})
*/
private OrderEntity $order;
}
// The order is created inline and persisted automatically via the item
$item = new OrderItemEntity();
$item->setOrder(new OrderEntity());
$entityManager->persist($item); // order is persisted automatically
$entityManager->flush();
Avoid combining this with cascade on the parent side, as it creates circular cascade chains (see Bidirectional Relationships below).
Important Considerations
Cascade is Application-Level, Not Database-Level
ObjectQuel cascade runs in PHP, not as a database constraint. This has practical consequences:
- ObjectQuel loads related entities into memory before deleting them
- Lifecycle events (
@PreDelete,@PostDelete) fire for each cascaded delete - You have full visibility over what is being deleted
- Database foreign key constraints are not required, but can be added independently
Performance Impact
Because cascade remove operates at the application level, it generates a query per child entity. Deleting a parent with a large collection can be slow:
// Removing an order with 100 items triggers:
// 1. SELECT to load the order
// 2. SELECT to load all 100 items
// 3. 100 individual DELETE queries (one per item)
// 4. DELETE for the order itself
$order = $entityManager->find(OrderEntity::class, 123);
$entityManager->remove($order);
$entityManager->flush();
For large collections, consider batch delete queries or delegating removal to a database-level ON DELETE CASCADE foreign key constraint instead.
Bidirectional Relationships
Never configure cascade remove on both sides of a bidirectional relationship. If the parent cascades remove to children, and a child cascades remove back to the parent, ObjectQuel will enter a recursive loop:
// DANGEROUS — circular cascade
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"}) // triggers cascade back to OrderEntity
*/
private OrderEntity $order;
}
As a rule, cascade in one direction only — from parent to children.
Examples
Blog Post with Comments
class PostEntity {
/**
* @Orm\OneToMany(targetEntity="CommentEntity", mappedBy="postId")
* @Orm\Cascade(operations={"remove"})
*/
public EntityCollection $comments;
}
// Deleting the post deletes all its 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;
}
// Items are persisted and removed together with the cart
$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 where the parent owns the children's lifecycle
- Avoid cascade for association relationships between independent entities
- Treat cascade remove as a destructive operation — test it carefully before using in production
- Document cascade behavior in entity classes so it is visible without reading annotations
- Cascade in one direction only — parent to children, not both ways
- For large collections, benchmark cascade remove and consider database-level alternatives