Using the new OneToMany Collections
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 SubscribeOpen up the genus/show.html.twig
template. Actually, let's start in the Genus
class itself. Find getGenusScientists()
:
// ... lines 1 - 14 | |
class Genus | |
{ | |
// ... lines 17 - 195 | |
/** | |
* @return ArrayCollection|User[] | |
*/ | |
public function getGenusScientists() | |
{ | |
return $this->genusScientists; | |
} | |
} |
This method is lying! It does not return an array of User
objects, it returns an array of GenusScientist
objects!
// ... lines 1 - 14 | |
class Genus | |
{ | |
// ... lines 17 - 195 | |
/** | |
* @return ArrayCollection|GenusScientist[] | |
*/ | |
public function getGenusScientists() | |
{ | |
return $this->genusScientists; | |
} | |
} |
In the template, when we loop over genus.genusScientists
, genusScientist
is not a User
anymore. Update to genusScientist.user.fullName
, and above, for the user_show
route, change this to genusScientist.user.id
:
// ... lines 1 - 4 | |
{% block body %} | |
// ... lines 6 - 7 | |
<div class="sea-creature-container"> | |
<div class="genus-photo"></div> | |
<div class="genus-details"> | |
<dl class="genus-details-list"> | |
// ... lines 12 - 21 | |
<dd> | |
<ul class="list-group"> | |
{% for genusScientist in genus.genusScientists %} | |
<li class="list-group-item js-scientist-item"> | |
<a href="{{ path('user_show', { | |
'id': genusScientist.user.id | |
}) }}"> | |
{{ genusScientist.user.fullName }} | |
// ... line 30 | |
</a> | |
// ... lines 32 - 41 | |
</li> | |
{% endfor %} | |
</ul> | |
</dd> | |
</dl> | |
</div> | |
</div> | |
<div id="js-notes-wrapper"></div> | |
{% endblock %} | |
// ... lines 51 - 92 |
Then, in the link, let's show off our new yearsStudied
field: {{ genusScientist.yearsStudied }}
then years:
// ... lines 1 - 4 | |
{% block body %} | |
// ... lines 6 - 7 | |
<div class="sea-creature-container"> | |
<div class="genus-photo"></div> | |
<div class="genus-details"> | |
<dl class="genus-details-list"> | |
// ... lines 12 - 21 | |
<dd> | |
<ul class="list-group"> | |
{% for genusScientist in genus.genusScientists %} | |
<li class="list-group-item js-scientist-item"> | |
<a href="{{ path('user_show', { | |
'id': genusScientist.user.id | |
}) }}"> | |
{{ genusScientist.user.fullName }} | |
({{ genusScientist.yearsStudied }} years) | |
</a> | |
// ... lines 32 - 41 | |
</li> | |
{% endfor %} | |
</ul> | |
</dd> | |
</dl> | |
</div> | |
</div> | |
<div id="js-notes-wrapper"></div> | |
{% endblock %} | |
// ... lines 51 - 92 |
We still need to fix the remove link, but let's see how it looks so far!
Refresh! It's way less broken! Well, until you click to view the user!
Updating the User Template
To fix this, start by opening User
and finding getStudiedGenuses()
. Change the PHPDoc to advertise that this now returns an array of GenusScientist
objects:
// ... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
// ... lines 19 - 214 | |
/** | |
* @return ArrayCollection|GenusScientist[] | |
*/ | |
public function getStudiedGenuses() | |
{ | |
return $this->studiedGenuses; | |
} | |
// ... lines 222 - 241 | |
} |
Next, go fix the template: user/show.html.twig
. Hmm, let's rename this variable to be a bit more clear: genusScientist
, to match the type of object it is. Now, update slug
to be genusScientist.genus.slug
. And print genusScientist.genus.name
:
// ... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
// ... lines 6 - 38 | |
<div class="col-xs-4"> | |
<h3>Genus Studied</h3> | |
<ul class="list-group"> | |
{% for genusScientist in user.studiedGenuses %} | |
<li class="list-group-item"> | |
<a href="{{ path('genus_show', { | |
'slug': genusScientist.genus.slug | |
}) }}"> | |
{{ genusScientist.genus.name }} | |
</a> | |
</li> | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Try it! Page is alive!
Updating the Delete Link
Back on the genus page, the other thing we need to fix is this remove link. In the show.html.twig
template for genus, update the userId
part of the URL: genusScientist.user.id
:
// ... lines 1 - 4 | |
{% block body %} | |
// ... lines 6 - 7 | |
<div class="sea-creature-container"> | |
<div class="genus-photo"></div> | |
<div class="genus-details"> | |
<dl class="genus-details-list"> | |
// ... lines 12 - 21 | |
<dd> | |
<ul class="list-group"> | |
{% for genusScientist in genus.genusScientists %} | |
<li class="list-group-item js-scientist-item"> | |
// ... lines 26 - 32 | |
<a href="#" | |
class="btn btn-link btn-xs pull-right js-remove-scientist-user" | |
data-url="{{ path('genus_scientists_remove', { | |
genusId: genus.id, | |
userId: genusScientist.user.id | |
}) }}" | |
> | |
<span class="fa fa-close"></span> | |
</a> | |
</li> | |
{% endfor %} | |
</ul> | |
</dd> | |
</dl> | |
</div> | |
</div> | |
<div id="js-notes-wrapper"></div> | |
{% endblock %} | |
// ... lines 51 - 92 |
Next, find this endpoint in GenusController
: removeGenusScientistAction()
:
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 126 | |
public function removeGenusScientistAction($genusId, $userId) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
/** @var Genus $genus */ | |
$genus = $em->getRepository('AppBundle:Genus') | |
->find($genusId); | |
if (!$genus) { | |
throw $this->createNotFoundException('genus not found'); | |
} | |
$genusScientist = $em->getRepository('AppBundle:User') | |
->find($userId); | |
if (!$genusScientist) { | |
throw $this->createNotFoundException('scientist not found'); | |
} | |
$genus->removeGenusScientist($genusScientist); | |
$em->persist($genus); | |
$em->flush(); | |
return new Response(null, 204); | |
} | |
} |
It's about to get way nicer. Kill the queries for Genus
and User
. Replace them with $genusScientist = $em->getRepository('AppBundle:GenusScientist')
and findOneBy()
, passing it user
set to $userId
and genus
set to $genusId
:
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 126 | |
public function removeGenusScientistAction($genusId, $userId) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$genusScientist = $em->getRepository('AppBundle:GenusScientist') | |
->findOneBy([ | |
'user' => $userId, | |
'genus' => $genusId | |
]); | |
// ... lines 136 - 140 | |
} | |
} |
Then, instead of removing this link from Genus
, we simply delete the entity: $em->remove($genusScientist)
:
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 126 | |
public function removeGenusScientistAction($genusId, $userId) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$genusScientist = $em->getRepository('AppBundle:GenusScientist') | |
->findOneBy([ | |
'user' => $userId, | |
'genus' => $genusId | |
]); | |
$em->remove($genusScientist); | |
$em->flush(); | |
return new Response(null, 204); | |
} | |
} |
And celebrate!
Go try it! Quick, delete that scientist! It disappears in dramatic fashion, and, when we refresh, it's definitely gone.
Phew! We're almost done. By the way, you can see that this refactoring takes some work. If you know that your join table will probably need extra fields on it, you can save yourself this work by setting up the join entity from the very beginning and avoiding ManyToMany
. But, if you definitely won't have extra fields, ManyToMany
is way nicer.
Updating the Fixtures
The last thing to fix is the fixtures. We won't set the genusScientists
property up here anymore. Instead, scroll down and add a new AppBundle\Entity\GenusScientist
section:
// ... lines 1 - 38 | |
AppBundle\Entity\GenusScientist: | |
// ... lines 40 - 44 |
It's simple: we'll just build new GenusScientist
objects ourselves, just like we did via newAction()
in PHP code earlier. Add genus.scientist_{1..50}
to create 50 links. Then, assign user
to a random @user.aquanaut_*
and genus
to a random @genus_*
. And hey, set yearsStudied
to something random too: <numberBetween(1, 30)>
:
// ... lines 1 - 38 | |
AppBundle\Entity\GenusScientist: | |
genus.scientist_{1..50}: | |
user: '@user.aquanaut_*' | |
genus: '@genus_*' | |
yearsStudied: <numberBetween(1, 30)> |
Nice! Go find your terminal and reload!
./bin/console doctrine:fixtures:load
Ok, go back to /genus
... and click one of them. We have scientists!
So our app is fixed, right? Well, not so fast. Go to /admin/genus
: you might need to log back in - password iliketurtles
. Our genus form is still totally broken. Ok, no error: but it doesn't even make sense anymore: our relationship is now more complex than checkboxes can handle. For example, how would I set the yearsStudied
?
Time to take this form up a level.
Everything in this lesson works great for me right up until the end (time 5:27 in your video) where I click the edit link on the admin/genus page. Things blow up with "Cannot access private property AppBundle\Entity\GenusScientist::$id". I can't figure out why this is happening. I do have a "public function getId()" in my GenusScientist entity. I even copied your final version of this entity from the course files and the same thing happens, so this has to be coming from somewhere else.
Any hints?