If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Our goal is clear: list all of the genuses studied by this User
.
Back in our Doctrine Relations tutorial, we learned that every
relationship has two different sides: a mapping, or owning side, and an inverse side.
In that course, we added a GenusNote
entity and gave it a ManyToOne
relationship
to Genus
:
... lines 1 - 10 | |
class GenusNote | |
{ | |
... lines 13 - 39 | |
/** | |
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="notes") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $genus; | |
... lines 45 - 99 | |
} |
This is the owning side, and it's the only one that we actually needed to create.
If you look in Genus
, we also mapped the other side of this relationship: a OneToMany
back to GenusNote
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 65 | |
/** | |
* @ORM\OneToMany(targetEntity="GenusNote", mappedBy="genus") | |
* @ORM\OrderBy({"createdAt" = "DESC"}) | |
*/ | |
private $notes; | |
... lines 71 - 187 | |
} |
This is the inverse side of the relationship, and it's optional. When we mapped
the inverse side, it caused no changes to our database structure. We added
it purely for convenience, because we decided it sure would be fancy and nice
if we could say $genus->getNotes()
to automagically fetch all the GenusNotes
for this Genus
.
With a ManyToOne
relationship, we don't choose which side is which: the ManyToOne
side is always the required, owning side. And that makes sense, it's the table
that holds the foreign key column, i.e. GenusNote
has a genus_id
column.
We can also look at our ManyToMany
relationship in two different directions.
If I have a Genus
object, I can say:
Hello fine sir: please give me all Users related to this Genus.
But if I have a User
object, I should also be able to say the opposite:
Good evening madame: I would like all Genuses related to this User.
The tricky thing about a ManyToMany
relationship is that you get to choose
which side is the owning side and which is the inverse side. And, I hate choices!
The choice does have consequences.... but don't worry about that - we'll learn
why soon.
Since we only have one side of the relationship mapped now, it's the owning side.
To map the inverse side, open User
and add a new property: $studiedGenuses
.
This will also be a ManyToMany
with targetEntity
set to Genus
. But also add
mappedBy="genusScientists
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 77 | |
/** | |
* @ORM\ManyToMany(targetEntity="Genus", mappedBy="genusScientists") | |
*/ | |
private $studiedGenuses; | |
... lines 82 - 221 | |
} |
That refers to the property inside of Genus
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\ManyToMany(targetEntity="User") | |
* @ORM\JoinTable(name="genus_scientist") | |
*/ | |
private $genusScientists; | |
... lines 77 - 190 | |
} |
Now, on that property, add inversedBy="studiedGenuses
, which points back
to the property we just added in User
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses") | |
* @ORM\JoinTable(name="genus_scientist") | |
*/ | |
private $genusScientists; | |
... lines 77 - 190 | |
} |
When you map both sides of a ManyToMany
relationship, this mappedBy
and inversedBy
configuration is how you tell Doctrine which side is which. We don't really know
why that's important yet, but we will soon.
Back in User
, remember that whenever you have a relationship that holds a collection
of objects, like a collection of "studied genuses", you need to add a __construct
function and initialize that to a new ArrayCollection()
:
... lines 1 - 4 | |
use Doctrine\Common\Collections\ArrayCollection; | |
... lines 6 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 82 | |
public function __construct() | |
{ | |
$this->studiedGenuses = new ArrayCollection(); | |
} | |
... lines 87 - 221 | |
} |
Finally, since we'll want to be able to access these studiedGenuses
, go to the
bottom of User
and add a new public function getStudiedGenuses()
. Return that
property inside. And of course, we love PHP doc, so add @return ArrayCollection|Genus[]
:
... lines 1 - 4 | |
use Doctrine\Common\Collections\ArrayCollection; | |
... lines 6 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 214 | |
/** | |
* @return ArrayCollection|Genus[] | |
*/ | |
public function getStudiedGenuses() | |
{ | |
return $this->studiedGenuses; | |
} | |
} |
And just by adding this new property, we are - as I love to say - dangerous.
Head into the user/show.html.twig
template that renders the page we're looking
at right now. Add a column on the right side of the page, a little "Genuses Studied"
header, then a ul
. To loop over all of the genuses that this user is studying, just
say for genusStudied in user.studiedGenuses
. Don't forget the endfor
:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
... lines 6 - 38 | |
<div class="col-xs-4"> | |
<h3>Genus Studied</h3> | |
<ul class="list-group"> | |
{% for genusStudied in user.studiedGenuses %} | |
... lines 43 - 49 | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Inside, add our favorite list-group-item
and then a link. Link this back to
the genus_show
route, passing slug
set to genusStudied.slug
. Print out
genusStudied.name
:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
... lines 6 - 38 | |
<div class="col-xs-4"> | |
<h3>Genus Studied</h3> | |
<ul class="list-group"> | |
{% for genusStudied in user.studiedGenuses %} | |
<li class="list-group-item"> | |
<a href="{{ path('genus_show', { | |
'slug': genusStudied.slug | |
}) }}"> | |
{{ genusStudied.name }} | |
</a> | |
</li> | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
But will it blend? I mean, will it work? Refresh!
Hot diggity dog! There are the three genuses that this User
studies. We did
nothing to deserve this nice treatment: Doctrine is doing all of the query work for
us.
In fact, click the database icon on the web debug toolbar to see what the query looks
like. When we access the property, Doctrine does a SELECT
from genus
with an
INNER JOIN
to genus_scientist
where genus_scientist.user_id
equals this User's
id: 11. That's perfect! Thanks Obama!
The only bummer is that we can't control the order of the genuses. What if we want to list them alphabetically? We can't - we would instead need to make a custom query for the genuses in the controller, and pass them into the template.
What? Just kidding! In User
, add another annotation: @ORM\OrderBy({"name" = "ASC")
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 77 | |
/** | |
* @ORM\ManyToMany(targetEntity="Genus", mappedBy="genusScientists") | |
* @ORM\OrderBy({"name" = "ASC"}) | |
*/ | |
private $studiedGenuses; | |
... lines 83 - 222 | |
} |
Refresh that!
If you didn't see a difference, you can double-check the query to prove it. Boom!
There's our new ORDER BY
. Later, I'll show you how you can mess with the query
made for collections even more via Doctrine Criteria.
But up next, the last missing link: what if a User
stops studying a Genus
?
How can we remove that link?
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
"stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}