Doctrine Cascade Operations

Cascading operations mean that when you perform a persist, remove, merge (deleted), detach, refresh (does not work) operation on an entity, the same operation will also be performed on the associated entity.

Persist

The persist operation saves the new entity to the database. The persist operation only needs to be performed when a new entity needs to be created, if the entity was obtained from the repository, then persist is not necessary, since the changes are already tracked by the EntityManager, it is enough to just flush.
When specifying cascade: ['persist'], persist will also be executed on the related entity.

When cascade: ['persist'] is NOT specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product')]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

Wrong code, it won't work:

$product = new Product();
$photo = new Photo();
$product->setPhoto($photo);

$em->persist($product);
$em->flush();

When executing this code, an error will occur:

A new entity was found through the relationship 'App\Entity\Product#photo' that was not configured to 
cascade persist operations for entity: App\Entity\Photo@266. 
To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or 
configure cascade persist this association in the mapping for example 
@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the 
problem implement 'App\Entity\Photo#__toString()' to get a clue.

This is correct code, but you need to additionally perform a persist operation on the Photo entity:

$product = new Product();
$photo = new Photo();
$product->setPhoto($photo);

$em->persist($photo); // if cascade: ['persist'] is specified, then this is an optional operation
$em->persist($product);
$em->flush();

When cascade: ['persist'] is specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product', cascade: ['persist'])]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

Having performed one persist operation on the Product entity, this operation was also performed on the Photo entity.

$product = new Product();
$photo = new Photo();
$product->setPhoto($photo);

$em->persist($product);
$em->flush();

Remove

The remove operation removes an entity from the database.
When cascade: ['remove'] is specified, remove will also be executed on the associated entity.

When cascade: ['remove'] is NOT specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product')]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

Wrong code, it won't work:

$em->remove($product);
$em->flush();

When executing this code, an error will occur:

An exception occurred while executing a query: SQLSTATE[23503]: Foreign key violation: 
7 ERROR:  update or delete on table "product" violates foreign key constraint "fk_14b784184584665a" 
on table "photo" DETAIL:  Key (id)=(1) is still referenced from table "photo".

This is correct code, but you need to additionally perform a remove operation on the Photo entity:

$em->remove($product->getPhoto()); // if cascade: ['remove'] is specified, then this is an optional operation
$em->remove($product);
$em->flush();

When cascade: ['remove'] is specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product', cascade: ['remove'])]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

Having performed one remove operation on the Product entity, this operation was also performed on the Photo entity.

$em->remove($product);
$em->flush();

Merge (removed)

Operation merge adds an entity under the control of the EntityManager.
The merge operation has been removed from Doctrine.

Detach

The detach operation detaches the entity from the control of the EntityManager so that the changes are not saved by the flush operation.
When cascade: ['merge'] is specified, merge will also be executed on the associated entity.

When cascade: ['detach'] is NOT specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product')]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

After executing this code, only the Product entity will be detached from the EntityManager:

$em->detach($product);

When cascade: ['detach'] is specified

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\OneToOne(mappedBy: 'product', cascade: ['detach'])]
    private ?Photo $photo = null;
}

#[ORM\Entity(repositoryClass: PhotoRepository::class)]
class Photo
{
    #[ORM\OneToOne(inversedBy: 'photo')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Product $product = null;
}

This code will change the name of the Photo entity to bc43283c-309a-4031-8b8f-35c18c18e9a3, but the change will not affect the database:

$manager->detach($product);
$product->getPhoto()->setName('bc43283c-309a-4031-8b8f-35c18c18e9a3');
        
$em->flush();

The Product and Photo entities will be detached from the EntityManager and will not affect the database.

Refresh (not working)

The refresh operation loads the actual entity data from the database.
When specifying cascade: ['refresh'], refresh will also be executed on the related entity.

In the current stable release of Doctrine 2.13 cascade: ['refresh'] does not work
https://github.com/doctrine/orm/pull/6798.

When cascade: ['refresh'] is NOT specified

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
    #[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
    private $products;
}

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\ManyToOne(inversedBy: 'products')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Category $category = null;
}

This code will only load the actual data for the Category entity, not for the Product entity:

$em->refresh($category);

When cascade: ['refresh'] is specified

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
    #[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class, cascade: ['refresh'])]
    private $products;
}

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\ManyToOne(inversedBy: 'products')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Category $category = null;
}

This code will load the actual data for both the Category entity and the Product entity.

$em->refresh($category);