Cascade Persist
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCheck out this error: it's a doozy!
An entity was found through the relationship
Starship.droids
that was not configured to "cascade persist" operations for entityStarshipDroid
.
Let me translate that for you:
Hey, you're saving this
Starship
and it's got aStarshipDroid
attached to it. That's great, but you forgot to tell me to persist theStarshipDroid
. What do you want me to do?
But again, from inside Starship
, we can't get the entity manager to say $manager->persist($starshipDroid)
.
Harnessing the Power of cascade=['persist']
The solution is to use something called cascade persist.
Scroll up to the $starshipDroids
property, and find the OneToMany
. Add a new option: cascade
. I'll type it in manually. Set it to an array with persist
inside:
// ... lines 1 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 53 | |
#[ORM\OneToMany(targetEntity: StarshipDroid::class, mappedBy: 'starship', cascade: ['persist'])] | |
private Collection $starshipDroids; | |
// ... lines 56 - 260 | |
} |
We're setting up a sort of domino effect. If anyone persists this starship
, we're going to cascade that persist down to any attached relationships.
A word of caution though: use this power wisely. It makes your code more automatic, which is great, but it can also make it harder to spot bugs.
But in this case, it's exactly the fix we need.
Give those fixtures another whirl:
symfony console doctrine:fixtures:load
Back to Adding Droids
We're back in business. We can use ship->addDroid()
once more. But I still want to create a fleet of starships
with droids
attached to them.
Remove all the manual code and bring back the droids
property on the StarshipFactory
:
// ... lines 1 - 13 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 18 - 51 | |
DroidFactory::createMany(100); | |
StarshipFactory::createMany(100, fn() => [ | |
'droids' => DroidFactory::randomRange(1, 5), | |
]); | |
StarshipPartFactory::createMany(100); | |
} | |
} |
Fire up the fixtures again:
symfony console doctrine:fixtures:load
Guess what? They work!
Behind the scenes, Foundry is calling addDroid()
on each Starship
for each droid
. And we just proved that addDroid()
is back in action.
The creation of the StarshipDroid
join entity is now hidden from our entire codebase!
Finer Control with assignedAt
But, what if you want to add a droid
to a starship
and control the assignedAt
property? Add an argument for addDroid()
in Starship
: a DateTimeImmutable
. Make it optional to keep things flexible. Then, after creating the StarshipDroid
, set the $assignedAt
if we passed it in:
// ... lines 1 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 207 | |
public function addDroid(Droid $droid, \DateTimeImmutable $assignedAt = null): static | |
{ | |
if (!$this->getDroids()->contains($droid)) { | |
// ... lines 211 - 213 | |
if ($assignedAt) { | |
$starshipDroid->setAssignedAt($assignedAt); | |
} | |
// ... line 217 | |
} | |
// ... lines 219 - 220 | |
} | |
// ... lines 222 - 263 | |
} |
Cool... but there's a slight issue. Foundry won't let us control the assignedAt
field. So, if you want to assign some droids
at a specific time, you'll need to take the wheel manually.
Displaying assignedAt
Finally, let's make that assignedAt
visible on our site. We'll need the StarshipDroid
join entity object to do that. It's a bit more work, but totally doable.
Change the loop to for starshipDroid in ship.starshipDroids
. Then starshipDroid.droid.name
and starshipDroid.assignedAt
with the ago
filter for flair:
// ... lines 1 - 4 | |
{% block body %} | |
// ... lines 6 - 19 | |
<div class="md:flex justify-center space-x-3 mt-5 px-4 lg:px-8"> | |
// ... lines 21 - 25 | |
<div class="space-y-5"> | |
<div class="mt-8 max-w-xl mx-auto"> | |
<div class="px-8 pt-8"> | |
// ... lines 29 - 61 | |
<p class="text-[22px] font-semibold"> | |
{% for starshipDroid in ship.starshipDroids %} | |
{{ starshipDroid.droid.name }} (assigned {{ starshipDroid.assignedAt|ago }} {% if not loop.last %}, {% endif %} | |
// ... lines 65 - 66 | |
{% endfor %} | |
</p> | |
// ... lines 69 - 84 | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Refresh and... we can now see when each droid
was assigned.
That's it, friends! We've explored the deepest corners of Doctrine relationships, even the elusive many-to-many with extra fields. As always, if you have questions, drop them in the comments below. We're all in this together!
Thank you Ryan! It's wonderful to be able to accompany you in this tutorial. It's always nice and I always learn something new or at least refresh some knowledge. You are a great motivation and inspiration for me - and have been for all these great years.