Using the new OneToMany Collections

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

Open up the genus/show.html.twig template. Actually, let's start in the Genus class itself. Find getGenusScientists():

... lines 1 - 14
class Genus
{
... lines 17 - 195
/**
* @return ArrayCollection|User[]
*/
public function getGenusScientists()
{
return $this->genusScientists;
}
}

This method is lying! It does not return an array of User objects, it returns an array of GenusScientist objects!

... lines 1 - 14
class Genus
{
... lines 17 - 195
/**
* @return ArrayCollection|GenusScientist[]
*/
public function getGenusScientists()
{
return $this->genusScientists;
}
}

In the template, when we loop over genus.genusScientists, genusScientist is not a User anymore. Update to genusScientist.user.fullName, and above, for the user_show route, change this to genusScientist.user.id:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 21
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item js-scientist-item">
<a href="{{ path('user_show', {
'id': genusScientist.user.id
}) }}">
{{ genusScientist.user.fullName }}
... line 30
</a>
... lines 32 - 41
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 51 - 92

Then, in the link, let's show off our new yearsStudied field: {{ genusScientist.yearsStudied }} then years:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 21
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item js-scientist-item">
<a href="{{ path('user_show', {
'id': genusScientist.user.id
}) }}">
{{ genusScientist.user.fullName }}
({{ genusScientist.yearsStudied }} years)
</a>
... lines 32 - 41
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 51 - 92

We still need to fix the remove link, but let's see how it looks so far!

Refresh! It's way less broken! Well, until you click to view the user!

Updating the User Template

To fix this, start by opening User and finding getStudiedGenuses(). Change the PHPDoc to advertise that this now returns an array of GenusScientist objects:

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 214
/**
* @return ArrayCollection|GenusScientist[]
*/
public function getStudiedGenuses()
{
return $this->studiedGenuses;
}
... lines 222 - 241
}

Next, go fix the template: user/show.html.twig. Hmm, let's rename this variable to be a bit more clear: genusScientist, to match the type of object it is. Now, update slug to be genusScientist.genus.slug. And print genusScientist.genus.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 genusScientist in user.studiedGenuses %}
<li class="list-group-item">
<a href="{{ path('genus_show', {
'slug': genusScientist.genus.slug
}) }}">
{{ genusScientist.genus.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

Try it! Page is alive!

Back on the genus page, the other thing we need to fix is this remove link. In the show.html.twig template for genus, update the userId part of the URL: genusScientist.user.id:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 21
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item js-scientist-item">
... lines 26 - 32
<a href="#"
class="btn btn-link btn-xs pull-right js-remove-scientist-user"
data-url="{{ path('genus_scientists_remove', {
genusId: genus.id,
userId: genusScientist.user.id
}) }}"
>
<span class="fa fa-close"></span>
</a>
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 51 - 92

Next, find this endpoint in GenusController: removeGenusScientistAction():

... lines 1 - 14
class GenusController extends Controller
{
... lines 17 - 126
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
if (!$genus) {
throw $this->createNotFoundException('genus not found');
}
$genusScientist = $em->getRepository('AppBundle:User')
->find($userId);
if (!$genusScientist) {
throw $this->createNotFoundException('scientist not found');
}
$genus->removeGenusScientist($genusScientist);
$em->persist($genus);
$em->flush();
return new Response(null, 204);
}
}

It's about to get way nicer. Kill the queries for Genus and User. Replace them with $genusScientist = $em->getRepository('AppBundle:GenusScientist') and findOneBy(), passing it user set to $userId and genus set to $genusId:

... lines 1 - 14
class GenusController extends Controller
{
... lines 17 - 126
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
$genusScientist = $em->getRepository('AppBundle:GenusScientist')
->findOneBy([
'user' => $userId,
'genus' => $genusId
]);
... lines 136 - 140
}
}

Then, instead of removing this link from Genus, we simply delete the entity: $em->remove($genusScientist):

... lines 1 - 14
class GenusController extends Controller
{
... lines 17 - 126
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
$genusScientist = $em->getRepository('AppBundle:GenusScientist')
->findOneBy([
'user' => $userId,
'genus' => $genusId
]);
$em->remove($genusScientist);
$em->flush();
return new Response(null, 204);
}
}

And celebrate!

Go try it! Quick, delete that scientist! It disappears in dramatic fashion, and, when we refresh, it's definitely gone.

Phew! We're almost done. By the way, you can see that this refactoring takes some work. If you know that your join table will probably need extra fields on it, you can save yourself this work by setting up the join entity from the very beginning and avoiding ManyToMany. But, if you definitely won't have extra fields, ManyToMany is way nicer.

Updating the Fixtures

The last thing to fix is the fixtures. We won't set the genusScientists property up here anymore. Instead, scroll down and add a new AppBundle\Entity\GenusScientist section:

... lines 1 - 38
AppBundle\Entity\GenusScientist:
... lines 40 - 44

It's simple: we'll just build new GenusScientist objects ourselves, just like we did via newAction() in PHP code earlier. Add genus.scientist_{1..50} to create 50 links. Then, assign user to a random @user.aquanaut_* and genus to a random @genus_*. And hey, set yearsStudied to something random too: <numberBetween(1, 30)>:

... lines 1 - 38
AppBundle\Entity\GenusScientist:
genus.scientist_{1..50}:
user: '@user.aquanaut_*'
genus: '@genus_*'
yearsStudied: <numberBetween(1, 30)>

Nice! Go find your terminal and reload!

./bin/console doctrine:fixtures:load

Ok, go back to /genus... and click one of them. We have scientists!

So our app is fixed, right? Well, not so fast. Go to /admin/genus: you might need to log back in - password iliketurtles. Our genus form is still totally broken. Ok, no error: but it doesn't even make sense anymore: our relationship is now more complex than checkboxes can handle. For example, how would I set the yearsStudied?

Time to take this form up a level.

Leave a comment!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// 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
        "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
    }
}