Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Querying the Database

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 we've saved some stuff to the database, how can we read or query for it? Once again, at least for simple stuff, Doctrine doesn't want you to worry about querying. Instead, we just ask Doctrine for the objects we want.

Head over to src/Controller/VinylController.php and find the browse() action.

... lines 1 - 10
class VinylController extends AbstractController
{
... lines 13 - 37
public function browse(string $slug = null): Response
{
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null;
$mixes = $this->mixRepository->findAll();
return $this->render('vinyl/browse.html.twig', [
'genre' => $genre,
'mixes' => $mixes,
]);
}
}

Here, we're loading all of the $mixes in our project... and we're currently doing it via this MixRepository service class that we created in the last episode. This class talks to a GitHub repository and reads from a hard-coded text file.

We're going to stop using this MixRepository and instead load these $mixes from the database.

Querying through the Entity Manager

Ok: to save objects, we leveraged the EntityManagerInterface service, which is the most important service by far in Doctrine. Whelp, this service can also query for objects. Let's take advantage of that. Add a new argument to browse(), type-hinted with EntityManagerInterface... and call it $entityManager.

... lines 1 - 6
use Doctrine\ORM\EntityManagerInterface;
... lines 8 - 12
class VinylController extends AbstractController
{
... lines 15 - 39
public function browse(EntityManagerInterface $entityManager, string $slug = null): Response
{
... lines 42 - 51
}
}

Then, below, replace the $mixes line with two lines. Start with $mixRepository = $entityManager->getRepository() passing this the name of the class that we want to query from. Yes, we think about querying from an entity class, not a table. In this case, we want to query from VinylMix::class.

We'll talk more about this repository concept in a minute. Then, to get the mixes themselves, say $mixes = $mixRepository-> and call one of the methods on it: findAll().

To see what this gives us, let's dd($mixes).

... lines 1 - 39
public function browse(EntityManagerInterface $entityManager, string $slug = null): Response
{
... lines 42 - 43
$mixRepository = $entityManager->getRepository(VinylMix::class);
$mixes = $mixRepository->findAll();
dd($mixes);
... lines 47 - 51
}
... lines 53 - 54

Ok, testing time! Spin over, head back to the homepage, click "Browse mixes" to hit that action, and... voila! We get six results! And each of them, most importantly, is a VinylMix object.

Behind the scenes, Doctrine did query the table and the columns. But instead of giving us that raw data, it put it onto objects and gave us those, which is so much nicer.

Working with Objects in Twig

If we remove the dd()... this array of VinylMix object will be passed into the template, instead of the array of array data that we had before. But... the page still works. Though, these images are broken because apparently the service I'm using to load them is down right now. Ah... the joys of video recording. But that won't stop us!

The fact that all the data still renders without any errors is... actually kind of by luck. When we render the template - templates/vinyl/browse.html.twig - we loop over all of the mixes. The template works because the old GitHub repository text file had the same keys (like title, trackCount, and genre) as our VinylMix class.

... lines 1 - 28
{% for mix in mixes %}
... line 30
<div class="mixed-vinyl-container p-3 text-center">
... line 32
<p class="mt-2"><strong>{{ mix.title }}</strong></p>
<span>{{ mix.trackCount }} Tracks</span>
|
<span>{{ mix.genre }}</span>
|
<span>{{ mix.createdAt|ago }}</span>
... lines 39 - 40
{% endfor %}
... lines 42 - 46

There is one cool thing happening here, though. When we say mix.genre, mix is now an object... and this genre property is private. That means we cannot access it directly. But Twig is smart. It realizes that this is private and looks for a getGenre() method. So in our template, we say mix.genre, but in reality, it calls the getGenre() method. That's pretty awesome.

Visualizing the Queries for the Page

Know what else is awesome? We can see the queries any page made! Down in the web debug toolbar, Doctrine gives us a fancy new icon. Oooo. And if we click into that... tah dah! There's one database query... and we can even see what it is. You can also see a formatted version of it... though I need to refresh the page for this to work... because the Turbo JavaScript library we installed in the first tutorial doesn't always play nice with this profiler area. Anyways, we can also see a runnable version of the query or run "Explain" on it.

The "Repository"

All right, back in the controller, even though we can query through the EntityManagerInterface, we normally query through something called the repository. dd() this $mixRepository object to get more info about it.

... lines 1 - 12
class VinylController extends AbstractController
{
... lines 15 - 39
public function browse(EntityManagerInterface $entityManager, string $slug = null): Response
{
... lines 42 - 44
dd($mixRepository);
... lines 46 - 51
}
}

Then go back to the /browse page and... it's an App\Repository\VinylMixRepository object. Hey! We know that class! It lives in our code, in the src/Repository/ directory. It was generated by MakerBundle.

Inside the ORM\Entity attribute above our entity class, MakerBundle generated a repositoryClass option that points to this. Thanks to this config, our entity, VinylMix, is tied to VinylMixRepository. So when you ask Doctrine to give us the repository for the VinylMix class, it knows to return the VinylMixRepository object.

The repository for an entity knows everything about how to query for its data. And, without us doing anything, it already has a bunch of useful methods on it for basic queries, like findAll(), findOneBy() and several others. In a bit, we'll learn how to add new methods to the repository to make custom queries.

Anyway, VinylMixRepository is actually a service in the container... so we can get it more easily by autowiring it directly. Add a VinylMixRepository $mixRepository argument... and then we don't need this line at all. That is simpler... and it still works!

... lines 1 - 38
public function browse(VinylMixRepository $mixRepository, string $slug = null): Response
{
... lines 41 - 42
$mixes = $mixRepository->findAll();
... lines 44 - 48
}
... lines 50 - 51

The takeaway is this: if you want to query from a table, you'll do that through the repository of the entity whose data you need.

Next: The fact that we changed our code to load from the database and didn't need to update our Twig template at all was kind of awesome! And courtesy of some Twig magic. Let's talk more about that magic and create a virtual property that we can print in the template.

Leave a comment!

3
Login or Register to join the conversation
Andrey Avatar

Hey, you keep mentioning MixRepository service "...that we created in the last episode", but that episode seems to be missing.

Reply
victor Avatar victor | SFCASTS | Andrey | posted 26 days ago | HIGHLIGHTED

Hey Andrey,

About the episode, we meant the previous course, not a chapter. Take a look at this chapter: https://symfonycasts.com/screencast/symfony-fundamentals/create-service

Cheers!

Reply
Andrey Avatar

Thanks for clarification!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}