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 SubscribeI have one last cool trick to show you. Go back to /genus
.
Oh, but real quick, I need to fix two little things that I messed up before we finish.
inversedBy()
First, see that red label on the web debug toolbar? Click it, and scroll down. It's a mapping warning:
The field
User#studiedGenuses
property is on the inverse side of a bidirectional relationship, but the association on blah-blah-blah does not contain the requiredinversedBy()
.
In human-speak, this says that my User
correctly has a studiedGenuses
property with a mappedBy
option...
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 77 | |
/** | |
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="user") | |
*/ | |
private $studiedGenuses; | |
... lines 82 - 221 | |
} |
But on GenusScientist
, I forgot to add the inversedBy()
that points back to this:
... lines 1 - 17 | |
class GenusScientist | |
{ | |
... lines 20 - 32 | |
/** | |
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses") | |
... line 35 | |
*/ | |
private $user; | |
... lines 38 - 78 | |
} |
I don't really know why Doctrine requires this... since it didn't seem to break anything, but hey! This fixes the warning.
The second thing I need to fix is this yearsStudied
field. When PhpStorm generated the annotation for us, it used type="string"
... and I forgot to fix it! Change it to type="integer"
:
... lines 1 - 17 | |
class GenusScientist | |
{ | |
... lines 20 - 38 | |
/** | |
* @ORM\Column(type="integer") | |
... line 41 | |
*/ | |
private $yearsStudied; | |
... lines 44 - 78 | |
} |
It hasn't caused a problem yet... but it would if we tried to do some number operations on it inside the database.
Of course, we need a migration!
./bin/console doctrine:migrations:diff
Just trust that it's correct - live dangerously:
./bin/console doctrine:migrations:migrate
Sweet! Now go back to /genus
.
We're already printing the number of scientists that each Genus
has. And thanks to a fancy query we made inside GenusRepository
, that joins over and fetches the related User
data all at once... this entire page is built with one query:
... lines 1 - 7 | |
class GenusRepository extends EntityRepository | |
{ | |
/** | |
* @return Genus[] | |
*/ | |
public function findAllPublishedOrderedByRecentlyActive() | |
{ | |
return $this->createQueryBuilder('genus') | |
->andWhere('genus.isPublished = :isPublished') | |
->setParameter('isPublished', true) | |
->leftJoin('genus.notes', 'genus_note') | |
->orderBy('genus_note.createdAt', 'DESC') | |
->leftJoin('genus.genusScientists', 'genusScientist') | |
->addSelect('genusScientist') | |
->getQuery() | |
->execute(); | |
} | |
} |
Well, except for the query that loads my security user from the database.
So this is cool! Well, its maybe cool - as we talked about earlier, this is fetching a lot of extra data. And more importantly, this page may not be a performance problem in the first place. Anyways, I want to show you something cool, so comment out those joins:
... lines 1 - 7 | |
class GenusRepository extends EntityRepository | |
{ | |
... lines 10 - 12 | |
public function findAllPublishedOrderedByRecentlyActive() | |
{ | |
return $this->createQueryBuilder('genus') | |
->andWhere('genus.isPublished = :isPublished') | |
->setParameter('isPublished', true) | |
->leftJoin('genus.notes', 'genus_note') | |
->orderBy('genus_note.createdAt', 'DESC') | |
// ->leftJoin('genus.genusScientists', 'genusScientist') | |
// ->addSelect('genusScientist') | |
->getQuery() | |
->execute(); | |
} | |
} |
Refresh again! Our one query became a bunch! Every row now has a query, but it's a really efficient COUNT query thanks to our fetch EXTRA_LAZY
option:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany( | |
... lines 74 - 75 | |
* fetch="EXTRA_LAZY", | |
... lines 77 - 78 | |
* ) | |
... line 80 | |
*/ | |
private $genusScientists; | |
... lines 83 - 209 | |
} |
Here's my new wild idea: any scientist that has studied a genus for longer than 20 years should be considered an expert. So, in addition to the number of scientists I also want to print the number of expert scientists next to it.
Look inside the list template: we're printing this number by saying genus.genusScientists|length
:
... lines 1 - 2 | |
{% block body %} | |
<table class="table table-striped"> | |
... lines 5 - 12 | |
<tbody> | |
{% for genus in genuses %} | |
<tr> | |
... lines 16 - 21 | |
<td>{{ genus.genusScientists|length }}</td> | |
... line 23 | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
{% endblock %} |
In other words, call getGenusScientists()
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 202 | |
/** | |
* @return ArrayCollection|GenusScientist[] | |
*/ | |
public function getGenusScientists() | |
{ | |
return $this->genusScientists; | |
} | |
} |
Fetch the results, and then count them:
But how could we filter this to only return GenusScientist
results that have studied the Genus
for longer than 20 years?
It's easy! In Genus
, create a new public function called getExpertScientists()
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 213 | |
public function getExpertScientists() | |
{ | |
... lines 216 - 218 | |
} | |
} |
Then, we'll loop over all of the scientists to find the experts. And actually, we can do that very easily by saying $this->getGenusScientists()->filter()
, which is a method on the ArrayCollection
object. Pass that an anonymous function with a GenusScientist
argument. Inside, return $genusScientist->getYearsStudied() > 20
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 213 | |
public function getExpertScientists() | |
{ | |
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) { | |
return $genusScientist->getYearsStudied() > 20; | |
}); | |
} | |
} |
This will loop over all of the genus scientists and return a new ArrayCollection
that only contains the ones that have studied for more than 20 years. It's perfect!
To print this in the template, let's add a new line, then {{ genus.expertScientists|length }}
and then "experts":
... lines 1 - 2 | |
{% block body %} | |
<table class="table table-striped"> | |
... lines 5 - 12 | |
<tbody> | |
{% for genus in genuses %} | |
<tr> | |
... lines 16 - 21 | |
<td> | |
{{ genus.genusScientists|length }} | |
({{ genus.expertScientists|length }} experts) | |
</td> | |
... line 26 | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
{% endblock %} |
Try it! Refresh! Zero! What!? Oh... I forgot my return
statement from inside the filter function. Lame!
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 213 | |
public function getExpertScientists() | |
{ | |
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) { | |
return $genusScientist->getYearsStudied() > 20; | |
}); | |
} | |
} |
Try it now. Yes!
Click to check out the queries. It still makes a COUNT query for each row... but wait: it also queries for all of the genus_scientist
results for each genus
. That sucks! Even if a Genus
only has two experts... we're fetching all of the data for all of its genus scientists.
Why? Well, as soon as we loop over genusScientists
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 213 | |
public function getExpertScientists() | |
{ | |
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) { | |
return $genusScientist->getYearsStudied() > 20; | |
}); | |
} | |
} |
Doctrine realizes that it needs to go and query for all of the genus scientists for this Genus
. Then, we happily loop over them to see which ones have more than 20 yearsStudied
.
This may or may not be a huge performance problem. If every Genus
always has just a few scientists, no big deal! But if a Genus
has hundreds of scientists, this page will grind to a halt while it queries for and hydrates all of those extra GenusScientist
objects.
There's a better way: and it uses a feature in Doctrine that - until recently - even I didn't know existed. And I'm super happy it does.
Hey Peter K.
Yeah you have a tricky situation. As I know this cannot be done automatically because this fields are not synced, to sync them you can create Doctrine entity listener, and listen to PreUpdate and probable PrePersist and check your list changes and sync lists.
Hope this will help. Cheers!
Awesome! This is one of real super powers of Symfony! You choose the way you want to code =)
Cheers!
Hey Michael,
Ah, great catch! Thank you for pointing it out! It was fixed in https://github.com/knpunive...
Cheers!
Great, I didn't know the course content was available on gitub. I'm not familiar with the github [[[ code('..') ]]], how is that set up? (Searching for 'github code' gets everything!). Is it a gist? Or something you tag in the code somehow?
Hey Michael,
Yeah, that's a little-known fact. You may even notice a pencil icon on course chapter pages in the right top corner of scripts. If you press it - it will move you to the corresponding page on GitHub.
About that [[[ code('..') ]]] syntax - it's our own, we implemented it internally. It is dynamically replaced with actual code blocks, but we use an internal tool that helps to display specific code on different steps. It helps us to show the *actual* code on chapter pages - no more outdated static code blocks. And that's the one of our coolest features on SymfonyCasts. :)
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"symfony/symfony": "3.4.*", // v3.4.49
"doctrine/orm": "^2.5", // 2.7.5
"doctrine/doctrine-bundle": "^1.6", // 1.12.13
"doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.23.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.47
"nelmio/alice": "^2.1", // v2.3.6
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}
I am struggling with my scenario:
Imagine you have a Teacher, Subject, Room entities.
Teacher can teach many Subjects
Teacher can teach in many Rooms
Subject can be tough in many Rooms so rooms and subjects are linked but 1 Room can have 1 subject. OneToMany
Now imagine I want to create a Teacher so I will select Subjects(teacherSubjects = arrayCollection)
After subjects have been added I can pre-filter Rooms available and store them (teacherRooms = arrayCollection)
Now I will remove subject from the removeSubject and click save.
I want automatically remove all associated Rooms from teacherRooms.
I tried to do it inside removeSubject but when form is submitted it is not even triggering this method same is happening when I am adding new subject method addSubject is not hit.
I know I could probably solve this if I decide to set this field as not mapped but I would assume this can be done automatically.