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 SubscribeHead back to our form. We have a field called studiedGenuses
:
... lines 1 - 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 | |
} |
Because all of our properties are private, the form component works by calling the setter method for each field. I mean, when we submit, it takes the submitted email and calls setEmail()
on User
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 30 | |
private $email; | |
... lines 32 - 137 | |
public function setEmail($email) | |
{ | |
$this->email = $email; | |
} | |
... lines 142 - 222 | |
} |
But wait... we do have a field called studiedGenuses
... but we do not have a setStudiedGenuses
method:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 81 | |
private $studiedGenuses; | |
... lines 83 - 222 | |
} |
Shouldn't the form component be throwing a huge error about that?
In theory... yes! But, the form is being really sneaky. Remember, the studiedGenuses
property is an ArrayCollection
object:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 81 | |
private $studiedGenuses; | |
public function __construct() | |
{ | |
$this->studiedGenuses = new ArrayCollection(); | |
} | |
... lines 88 - 215 | |
/** | |
* @return ArrayCollection|Genus[] | |
*/ | |
public function getStudiedGenuses() | |
{ | |
return $this->studiedGenuses; | |
} | |
} |
When the form is building, it calls getStudiedGenuses()
so that it knows which checkboxes to check. Then on submit, instead of trying to call a setter, it simply modifies that ArrayCollection
. Basically, since ArrayCollection
is an object, the form realizes it can be lazy: it adds and removes genuses directly from the object, but never sets it back on User
. It doesn't need to, because the object is linked to the User
by reference.
This ultimately means that our studiedGenuses
property is being updated like we expected... just in a fancy way.
So... why should we care? We don't really... except that by disabling this fancy functionality, we will uncover a way to fix all of our problems.
How? Add a new option to the field: by_reference
set to false
:
... lines 1 - 14 | |
class UserEditForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
... lines 20 - 24 | |
->add('studiedGenuses', EntityType::class, [ | |
... lines 26 - 29 | |
'by_reference' => false, | |
]) | |
; | |
} | |
... lines 34 - 40 | |
} |
It says:
Stop being fancy! Just call the setter method like normal!
Go refresh the form, and submit!
Ah! It's yelling at us! This is the error we expected all along:
Neither the property
studiedGenuses
nor one of the methods - and then it lists a bunch of potential methods, includingsetStudiedGenuses()
- exist and have public access in theUser
class.
In less boring terms, the form system is trying to say:
Hey! I can't set the
studiedGenuses
back onto theUser
object unless you create one of these public methods!
So, should we create a setStudiedGenuses()
method like it suggested? Actually, no. Another option is to create adder & remover methods.
Create a public function addStudiedGenus()
with a Genus
argument:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 223 | |
public function addStudiedGenus(Genus $genus) | |
{ | |
... lines 226 - 230 | |
} | |
... lines 232 - 236 | |
} |
Here, we'll do the same type of thing we did back in our Genus
class: if $this->studiedGenuses->contains($genus)
, then do nothing. Otherwise $this->studiedGenuses[] = $genus
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 223 | |
public function addStudiedGenus(Genus $genus) | |
{ | |
if ($this->studiedGenuses->contains($genus)) { | |
return; | |
} | |
$this->studiedGenuses[] = $genus; | |
} | |
... lines 232 - 236 | |
} |
After that, add the remover: public function removeStudiedGenus()
also with a Genus
argument. In here, say $this->studiedGenuses->removeElement($genus)
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 231 | |
public function removeStudiedGenus(Genus $genus) | |
{ | |
$this->studiedGenuses->removeElement($genus); | |
} | |
} |
Perfect!
Go back to the form. Uncheck one of the genuses and check a new one. When we submit, it should call addStudiedGenus()
once for the new checkbox and removeStudiedGenus()
once for the box we unchecked.
Ok, hit update! Hmm, it looked successful... but it still didn't actually work. And that's expected! We just setup a cool little system where the form component calls our adder and remover methods to update the studiedGenuses
property. But... this hasn't really changed anything: we're still not setting the owning side of the relationship.
But, we're just one small step from doing that.
Hey ahmedbhs!
1) So when we have a ManyToMany relation always we should add
by_reference
to our EntityType field ?
Yes... but it depends :p. What I mean is, adding by_reference is probably always a safe option. It would just mean that the form component will call your adder/remover methods instead of modifying (for example) the studiedGenuses object directly. From a PHP perspective, the end result in both cases is really the same: the studiedGenuses will be updated in whatever way is needed. The big difference is that, by calling your adder/remover, the code in those methods can update the other side of the relation (thanks to how make:entity generates those methods).
So, by_reference is probably always a good idea. However, it's only technically needed if you're updating the "inverse" side of a ManyToMany relationship. For example, suppose you have Product that is ManyToMany with Tags. And suppose that the Product.tags is the "owning" side of that relationship. If you have a ProductFormType with a "tags" field that is an EntityType, you would not need to set by_reference to false on that field. The reason is that, on save, Doctrine looks at the Product.tags field to figure out what to do, and that will be updated correctly regardless of the value for by_reference. But if the owning side of the relationship is Tag.products, then you must us by_reference so that the adder/remover methods on Product are called (and these methods then update the Tag object correctly).
Let me know if that makes any sense at all :p
2) Same behavior for ChoiceType and CollectionType?
If you use ChoiceType with Doctrine entities then yes. But usually ChoiceType is handling scalar values, which can't be updated by reference, and so the setter method is always called anyways.
For the CollectionType, this is only a concern if you "allow add" or "allow remove". But it's the same answer as part (1): it's probably always a good idea, but only needed if your form is trying to update the inverse side of the relationship.
3) Normally even without setting by reference to false, the form builder should modify studied genius, because it's already setted to a new arrayCollection via the constructer !
Yes! If you do NOT set 'by_reference'=>false
for a "collection" field like this, then it defaults to true
. This means that the ArrayCollection object is modified directly without every calling a setter/adder/remove method. In fact, iirc, you could even delete those methods (only have a getter) and things would still work. But, this is not what we want if our form is updating the inverse side of a Doctrine relation: we do want the adder/remove to be called so we can sync the owning side.
Let me know if this helps - it's a complex topic to think about1
Cheers!
Hi Ryan, I am running into following error. As soon I set by_reference to false I get "Could not determine access type for property myvarname. myvarname would be in your example the $genusScientists. Adder/Remover-Methods are there too.
Oh man ... Symfony is a great framework but I mention it is too much influenced by our most lovely human beings: womans :-)
The error was caused because Symfony did not found any adder/remover-method even though it exists (and in a singular form)!
To
find the methods our girl symfony is using the PropertyAccessor -
class. It searches for singular forms of the varname and check if this
varname exists. If not BAM! (%**!"!!(/)!/")!). It throws an NoSuchPropertyException (Could not determine access type for property))
I checked with a dump how symfony is thinking the singular form should be. For this I hacked into
vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php (Method findAdderAndRemover)
Because my varname was not good enough for my lovely symfony I decided to rename it and now it works :)
I met the same situation too.
When I named the variable $studiedGenuses as $studiedGenus
I create the function addStudiedGenus() and removeStudiedGenus()
It threw the exception to me
'Could not determine access type for property "studiedGenus".'
When I changed it back to studiedGenuses . It worked.
Hey 黃俊凱
Symfony has some problems detecting words in plural, I hope in the future it will get better but right now, we have to make him happy and adjust the name of our accessors/variables
Cheers!
Hey Thomas,
I'm glad you got it working so quickly! Really, when you set by_reference to false you have to add adder/remover method, otherwise Symfony can set a value for those properties.
Cheers!
You are right but Symfony simply was not "seeing" my adder/remover-methods dispite they exists. This was caused by the weird Singular-thing.
Ah, now I see. Really, it should be singular form. Just wonder what property name did you use? "varname"?
Cheers!
Yeah, you have to add addBosiContract()/removeBosiContract() methods for this property.
Cheers!
Hm, then I'm confused... How did you solve it? What exactly adder/remover method names are you using? :)
Cheers!
Hey Ryan, I got the same error in Symfony 3.1.10 when using the studiedGenuses property name:
"Could not determine access type for property "studiedGenuses"
But after adding the "addStudiedGenus" and "removeStudiedGenus" functions it works.
Hey Dominik,
Yeah, with addStudiedGenus/removeStudiedGenus it should work well - you just have to look closer and do not make a misprint. :)
Thanks for sharing it!
Cheers!
Hi guys,
Just a short comment to avoid some of you loose time. At the end of this video, I could not make the check box retain the selection I made. After spending 20min comparing the codes and trying to figure out what's wrong (hard when no errors are displayed), I continued to the next video and the two line of codes to sync the entities fixed the problem.
cheers
Hi Guys:
If I want to be super lazy (PHPstorm can auto create setter and getter)without using Adder and Remover and use setter and getter, would that works?
Hey Jian,
It depends: if you do not use "'by_reference' => false", then setters/getters will be enough for you, otherwise, you'll get fatal error due to the thrown exception if adder/remover won't be found.
Cheers!
And by_reference
is important! Because we do want those adder and removers to be called: it's our opportunity to make sure the owning and inverse sides of the relationships are all set correctly :).
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
}
}
1) So when we have a ManyToMany relation always we should add `
`by_reference` to our EntityType field ?
2) Same behavior for ChoiceType and CollectionType?
3) Normally even without setting by reference to false, the form builder should modify studied genius, because it's already setted to a new arrayCollection via the constructer !