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 SubscribeWith the article show page now dynamic, let's turn to the homepage... cause these news stories are totally still hardcoded. Open ArticleController
and find the homepage()
action:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage() | |
{ | |
return $this->render('article/homepage.html.twig'); | |
} | |
... lines 35 - 74 | |
} |
Perfect. Just like before, we need to query for the articles. This means that we need an EntityManagerInterface $em
argument:
... lines 1 - 7 | |
use Doctrine\ORM\EntityManagerInterface; | |
... lines 9 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage(EntityManagerInterface $em) | |
{ | |
... lines 33 - 38 | |
} | |
... lines 40 - 79 | |
} |
Next, we get the repository for the class: $repository = $em->getRepository(Article::class)
. And then we can say, $articles = $repository->findAll()
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage(EntityManagerInterface $em) | |
{ | |
$repository = $em->getRepository(Article::class); | |
$articles = $repository->findAll(); | |
... lines 35 - 38 | |
} | |
... lines 40 - 79 | |
} |
Nice! With this array of Article
objects in hand, let's pass those into the template as a new articles
variable:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage(EntityManagerInterface $em) | |
{ | |
$repository = $em->getRepository(Article::class); | |
$articles = $repository->findAll(); | |
return $this->render('article/homepage.html.twig', [ | |
'articles' => $articles, | |
]); | |
} | |
... lines 40 - 79 | |
} |
Now, to the template! Open homepage.html.twig
and scroll down just a little bit. Yes: here is the article list:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<!-- Article List --> | |
<div class="col-sm-12 col-md-8"> | |
... lines 10 - 18 | |
<!-- Supporting Articles --> | |
<div class="article-container my-1"> | |
<a href="{{ path('article_show', {slug: 'why-asteroids-taste-like-bacon'}) }}"> | |
<img class="article-img" src="{{ asset('images/asteroid.jpeg') }}"> | |
<div class="article-title d-inline-block pl-3 align-middle"> | |
<span>Why do Asteroids Taste Like Bacon?</span> | |
<br> | |
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> Mike Ferengi </span> | |
<span class="pl-5 article-details float-right"> 3 hours ago</span> | |
</div> | |
</a> | |
</div> | |
... lines 32 - 56 | |
</div> | |
... lines 58 - 77 | |
</div> | |
</div> | |
{% endblock %} |
Well, there's a "main" article on top, but I'm going to ignore that for now. Down below, add for article in articles
with, at the bottom, endfor
:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<!-- Article List --> | |
<div class="col-sm-12 col-md-8"> | |
... lines 10 - 18 | |
<!-- Supporting Articles --> | |
{% for article in articles %} | |
<div class="article-container my-1"> | |
... lines 23 - 31 | |
</div> | |
{% endfor %} | |
</div> | |
... lines 35 - 54 | |
</div> | |
</div> | |
{% endblock %} |
Then... just make things dynamic: article.slug
, article.title
, and for the three hours ago, if article.publishedAt
is not null, print article.publishedAt|ago
. If it's not published, do nothing. With this in place, delete the last two hardcoded articles:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<!-- Article List --> | |
<div class="col-sm-12 col-md-8"> | |
... lines 10 - 18 | |
<!-- Supporting Articles --> | |
{% for article in articles %} | |
<div class="article-container my-1"> | |
<a href="{{ path('article_show', {slug: article.slug}) }}"> | |
<img class="article-img" src="{{ asset('images/asteroid.jpeg') }}"> | |
<div class="article-title d-inline-block pl-3 align-middle"> | |
<span>{{ article.title }}</span> | |
<br> | |
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> Mike Ferengi </span> | |
<span class="pl-5 article-details float-right"> {{ article.publishedAt ? article.publishedAt|ago }}</span> | |
</div> | |
</a> | |
</div> | |
{% endfor %} | |
</div> | |
... lines 35 - 54 | |
</div> | |
</div> | |
{% endblock %} |
Let's give it a try: find your browser and, refresh! Nice! You can see a mixture of published and unpublished articles. But... you can also see that the articles just printed out in whatever order they were created, regardless of the publish date. Space travellers demand fresh content! So let's print the newest articles first.
Head back to ArticleController
. Hmm... the findAll()
methods gives us everything... but it's pretty limited. In fact, it takes zero arguments: you can't control it at all:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage(EntityManagerInterface $em) | |
{ | |
... line 33 | |
$articles = $repository->findAll(); | |
... lines 35 - 38 | |
} | |
... lines 40 - 79 | |
} |
But, some of the other methods are just a little bit more flexible. To control the order, use findBy()
instead, pass this an empty array, and then another array with publishedAt
set to DESC
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 30 | |
public function homepage(EntityManagerInterface $em) | |
{ | |
... line 33 | |
$articles = $repository->findBy([], ['publishedAt' => 'DESC']); | |
... lines 35 - 38 | |
} | |
... lines 40 - 79 | |
} |
The first array is where you would normally pass some criteria for a WHERE clause. If we pass nothing, we get everything!
Try it - refresh! Much better!
Except... hmm... it probably does not make sense to show the unpublished articles on the homepage. And this is when things get a bit more interesting. Sure, you can pass simple criteria to findBy()
, like slug
equal to some value. But, in this case, we need a query that says WHERE publishedAt IS NOT NULL
. That's just not possible with findBy()
!
And so... for the first time, we're going to write - drumroll - a custom query!
Let me show you something cool: when we originally generated our entity, the command created the Article
class, but it also created an ArticleRepository
class in the Repository
directory. Try this: dump($repository)
and, refresh. Guess what? This is an instance of that ArticleRepository
!
Yes, there is a connection between the Article
and ArticleRepository
classes. In fact, that connection is explicitly configured right at the top of your Article
class:
... lines 1 - 6 | |
/** | |
* @ORM\Entity(repositoryClass="App\Repository\ArticleRepository") | |
*/ | |
class Article | |
{ | |
... lines 12 - 91 | |
} |
This says: when we ask for the Article
class's repository, Doctrine should give us an instance of this ArticleRepository
class:
... lines 1 - 5 | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
... lines 7 - 14 | |
class ArticleRepository extends ServiceEntityRepository | |
{ | |
... lines 17 - 49 | |
} |
Oh, and the built-in find*()
methods actually come from one of the parent classes of ArticleRepository
.
So... why the heck are we talking about this? Because, if you want to create a custom query, you can do that by creating a custom method inside of this class. And, hey! It even has a couple of examples:
... lines 1 - 14 | |
class ArticleRepository extends ServiceEntityRepository | |
{ | |
... lines 17 - 21 | |
// /** | |
// * @return Article[] Returns an array of Article objects | |
// */ | |
/* | |
public function findByExampleField($value) | |
{ | |
return $this->createQueryBuilder('a') | |
->andWhere('a.exampleField = :val') | |
->setParameter('val', $value) | |
->orderBy('a.id', 'ASC') | |
->setMaxResults(10) | |
->getQuery() | |
->getResult() | |
; | |
} | |
*/ | |
/* | |
public function findOneBySomeField($value): ?Article | |
{ | |
return $this->createQueryBuilder('a') | |
->andWhere('a.exampleField = :val') | |
->setParameter('val', $value) | |
->getQuery() | |
->getOneOrNullResult() | |
; | |
} | |
*/ | |
} |
Uncomment the first example, and rename it to findAllPublishedOrderedByNewest()
:
... lines 1 - 14 | |
class ArticleRepository extends ServiceEntityRepository | |
{ | |
... lines 17 - 21 | |
/** | |
* @return Article[] | |
*/ | |
public function findAllPublishedOrderedByNewest() | |
{ | |
... lines 27 - 34 | |
} | |
... lines 36 - 47 | |
} |
I love descriptive names... or maybe I love long names... not sure.
Anyways, it's time to talk about how you actually write custom queries in Doctrine. Let's do that next!
Hey sadikoff
Thank you for your response and interest. In the meantime I have fixed the issue. It was on my side. I will delete this whole thread now because I don't see any value that it brings to the community. Again, wonderful project symfonycasts.
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.4
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.0.4
"symfony/console": "^4.0", // v4.0.14
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.0.14
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/twig-bundle": "^4.0", // v4.0.4
"symfony/web-server-bundle": "^4.0", // v4.0.4
"symfony/yaml": "^4.0" // v4.0.14
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"fzaninotto/faker": "^1.7", // v1.7.1
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
"symfony/dotenv": "^4.0", // v4.0.14
"symfony/maker-bundle": "^1.0", // v1.4.0
"symfony/monolog-bundle": "^3.0", // v3.1.2
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.0.4
}
}
Hey there
Pretty weird situation. Can you show how is Repository annotation is defined in your Entity? and it will be great to know Symfony version you are using.
Cheers!