Fetching Relations
Yes! Each Article
is now related to two comments in the database. So, on the article show page, it's time to get rid of this hardcoded stuff and, finally, query for the true comments for this Article
.
In src/Controller
, open ArticleController
and find the show()
action:
// ... lines 1 - 16 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 19 - 40 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack) | |
{ | |
if ($article->getSlug() === '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', | |
]; | |
return $this->render('article/show.html.twig', [ | |
'article' => $article, | |
'comments' => $comments, | |
]); | |
} | |
// ... lines 61 - 73 | |
} |
This renders a single article. So, how can we find all of the comments related to this article? Well, we already know one way to do this.
Remember: whenever you need to run a query, step one is to get that entity's repository. And, surprise! When we generated the Comment
class, the make:entity
command also gave us a new CommentRepository
:
// ... lines 1 - 2 | |
namespace App\Repository; | |
use App\Entity\Comment; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Symfony\Bridge\Doctrine\RegistryInterface; | |
/** | |
* @method Comment|null find($id, $lockMode = null, $lockVersion = null) | |
* @method Comment|null findOneBy(array $criteria, array $orderBy = null) | |
* @method Comment[] findAll() | |
* @method Comment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | |
*/ | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
public function __construct(RegistryInterface $registry) | |
{ | |
parent::__construct($registry, Comment::class); | |
} | |
// ... lines 21 - 49 | |
} |
Thanks MakerBundle!
Get the repository by adding a CommentRepository
argument. Then, let's see, could we use one of the built-in methods? Try $comments = $commentRepository->findBy()
, and pass this article
set to the entire $article
object:
// ... lines 1 - 6 | |
use App\Repository\CommentRepository; | |
// ... lines 8 - 17 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 20 - 41 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack, CommentRepository $commentRepository) | |
{ | |
if ($article->getSlug() === 'khaaaaaan') { | |
// ... line 48 | |
} | |
$comments = $commentRepository->findBy(['article' => $article]); | |
// ... lines 52 - 63 | |
} | |
// ... lines 65 - 77 | |
} |
Dump these comments and die:
// ... lines 1 - 17 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 20 - 41 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack, CommentRepository $commentRepository) | |
{ | |
// ... lines 47 - 50 | |
$comments = $commentRepository->findBy(['article' => $article]); | |
dump($comments);die; | |
// ... lines 53 - 63 | |
} | |
// ... lines 65 - 77 | |
} |
Then, find your browser and, try it! Yes! It returns the two Comment
objects related to this Article!
So, the weird thing is that, once again, you need to stop thinking about the columns in your tables, like article_id
, and only think about the properties on your entity classes. That's why we use 'article' => $article
. Of course, behind the scenes, Doctrine will make a query where article_id
= the id from this Article
. But, in PHP, we think all about objects.
Fetching Comments Directly from Article
As nice as this was... there is a much simpler way! When we generated the relationship, it asked us if we wanted to add an optional comments
property to the Article
class, for convenience. We said yes! And thanks to that, we can literally say $comments = $article->getComments()
. Dump $comments
again:
// ... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 16 - 37 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack) | |
{ | |
// ... lines 43 - 46 | |
$comments = $article->getComments(); | |
dump($comments);die; | |
// ... lines 49 - 59 | |
} | |
// ... lines 61 - 73 | |
} |
Oh, and now, we don't need the CommentRepository
anymore:
// ... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 16 - 37 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack) | |
{ | |
// ... lines 43 - 59 | |
} | |
// ... lines 61 - 73 | |
} |
Cool.
Lazy Loading
Head back to your browser and, refresh! It's the exact same as before. Wait, what? What's this weird PersistentCollection
thing?
Here's what's going on. When Symfony queries for the Article
, it only fetches the Article
data: it does not automatically fetch the related Comments. And, for performance, that's great! We may not even need the comment data! But, as soon as we call getComments()
and start using that, Doctrine makes a query in the background to go get the comment data.
This is called "lazy loading": related data is not queried for until, and unless, we use it. To make this magic possible, Doctrine uses this PersistentCollection
object. This is not something you need to think or worry about: this object looks and acts like an array.
To prove it, let's foreach over $comments as $comment
and dump each $comment
inside. Put a die
at the end:
// ... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 16 - 37 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack) | |
{ | |
// ... lines 43 - 46 | |
$comments = $article->getComments(); | |
foreach ($comments as $comment) { | |
dump($comment); | |
} | |
die; | |
// ... lines 52 - 62 | |
} | |
// ... lines 64 - 76 | |
} |
Try it again! Boom! Two Comment
objects!
Fetching the Comments in the Template
Back in the controller, we no longer need these hard-coded comments. In fact, we don't even need to pass comments
into the template at all! That's because we can call the getComments()
method directly from Twig!
Remove all of the comment logic:
// ... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
// ... lines 16 - 37 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show(Article $article, SlackClient $slack) | |
{ | |
if ($article->getSlug() === 'khaaaaaan') { | |
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...'); | |
} | |
return $this->render('article/show.html.twig', [ | |
'article' => $article, | |
]); | |
} | |
// ... lines 51 - 63 | |
} |
And then, jump into templates/article/show.html.twig
. Scroll down a little... ah, yes! First, update the count: article.comments|length
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 71 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 80 - 86 |
Easy! Then, below, change the loop to use for comment in article.comments
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 57 | |
{% for comment in article.comments %} | |
// ... lines 59 - 69 | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 80 - 86 |
And because each comment has a dynamic author, print that with {{ comment.authorName }}
. And the content is now comment.content
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 57 | |
{% for comment in article.comments %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
// ... line 61 | |
<div class="comment-container d-inline-block pl-3 align-top"> | |
<span class="commenter-name">{{ comment.authorName }}</span> | |
// ... lines 64 - 65 | |
<span class="comment"> {{ comment.content }}</span> | |
// ... line 67 | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 81 - 87 |
Oh, and, because each comment has a createdAt
, let's print that too, with our trusty ago
filter:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 57 | |
{% for comment in article.comments %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
// ... line 61 | |
<div class="comment-container d-inline-block pl-3 align-top"> | |
<span class="commenter-name">{{ comment.authorName }}</span> | |
<small>about {{ comment.createdAt|ago }}</small> | |
// ... line 65 | |
<span class="comment"> {{ comment.content }}</span> | |
// ... line 67 | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 81 - 87 |
Love it! Let's try it! Go back, refresh and... yes! Two comments, from about 17 minutes ago. And, check this out: on the web debug toolbar, you can see that there are two database queries. The first query selects the article
data only. And the second selects all of the comment data where article_id
matches this article's id - 112. This second query doesn't actually happen until we reference the comments from inside of Twig:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 57 | |
{% for comment in article.comments %} | |
// ... lines 59 - 70 | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 81 - 87 |
That laziness is a key feature of Doctrine relations.
Next, it's time to talk about the subtle, but super-important distinction between the owning and inverse sides of a relation.
Offtopic: I want to chain objects. I got OneToMany relations.
I want to go from Object1->Object2->Object3.
But when I dd(Object1) it says that Object3 is: +__isInitialized__: false
And other columns of Object3 are null, while in the database they have values.
How do I fix this?