Custom Repository Class

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Now that the show page is working, let's bring the homepage to life! This time, instead of querying for one Question object, we want to query for all of them.

findAll() for All Data

Head over to QuestionController and scroll up to homepage(). Ok, to fetch data, we need to autowire the entity manager with EntityManagerInterface $entityManager.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
... lines 30 - 34
}
... lines 36 - 98
}

Now add $repository = $entityManager->getRepository(Question::class).

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
$repository = $entityManager->getRepository(Question::class);
... lines 31 - 34
}
... lines 36 - 98
}

And finally, $questions = $repository->findAll(). Let's dd($questions) to see what these look like.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
$repository = $entityManager->getRepository(Question::class);
$questions = $repository->findAll();
dd($questions);
... lines 33 - 34
}
... lines 36 - 98
}

Rendering all the Questions

Ok, refresh the homepage. There we go! 12 Question objects for the 12 rows in my table. Now we're dangerous because we can pass these into our template. Add a second argument to render() - an array - to pass a questions variable set to our array of Question objects.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
$repository = $entityManager->getRepository(Question::class);
$questions = $repository->findAll();
return $this->render('question/homepage.html.twig', [
'questions' => $questions,
]);
}
... lines 37 - 99
}

Pop open the template: templates/question/homepage.html.twig. Let's see: the homepage currently has two hard coded questions. I want to loop right inside the row: {% for question in questions %}. Trace the markup down to see where this ends and... add {% endfor %}. Delete the 2nd hard-coded question completely.

... lines 1 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in questions %}
<div class="col-12 mb-3">
... lines 19 - 43
</div>
{% endfor %}
</div>
</div>
... lines 48 - 50

Perfect. Now it's just like the show page because we have a question variable. The first thing to update is the question name - {{ question.name }} and the slug also needs to be dynamic: question.slug.

... lines 1 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in questions %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
... lines 22 - 27
<div class="col">
<a class="q-title" href="{{ path('app_question_show', { slug: question.slug }) }}"><h2>{{ question.name }}</h2></a>
... lines 30 - 34
</div>
</div>
</div>
... lines 38 - 42
</div>
</div>
{% endfor %}
</div>
</div>
... lines 48 - 50

Below, for the question text, use {{ question.question|parse_markdown }}. We might also want to only show some of the question on the page - we could do that by adding a new method - like getQuestionPreview() to the entity - and using it here. We'll see this idea of custom entity methods later.

... lines 1 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in questions %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
... lines 22 - 27
<div class="col">
<a class="q-title" href="{{ path('app_question_show', { slug: question.slug }) }}"><h2>{{ question.name }}</h2></a>
<div class="q-display p-3">
<i class="fa fa-quote-left mr-3"></i>
<p class="d-inline">{{ question.question|parse_markdown }}</p>
<p class="pt-4"><strong>--Tisha</strong></p>
</div>
</div>
</div>
</div>
... lines 38 - 42
</div>
</div>
{% endfor %}
</div>
</div>
... lines 48 - 50

At the bottom, there's one more link: question.slug.

... lines 1 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in questions %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
... lines 22 - 27
<div class="col">
<a class="q-title" href="{{ path('app_question_show', { slug: question.slug }) }}"><h2>{{ question.name }}</h2></a>
<div class="q-display p-3">
<i class="fa fa-quote-left mr-3"></i>
<p class="d-inline">{{ question.question|parse_markdown }}</p>
<p class="pt-4"><strong>--Tisha</strong></p>
</div>
</div>
</div>
</div>
<a class="answer-link" href="{{ path('app_question_show', { slug: question.slug }) }}" style="color: #fff;">
... lines 39 - 41
</a>
</div>
</div>
{% endfor %}
</div>
</div>
... lines 48 - 50

Done! Doctrine makes it easy to query for data and Twig makes it easy to render. Go team! At the browser, refresh and... cool!

Ordering the Data

Each question has a random askedAt date - you can see it by clicking into each one. What we probably want to do is put the newest questions on top. In other words, we want to do the same query but with ORDER BY askedAt DESC.

If you click the database icon on the web debug toolbar, you can see that the query doesn't have an ORDER BY yet. When you're working with the built-in methods on the repository class, you're a bit limited - there are many custom things that these methods simply can't do. For example, findAll() doesn't have any arguments: there's no way to customize the order or anything else. Soon we'll learn how to write custom queries so we can do whatever we want.

But, in this case, there is another method that can help: findBy(). Pass this an empty array - we don't need any WHERE statements - and then another array with 'askedAt' => 'DESC'.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
... line 30
$questions = $repository->findBy([], ['askedAt' => 'DESC']);
... lines 32 - 35
}
... lines 37 - 99
}

Let's try it! Refresh! And... click the first: 10 days ago. Click the second: 1 month ago! I think we got it! If we jump into the profiler... yes! It has ORDER BY asked_at DESC.

We've now pushed the built-in repository methods about as far as they can go.

EntityRepository

Question time: when we call getRepository(), what does that actually return? It's an object of course, but what type of object? The answer is: EntityRepository.

In PhpStorm, I'll press Shift+Shift and type EntityRepository.php. I want to see what this looks like. Make sure to include all "non project items". Here it is!

EntityRepository lives deep down inside of Doctrine and it is where the methods we've been using live, like find(), findAll(), findBy(), findOneBy() and some more.

Our Custom Repository Class

But check this out: in the controller, dd($repository).

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
$repository = $entityManager->getRepository(Question::class);
dd($repository);
... lines 32 - 36
}
... lines 38 - 100
}

When we refresh... surprise! I lied! Sort of...

Instead of being an instance of EntityRepository - like I promised - this is an instance of App\Repository\QuestionRepository. Hey! That's a class that lives in our project! Open it up: src/Repository/QuestionRepository.php.

... lines 1 - 2
namespace App\Repository;
use App\Entity\Question;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Question|null find($id, $lockMode = null, $lockVersion = null)
* @method Question|null findOneBy(array $criteria, array $orderBy = null)
* @method Question[] findAll()
* @method Question[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class QuestionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Question::class);
}
... lines 21 - 49
}

When we originally ran make:entity to generate Question, it actually generated two classes: Question and QuestionRepository. This class extends another called ServiceEntityRepository. And if you hold Command or Ctrl and click into it, that class extends EntityRepository! The class we were just looking at.

When we ask for the repository for the Question entity, Doctrine actually returns a QuestionRepository object. But since that ultimately extends EntityRepository, we have access to all the helper methods like findAll() and findBy().

But... how does Doctrine knows to give us an instance of this class? How does it connect the Question entity to the QuestionRepository class? Is it relying on a naming convention?

Nope! The answer lives at the top of the Question class: we have @ORM\Entity() with repositoryClass=QuestionRepository::class. This was generated for us by make:entity.

... lines 1 - 7
/**
* @ORM\Entity(repositoryClass=QuestionRepository::class)
*/
class Question
{
... lines 13 - 91
}

Here's the big picture: when we call getRepository() and pass it Question::class, Doctrine will give us an instance of QuestionRepository. And because that extends EntityRepository, we get access to the shortcut methods!

Custom Repository Methods

The reason this is cool is that anytime we need to write a custom query for the Question entity, we can add a new method inside of QuestionRepository.

The class already has an example: uncomment the findByExampleField() method. If I have a findByExampleField() method in the repository, it means that we can call this from the controller.

... lines 1 - 14
class QuestionRepository extends ServiceEntityRepository
{
... lines 17 - 21
/**
* @return Question[] Returns an array of Question objects
*/
public function findByExampleField($value)
{
return $this->createQueryBuilder('q')
->andWhere('q.exampleField = :val')
->setParameter('val', $value)
->orderBy('q.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
... lines 36 - 47
}

In a few minutes, we're going to write a custom query that finds all questions WHERE askedAt IS NOT NULL. In QuestionRepository, let's create a method to hold this. How about: findAllAskedOrderedByNewest() and this won't need any arguments.

... lines 1 - 14
class QuestionRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllAskedOrderedByNewest()
{
... lines 27 - 34
}
... lines 36 - 47
}

In the controller, remove the dd() and say $questions = $repository->findAllAskedOrderedByNewest().

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 27
public function homepage(EntityManagerInterface $entityManager)
{
$repository = $entityManager->getRepository(Question::class);
$questions = $repository->findAllAskedOrderedByNewest();
... lines 32 - 35
}
... lines 37 - 99
}

Of course, that won't work yet because the logic is all wrong, but it will call the new method.

Next, let's learn about DQL and the query builder. Then, we'll create a custom query that will return the exact results we want.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.9.0
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}