If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Open 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!
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!
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.
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.
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
"stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}