Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine


Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We've got this cool ->andWhere() method that searches on the name or iconKey properties of the Category entity. But could we also search on the fortune cookie data inside each category? Sure!

Let's see how that relation is set up. In Category, we have a OneToMany relationship on a property called $fortuneCookies over to the FortuneCookie entity.

Thinking about JOINs in Doctrine

If we think about the problem from a database perspective, in order to update our WHERE clause to include WHERE fortune_cookie.fortune = :searchTerm, we first need to JOIN to the fortune_cookie table.

And that is what we're going to do in Doctrine... except with a twist. Instead of thinking about joining across tables, we're going to think about joining across entity classes. This might feel weird at first, but it's super cool. In this case, we want to JOIN across this fortuneCookies property over to the FortuneCookie entity.

Using leftJoin()

Let's do it! Back over in CategoryRepository... we can add the join anywhere in the query. Unlike SQL, the QueryBuilder doesn't care what order you do things. Add ->leftJoin() because we're joining from one category to many fortune cookies. Pass this category.fortuneCookies then fortuneCookie, which will be the alias for the joined entity.

... lines 1 - 17
class CategoryRepository extends ServiceEntityRepository
... lines 20 - 40
public function search(string $term): array
return $this->createQueryBuilder('category')
->leftJoin('category.fortuneCookies', 'fortuneCookie')
... lines 45 - 49
... lines 51 - 93

When we say category.fortuneCookies, we're referring to the fortuneCookies property. The cool thing is that... this is all we need! We don't need to tell Doctrine which entity or table we're joining to... and we don't need the ON fortune_cookie.category_id = category.id that we would normally see in SQL. We don't need any of this because Doctrine already has that info on the OneToMany mapping. We just say "join across this property" and it does the rest!

One thing to keep in mind, which we'll talk more about in a minute, is that, by joining over to something, we're not selecting more data. We're just making the properties on FortuneCookie available inside our query. This means we can make the ->andWhere() even longer. Add OR fortuneCookie (using the new alias from the join) .fortune (because fortune is the name of the property on FortuneCookie that stores the text) LIKE :searchTerm.

... lines 1 - 40
public function search(string $term): array
return $this->createQueryBuilder('category')
... line 44
->andWhere('category.name LIKE :searchTerm OR category.iconKey LIKE :searchTerm OR fortuneCookie.fortune LIKE :searchTerm')
... lines 46 - 49
... lines 51 - 95

Done! Head back to the site. One of my fortunes has the word "conclusion". Spin over to the homepage, search for "conclusion" and... got it! It looks like we have at least one match in our "Proverbs" category! Missing accomplished!

But if you click on the database icon of the web debug toolbar... this page has two queries. The first is for the category - it has FROM category and includes the LEFT JOIN we just added. The second is FROM fortune_cookie.

And if we go to the homepage without searching, there are seven queries in total: one to fetch all the categories... and then an additional 6 to find the fortune cookies for each of the six categories. This is called the N+1 query problem. Let's talk about it next and fix it with joins.

Leave a comment!

Login or Register to join the conversation
Daniyal-J Avatar
Daniyal-J Avatar Daniyal-J | posted 16 days ago

But how can I create a one-to-many relationship in the first place?


Hey Daniyal,

We don't focus too much on how to create relationships in this tutorial because this tutorial is about a different topic, specifically, how to work with Doctrine queries. So here we're mostly working on an existent DB structure. You can spy on how the relationship is created in the entity mapping, but if you want to learn more on how to create it - take a look at e.g.: https://symfonycasts.com/screencast/symfony3-doctrine-relations/many-to-one-relation - or try our advanced search to find more related content. For example, we talk a lot about Doctrine and its relationships in our latest Symfony Doctrine course: https://symfonycasts.com/screencast/symfony-doctrine


1 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": "*",
        "beberlei/doctrineextensions": "^1.3", // v1.3.0
        "doctrine/doctrine-bundle": "^2.7", // 2.9.1
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.15.1
        "symfony/asset": "6.2.*", // v6.2.7
        "symfony/console": "6.2.*", // v6.2.10
        "symfony/dotenv": "6.2.*", // v6.2.8
        "symfony/flex": "^2", // v2.2.5
        "symfony/framework-bundle": "6.2.*", // v6.2.10
        "symfony/proxy-manager-bridge": "6.2.*", // v6.2.7
        "symfony/runtime": "6.2.*", // v6.2.8
        "symfony/twig-bundle": "6.2.*", // v6.2.7
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.10
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/maker-bundle": "^1.47", // v1.48.0
        "symfony/stopwatch": "6.2.*", // v6.2.7
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.10
        "zenstruck/foundry": "^1.22" // v1.32.0