Buy

Show them a Genus, and the 404

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":

91 lines src/AppBundle/Controller/GenusController.php
... 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, and 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:

27 lines app/Resources/views/genus/list.html.twig
... 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!

Querying for One Genus

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():

96 lines src/AppBundle/Controller/GenusController.php
... 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:

96 lines src/AppBundle/Controller/GenusController.php
... 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:

96 lines src/AppBundle/Controller/GenusController.php
... 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:

96 lines src/AppBundle/Controller/GenusController.php
... 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:

40 lines app/Resources/views/genus/show.html.twig
... 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:

40 lines app/Resources/views/genus/show.html.twig
... 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:

40 lines app/Resources/views/genus/show.html.twig
... 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.

Handling 404's

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:

40 lines app/Resources/views/genus/show.html.twig
... 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'):

100 lines src/AppBundle/Controller/GenusController.php
... 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!

Leave a comment!

  • 2016-10-05 Dan Costinel

    Thank you!

    I've dug a little bit for this problem, and I guess my case is the same as using an OneToMany association. I mean, one category can have multiple articles.

    In fact what I need is:
    - I list some related articles as links with Article.title as innerHTML, and when I hover over the link (and when I click that particular link), I want to have the Category.name as part of my link. So if I have: <anchor>Symfony</anchor>, because this article belongs to "Programming" category, I want to be able to generate the link as:

    <anchor href="/programming/symfony">Symfony</anchor>

    I tried to solve this problem in the SQL tab of phpmyadmin, and I came up with this query:

    SELECT articles.title AS title,categories.name AS name FROM articles,categories WHERE articles.category_id = categories.id

    which does the job done. Now, you might be right, and the only thing I might need is an INNER JOIN on those two tables.

  • 2016-10-05 Victor Bocharsky

    Hey Dan,

    It's pretty simple - use entity manager!


    /**
    * @Route("/{categoryName}/{articleTitle}", name="article_show")
    * @Method("GET")
    */
    public function showAction($categoryName, $articleTitle)
    {
    $em = $this->getDoctrine()->getEntityManager();
    $category = $em->getRepository('AppBundle:Category')->findBy([
    'name' => $categoryName,
    ]);
    $article = $em->getRepository('AppBundle:Article')->findBy([
    'category' => $category,
    'title' => $articleTitle,
    ]);
    }

    Here will be 2 queries to the DB, but if you want, you can do more complex things: create method in ArticleRepository where you do INNER JOIN with Category, so you can do the same with just a single query.

    However, if you want - you can do that with ParamConverter of course: look closely to the "options":


    **
    * @Route("/{categoryName}/{articleTitle}", name="article_show")
    * @ParamConverter("category", class="AppBundle:Category", options={"name": "categoryName"})
    * @ParamConverter("article", class="AppBundle:Article", options={"title": "articleTitle"})
    * @Method("GET")
    */
    public function showAction(Category $category, Article $article)

    BTW, in any case, the category's name and article's title columns should be unique, otherwise you'll get problems.

  • 2016-10-05 Dan Costinel

    Hi guys!
    I have a big problem regarding a specific route. So I'll start by explaining my two db tables.

    Categories table:
    id | name
    -------------
    1 programming
    2 design

    Articles table:
    id | category_id | title | content
    ----------------------------------------------------
    1 1 symfony ...
    2 2 bootstrap ...
    3 1 php ...

    And I need a route like this: /app_dev.php/category_name/article_title. Some examples:
    - /programming/symfony
    - /programming/php
    - /design/bootstrap

    And I have to following action method:
    /**
    * @Route("/{categoryName}/{articleTitle}", name="article_show")
    * @Method("GET")
    */
    public function showAction(Category $categoryName, Article $articleTitle)
    {
    // Here I need to somehow query my db in order to achieve anchors like the ones above
    // {{ article.title }}
    // Can you please give me some advice in order to achieve my goal? Thank you!
    }

    // The relevant Twig code:
    {% for category in categories %}
    {% for article in articles %}
    {{ article.title }}
    {% endfor %}
    {% endfor %}

    The routes I obtain are ok, but the problem is that there are rendered too many links.
    I read the official Symfony doc article ( http://symfony.com/doc/current... ), but honestly I can figure out much from it, and I don't know if this applies for my case.