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!
Hey mlavrik ,
You're right, the most common mistake is to forget to clear the prod cache. Customizing error pages is really straightforward, we just keep it for another screencast. We have a simple screencast about it here: https://knpuniversity.com/s...
P.S. Your octopus is really cool - love it! :)
Cheers!
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/curr... ), but honestly I can figure out much from it, and I don't know if this applies for my case.
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.
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.
Hey there
How can we generate unique routes? Or in other words, when we have two genus with the same name like "Octopus", the link between the list.html and the show.html will always show the same genus.
I hope you understand my newbie-question;-)
Thanks!
Michael
Hey Michael,
To prevent this you have to make the name unique, i.e. add a "unique=true" option to the property annotation like:
/**
*@ORM\Column(type="string", unique=true)
*/
private $name;
After this, if you try to set a name which already taken - you'll get an exception. So before saving new Genus object - you will ensure that there's no such name in the database, i.e. send an extra query to the DB which will check it. Very often for this purpose devs uses another property, which names as slug (or alias). Take a look at the fist 2 chapters in https://knpuniversity.com/screencast/collections course, which explains how to use the StofDoctrineExtensionsBundle to implement the sluggable behavior in your project. This bundle will handle a lot of work for you.
Cheers!
Hey Victor
Thanks a lot for your great and fast explanations!!
In my database it will give a lot of same names. So I had a look at the screencasts and everything is clear by now.
I like symfony and this really great sreencasts here!!
Hey Victor
Can you explain me, why do you use the name for the route and not the ID in the tutorial? Are there some advantages?
By the way: your tutorials are suuuuuuper!!! I really like them!!!
Yo Michael,
Sure! Well, actually it's not so important, you can use whatever you want... but IDs looks pretty old. Look at modern websites - they all use slugs instead of IDs. The first advantage is just it's more readable. Let me show you an example: http://example.com/php-tuto... - I can guess what I can find following this link due to the slug in it. But I have no idea what is the link about if it looks like: http://example.com/128. So ID is something internal, which you probably would like to use in admin panel (because it's simpler), but not for your users. Also, slugs help you to promote your website for search engines, i.e. it's a part of SEO. That's why slugs FTW!
P.S. Thanks for your kind word! ;)
Cheers!
Why are we passing genus.name to the show action via the href and not some abstraction of the primary key? Or is all this optimised in the framework? I'm also wondering why we're querying again in the show action (on name and not just by pkey/abstraction of pkey at that) . Is it because the collection created for the list is disposed of following the list show action?
Hey Max,
We just want to make our URLs more readable, because IDs is something internal. Here's my expanded answer about it: https://knpuniversity.com/s...
If you will still have any questions - let me know!
Cheers!
Yes, I understand why not the pkey but name is a slower lookup - this is why I mentioned "abstraction" of the pkey (as well as the actual url being rewriteable so it wouldnt have to actually show the key). I thought maybe the framework would then map that abstraction to the pkey for efficiency. Possibly I'm overthinking it. So here we are querying a list and getting all genuses - we then click a link and fetch the same one as we already had - I'm guessing this is normal in PHP web interfaces as we want to dispose of the original list query as soon as possible - buffering it to avoid the "show" lookup would make no sense on the big scale of things.
Hey Richard ,
Ah, I see what you mean! Yeah, you're right, searching by name is slower, so you're probably want to create an index for names, or probably makes them unique which will be even better. However, more ofter developers create a separate field and name it "slug" or "alias", which is unique. Btw, StofDoctrineExtensionsBundle could helps with it a lot. So in real project it's fair point and you have to think about some kind of slug field, but for simplicity, we just use names in this screencast ;)
Cheers!
Hey Tony!
This isn't a bad one at all :). This is a common issue - usually due to some slight differences in how your web server is setup (and some shortcuts that I took to save us some time - which help cause the issue)! We talk about this in the first tutorial when we put this part together: https://knpuniversity.com/s...
Are you using the built-in PHP web server, or something else like Nginx or Apache? And what is the URL you are going to in the browser when you access the site?
Cheers!
Oh, no. Not in my local dev. Here on KNP was trying to "send you a clever 404 page." No worries. ^_^
// 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
}
}
After this cast my production part was broken. Any route I try returns me a 404 or 500 errors. But there was no errors in code and the reason was simple - don't forget to clear the prod cache guys :)
Btw custom 404 is easy - https://ibb.co/f4NyrQ :)