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!

9
Login or Register to join the conversation
Stefan-P Avatar
Stefan-P Avatar Stefan-P | posted 4 months ago

Received following error: Cannot autowire service "App\Controller\VinylController": argument "$isDebug" of method "__construct()" is type-hinted "bool", you should configure its value explicitly. The Solution: I had to define another service in the services.yaml, on the last rows, after the Mixrepository Service:

App\Controller\VinylController:
    arguments:
        $isDebug: '%kernel.debug%'
        $mixRepository: '@App\Service\MixRepository'
Reply

Hey Stefan,

Thanks for sharing this alternative solution. Another one would be to use "bind" keyword in the services.yaml and put that autowired var there :) And from the error I see it seems you typehinted that var as bool $isDebug in the constructor, so it also would require that typehint in the bind too, i.e. bool $isDebug: '%kernel.debug%'.

Cheers!

1 Reply
Yard Avatar

When I hit the 127.0.0.1:8000/browse endpoint I see the dd($mixRepository); output but it is an object containing lazyObjectState { status: UNINITIALIZED_PARTIAL }

Reply

Hey Yard,

Perhaps you're on a newer Doctrine version? That's happening because Doctrine uses proxies under the hood for performance reasons. Try calling any method on the repository before the dump call

Cheers!

1 Reply
MattWelander Avatar
MattWelander Avatar MattWelander | posted 8 months ago | edited

Is there support for object versioning in Doctrine ORM? I have a case where I'm every now and then hit by "race conditions" i.e. simultaneous requests to the server will create unwanted behaviour, like the logical outline below

SQL find all entities where stoptime == NULL
if ([check other condition]) {
    set a stoptime for entity
    create new entity
}

In manual testing, it works fine. But every now and then, there slips into the DB a duplicate entity, i e the above if-clause evaluates to true for two separate requests. I assume the reason is that both requests run the SQL query and pick up that entity where stoptime == NULL, and then one request steps into the if-clause and goes ahead and sets a stop-time, and creates a new entity. And then the second request does the same thing.

I have tried putting an extra check in there before the "create new entity" which basically looks up the DB again and asks "is there STILL an entity with stoptime == NULL?) but it doesn't help. I suppose that is also timed in a way that it gets a positive answer and right after that the first request does it's UPDATE job.

Perhaps object versioning doesn't even solve my problem? =) I want to avoid locking an entire table at all cost, I know that business can be poisonous. Although that might be my only option here..?

Reply

Hey MattWelander!

Yup, I think you are diagnosing the problem correctly! And yes, I think locking is your solution. If you don't want to lock the entire table, you could use Symfony's lock system and create a lock with a name that is very specific to the task that you are accomplishing. Then, if another request tries to accomplish that exact same task, the lock would cause that request to wait. But, it wouldn't be a database lock.

Let me know if that sounds like a good solution :).

Cheers!

Reply
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 9 months 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
    }
}
userVoice