If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
We have a list page! Heck, we have a show page. Let's link them together.
First, the poor show route is nameless. Give it a name - and a new reason to live -
with name="genus_show"
:
... lines 1 - 11 | |
class GenusController extends Controller | |
{ | |
... lines 14 - 45 | |
/** | |
* @Route("/genus/{genusName}", name="genus_show") | |
*/ | |
public function showAction($genusName) | |
... lines 50 - 89 | |
} |
That sounds good.
In the list template, add the a
tag and use the path()
function to point this
to the genus_show
route. Remember - this route has a {genusName}
wildcard, so
we must pass a value for that here. Add a set of curly-braces to make an array...
But this is getting a little long: so break onto multiple lines. Much better. Finish
with genusName: genus.name
. And make sure the text is still genus.name
:
... lines 1 - 2 | |
{% block body %} | |
<table class="table table-striped"> | |
... lines 5 - 11 | |
<tbody> | |
{% for genus in genuses %} | |
<tr> | |
<td> | |
<a href="{{ path('genus_show', {'genusName': genus.name}) }}"> | |
{{ genus.name }} | |
</a> | |
</td> | |
... lines 20 - 21 | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
{% endblock %} |
Cool! Refresh. Oooh, pretty links. Click the first one. The name is "Octopus66", but the fun fact and other stuff is still hardcoded. It's time to grow up and finally make this dynamic!
In the controller, get rid of $funFact
. We need to query for a Genus that matches
the $genusName
. First, fetch the entity manager with $em = $this->getDoctrine()->getManager()
:
... lines 1 - 11 | |
class GenusController extends Controller | |
{ | |
... lines 14 - 48 | |
public function showAction($genusName) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
... lines 52 - 75 | |
} | |
... lines 77 - 94 | |
} |
Then, $genus = $em->getRepository()
with the AppBundle:Genus
shortcut.
Ok now, is there a method that can help us? Ah, how about findOneBy()
. This
works by passing it an array of things to find by - in our case 'name' => $genusName
:
... lines 1 - 50 | |
$em = $this->getDoctrine()->getManager(); | |
$genus = $em->getRepository('AppBundle:Genus') | |
->findOneBy(['name' => $genusName]); | |
... lines 55 - 96 |
Oh, and comment out the caching for now - it's temporarily going to get in the way:
... lines 1 - 50 | |
$em = $this->getDoctrine()->getManager(); | |
$genus = $em->getRepository('AppBundle:Genus') | |
->findOneBy(['name' => $genusName]); | |
// todo - add the caching back later | |
/* | |
$cache = $this->get('doctrine_cache.providers.my_markdown_cache'); | |
$key = md5($funFact); | |
if ($cache->contains($key)) { | |
$funFact = $cache->fetch($key); | |
} else { | |
sleep(1); // fake how slow this could be | |
$funFact = $this->get('markdown.parser') | |
->transform($funFact); | |
$cache->save($key, $funFact); | |
} | |
*/ | |
... lines 69 - 96 |
Get outta here caching!
Finally, since we have a Genus
object, we can simplify the render()
call and
only pass it:
... lines 1 - 11 | |
class GenusController extends Controller | |
{ | |
... lines 14 - 48 | |
public function showAction($genusName) | |
{ | |
... lines 51 - 72 | |
return $this->render('genus/show.html.twig', array( | |
'genus' => $genus | |
)); | |
} | |
... lines 77 - 94 | |
} |
Open up show.html.twig
: we just changed the variables passed into this template,
so we've got work to do. First, use genus.name
and then genus.name
again:
... lines 1 - 2 | |
{% block title %}Genus {{ genus.name }}{% endblock %} | |
{% block body %} | |
<h2 class="genus-name">{{ genus.name }}</h2> | |
... lines 7 - 21 | |
{% endblock %} | |
... lines 23 - 40 |
Remove the hardcoded sadness and replace it with genus.subFamily
, genus.speciesCount
and genus.funFact
. Oh, and remove the raw
filter - we're temporarily not rendering
this through markdown. Put it on the todo list:
... lines 1 - 4 | |
{% block body %} | |
<h2 class="genus-name">{{ genus.name }}</h2> | |
<div class="sea-creature-container"> | |
<div class="genus-photo"></div> | |
<div class="genus-details"> | |
<dl class="genus-details-list"> | |
<dt>Subfamily:</dt> | |
<dd>{{ genus.subFamily }}</dd> | |
<dt>Known Species:</dt> | |
<dd>{{ genus.speciesCount|number_format }}</dd> | |
<dt>Fun Fact:</dt> | |
<dd>{{ genus.funFact }}</dd> | |
</dl> | |
</div> | |
</div> | |
<div id="js-notes-wrapper"></div> | |
{% endblock %} | |
... lines 23 - 40 |
There's one more spot down in the JavaScript - change this to genus.name
:
... lines 1 - 23 | |
{% block javascripts %} | |
... lines 25 - 30 | |
<script type="text/babel"> | |
var notesUrl = '{{ path('genus_show_notes', {'genusName': genus.name}) }} | |
... lines 33 - 37 | |
</script> | |
{% endblock %} |
Okay team, let's give it a try. Refresh. Looks awesome! The known species is the number it should be, there is no fun fact, and the JavaScript is still working.
But what would happen if somebody went to a genus name that did not exist - like FOOBARFAKENAMEILOVEOCTOPUS? Woh! We get a bad error. This is coming from Twig:
Impossible to access an attribute ("name") on a
null
variable
because on line 3, genus
is null - it's not a Genus object:
... lines 1 - 2 | |
{% block title %}Genus {{ genus.name }}{% endblock %} | |
... lines 4 - 40 |
In the prod
environment, this would be a 500 page. We do not want that - we want
the user to see a nice 404 page, ideally with something really funny on it.
Back in the controller, the findOneBy()
method will either return one Genus object
or null. If it does not return an object, throw $this->createNotFoundException('No genus found')
:
... lines 1 - 11 | |
class GenusController extends Controller | |
{ | |
... lines 14 - 48 | |
public function showAction($genusName) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$genus = $em->getRepository('AppBundle:Genus') | |
->findOneBy(['name' => $genusName]); | |
if (!$genus) { | |
throw $this->createNotFoundException('genus not found'); | |
} | |
... lines 59 - 79 | |
} | |
... lines 81 - 98 | |
} |
Oh, and that message will only be shown to developers - not to end-users.
Head back, refresh, and this is a 404. In the prod
environment, the user
will see a 404 template that you need to setup. I won't cover how to customize the
template here - it's pretty easy - just make sure it's really clever, and send me
a screenshot. Do it!
// 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
},
"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
}
}