Querying for Data!
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 SubscribeHey! There are rows in our article table! So let's update the news page to not show this hard-coded article, but instead to query the database and print real, dynamic data.
Open ArticleController
and find the show()
method:
// ... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 16 - 33 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack) | |
{ | |
if ($slug === 'khaaaaaan') { | |
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...'); | |
} | |
$comments = [ | |
'I ate a normal rock once. It did NOT taste like bacon!', | |
'Woohoo! I\'m going on an all-asteroid diet!', | |
'I like bacon too! Buy some from my site! bakinsomebacon.com', | |
]; | |
$articleContent = <<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF; | |
$articleContent = $markdownHelper->parse($articleContent); | |
return $this->render('article/show.html.twig', [ | |
'title' => ucwords(str_replace('-', ' ', $slug)), | |
'slug' => $slug, | |
'comments' => $comments, | |
'articleContent' => $articleContent, | |
]); | |
} | |
// ... lines 77 - 88 | |
} |
This renders that page. As I mentioned earlier, DoctrineBundle gives us one service - the EntityManager - that has the power to save and fetch data. Let's get it here: add another argument: EntityManagerInterface $em
:
// ... lines 1 - 7 | |
use Doctrine\ORM\EntityManagerInterface; | |
// ... lines 9 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 35 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
When you want to query for data, the first step is always the same: we need to get the repository for the entity: $repository = $em->getRepository()
and then pass the entity class name: Article::class
:
// ... lines 1 - 4 | |
use App\Entity\Article; | |
// ... lines 6 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
if ($slug === 'khaaaaaan') { | |
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...'); | |
} | |
$repository = $em->getRepository(Article::class); | |
// ... lines 46 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
This repository object knows everything about how to query from the article
table. We can use it to say $article = $repository->
. Oh, nice! It has some built-in methods, like find()
where you can pass the $id
to fetch a single article. Or, findAll()
to fetch all articles. With the findBy()
method, you can fetch all articles where a field matches some value. And findOneBy()
is the same, but only returns one Article. Let's use that: ->findOneBy()
and pass it an array with 'slug' => $slug
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
// ... line 46 | |
$article = $repository->findOneBy(['slug' => $slug]); | |
// ... lines 48 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
This will fetch one row where the slug
field matches this value. These built-in find methods are nice... but they can't do much more than this. But, don't worry! We will of course learn how to write custom queries soon.
Above this line, just to help my editor, I'll tell it that this is an Article
object:
// ... lines 1 - 4 | |
use App\Entity\Article; | |
// ... lines 6 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
// ... lines 48 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
And... hold on, that's important! When you query for something, Doctrine returns objects, not just an associative arrays with data. That's really the whole point of Doctrine! You need to stop thinking about inserting and selecting rows in a database. Instead, think about saving and fetching objects... almost as if you didn't know that a database was behind-the-scenes.
Handling 404's
At this point, it's possible that there is no article in the database with this slug. In that case, $article
will be null
. How should we handle that? Well, in the real world, this should trigger a 404 page. To do that, say if !$article
, then, throw $this->createNotFoundException()
. Pass a descriptive message, like: No article for slug "%s"
and pass $slug
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
// ... lines 51 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
I want to dig a little bit deeper to see how this work. Hold Command
on a Mac - or Ctrl
otherwise - and click this method. Ah, it comes from a trait
that's used by the base AbstractController
. Fascinating! It just throws an exception!
In Symfony, to trigger a 404, you just need to throw this very special exception class. That's why, in the controller, we throw $this->createNotFoundException()
. The message can be as descriptive as possible because it will only be shown to you: the developer.
After all of this, let's dump()
the $article
to see what it looks like and die
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
dump($article);die; | |
// ... lines 53 - 86 | |
} | |
// ... lines 88 - 99 | |
} |
Head back to your browser and first, refresh. Ok! This is the 404 page: there's nothing in the database that matches this slug: all the real slugs have a random number at the end. We see the helpful error message because this is what the 404 page looks like for developers. But of course, when you switch into the prod
environment, your users will see a different page that you can customize.
We're not going to talk about how to customize error pages... because it's super friendly and easy. Just Google for "Symfony customize error pages" and... have fun! You can create separate pages for 404 errors, 403 errors, 500 errors, or whatever your heart desires.
To find a real slug, go back to /admin/article/new
. Copy that slug, go back, paste it and... it works! There is our full, beautiful, well-written, inspiring, Article object... with fake content about meat. Having an object is awesome! We are now... dangerous.
Rendering the Article Data: Twig Magic
Back in the controller, remove the dump()
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
// ... lines 51 - 61 | |
} | |
// ... lines 63 - 74 | |
} |
Keep the hardcoded comments for now. But, remove the $articleContent
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
$comments = [ | |
'I ate a normal rock once. It did NOT taste like bacon!', | |
'Woohoo! I\'m going on an all-asteroid diet!', | |
'I like bacon too! Buy some from my site! bakinsomebacon.com', | |
]; | |
return $this->render('article/show.html.twig', [ | |
// ... lines 59 - 60 | |
]); | |
} | |
// ... lines 63 - 74 | |
} |
Let's also remove the markdown parsing code and the now-unused argument:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 61 | |
} | |
// ... lines 63 - 74 | |
} |
We'll process the markdown in the template in a minute: Back down at render()
, instead of passing title
, articleContent
and slug
, just pass article
:
// ... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
// ... lines 41 - 57 | |
return $this->render('article/show.html.twig', [ | |
'article' => $article, | |
'comments' => $comments, | |
]); | |
} | |
// ... lines 63 - 74 | |
} |
Now, open that template! With the Symfony plugin, you can cheat and hold Command
or Ctrl
and click to open it. Or, it's just in templates/article
.
Updating the template is a dream. Instead of title
, print article.title
:
{% extends 'base.html.twig' %} | |
{% block title %}Read: {{ article.title }}{% endblock %} | |
// ... lines 4 - 83 |
Oh, and in many cases... but not always... you'll get auto-completion based on the methods on your entity class!
But look closely: it's auto-completing getTitle()
. But when I hit tab, it just prints article.title
. Behind the scenes, there is some serious Twig magic happening. When you say article.title
, Twig first looks to see if the class has a title
property:
// ... lines 1 - 9 | |
class Article | |
{ | |
// ... lines 12 - 21 | |
private $title; | |
// ... lines 23 - 91 | |
} |
It does! But since that property is private, it can't use it. No worries! It then looks for a getTitle()
method. And because that exists:
// ... lines 1 - 9 | |
class Article | |
{ | |
// ... lines 12 - 21 | |
private $title; | |
// ... lines 23 - 43 | |
public function getTitle(): ?string | |
{ | |
return $this->title; | |
} | |
// ... lines 48 - 91 | |
} |
It calls it and prints that value.
This is really cool because our template code can be simple: Twig figures out what to do. If you were printing a boolean field, something like article.published
, Twig would also look for isPublished()
a hasPublished()
methods. And, if article
were an array, the dot syntax would just fetch the keys off of that array. Twig: you're the bomb.
Let's update a few more places: article.title
, then, article.slug
, and finally, for the content, article.content
, but then |markdown
:
{% extends 'base.html.twig' %} | |
{% block title %}Read: {{ article.title }}{% endblock %} | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
// ... line 13 | |
<div class="show-article-title-container d-inline-block pl-3 align-middle"> | |
<span class="show-article-title ">{{ article.title }}</span> | |
// ... lines 16 - 18 | |
<span class="pl-2 article-details"> | |
// ... line 20 | |
<a href="{{ path('article_toggle_heart', {slug: article.slug}) }}" class="fa fa-heart-o like-article js-like-article"></a> | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="article-text"> | |
{{ article.content|markdown }} | |
</div> | |
</div> | |
</div> | |
// ... lines 33 - 71 | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 78 - 83 |
The KnpMarkdownBundle gives us a markdown
filter, so that we can just process it right here in the template.
Ready to try it? Move over, deep breath, refresh. Yes! It works! Hello dynamic title! Hello dynamic bacon content!
See your Queries in the Profiler
Oh, and I have a wonderful surprise! The web debug toolbar now has a database icon that tells us how many database queries this page executed and how long they took. But wait, there's more! Click the icon to go into the profiler. Yes! This actually lists every query. You can run "EXPLAIN" on each one or view a runnable query. I use this to help debug when a particularly complex query isn't returning the results I expect.
So, um, yea. This is awesome. Next, let's take a quick detour and have some fun by creating a custom Twig filter with a Twig extension. We need to do this, because our markdown processing is no longer being cached. Boo.
simple q I hope: how did you "know" that the knpmarkdownbundle gave you a twig filter? debug_container doesn't show anything related. I guess I am asking how, when developing, you can find this from the command line (I dont use phpstorm, rather emacs) using console? Or is it a case of googling up the official docs for our markdown and hoping they provide twig facilities?