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 SubscribeBack on the main part of the site, click one of the genuses, and then click one of the users that studies it. This page has a little edit button: click that. Welcome to a very simple User
form.
Ok, same plan: add checkboxes so that I can choose which genuses are being studied by this User
. Open the controller: UserController
and find editAction()
:
... lines 1 - 5 | |
use AppBundle\Form\UserEditForm; | |
... lines 7 - 11 | |
class UserController extends Controller | |
{ | |
... lines 14 - 57 | |
public function editAction(User $user, Request $request) | |
{ | |
$form = $this->createForm(UserEditForm::class, $user); | |
... lines 61 - 78 | |
} | |
} |
This uses UserEditForm
, so go open that as well.
In buildForm()
, we'll do the exact same thing we did on the genus form: add a new field called studiedGenuses
- that's the property name on User
that we want to modify:
... lines 1 - 6 | |
use Symfony\Bridge\Doctrine\Form\Type\EntityType; | |
... lines 8 - 14 | |
class UserEditForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
... lines 20 - 24 | |
->add('studiedGenuses', EntityType::class, [ | |
... lines 26 - 29 | |
]) | |
; | |
} | |
... lines 33 - 39 | |
} |
Keep going: use EntityType::class
and then set the options: class
set now to Genus::class
to make Genus
checkboxes. Then, multiple
set to true
, expanded
set to true
, and choice_label
set to name
to display that field from Genus
:
... lines 1 - 6 | |
use Symfony\Bridge\Doctrine\Form\Type\EntityType; | |
... lines 8 - 14 | |
class UserEditForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
... lines 20 - 24 | |
->add('studiedGenuses', EntityType::class, [ | |
'class' => Genus::class, | |
'multiple' => true, | |
'expanded' => true, | |
'choice_label' => 'name', | |
]) | |
; | |
} | |
... lines 33 - 39 | |
} |
Next! Open the template: user/edit.html.twig
. At the bottom, use form_row(userForm.studiedGenuses)
:
... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-8"> | |
... lines 7 - 8 | |
{{ form_start(userForm) }} | |
... lines 10 - 16 | |
{{ form_row(userForm.studiedGenuses) }} | |
... lines 18 - 19 | |
{{ form_end(userForm) }} | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
That's it.
Try it! Refresh! Cool! This User
is studying five genuses: good for them! Let's uncheck one genus, check a new one and hit Update.
Wait! It didn't work! The checkboxes just reverted back! What's going on!?
This is the moment where someone who doesn't know what we're about to learn, starts to hate Doctrine relations.
Earlier, we talked about how every relationship has two sides. You can start with a Genus
and talk about the genus scientist users related to it:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses", fetch="EXTRA_LAZY") | |
* @ORM\JoinTable(name="genus_scientist") | |
*/ | |
private $genusScientists; | |
... lines 77 - 195 | |
} |
Or, you can start with a User
and talk about its studied genuses:
... 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 | |
} |
Only one of these side - in this case the Genus
- is the owning side. So far, that hasn't meant anything: we can easily read data from either direction. BUT! The owning side has one special power: it is the only side that you're allowed to change.
What I mean is, if you have a User
object and you add or remove genuses from its studiedGenuses
property and save... Doctrine will do nothing. Those changes are completely ignored.
And it's not a bug! Doctrine is built this way on purpose. The data about which Genuses are linked to which Users is stored in two places. So Doctrine needs to choose one of them as the official source when it saves. It uses the owning side.
For a ManyToMany
relationship, we chose the owning side when we set the mappedBy
and inversedBy
options. The owning side is also the only side that's allowed to have the @ORM\JoinTable
annotation.
This is a long way of saying that if we want to update this relationship, we must add and remove users from the $genusScientists
property on Genus
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses", fetch="EXTRA_LAZY") | |
* @ORM\JoinTable(name="genus_scientist") | |
*/ | |
private $genusScientists; | |
... lines 77 - 195 | |
} |
Adding and removing genuses from the User
object will do nothing. And that's exactly what our form just did.
No worries! We can fix this, with just a little bit of really smart code.
// 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
}
}