Buy

Head back to our form. We have a field called studiedGenuses:

41 lines src/AppBundle/Form/UserEditForm.php
... 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:

224 lines src/AppBundle/Entity/User.php
... 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:

224 lines src/AppBundle/Entity/User.php
... 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?

The by_reference Form Option

In theory... yes! But, the form is being really sneaky. Remember, the studiedGenuses property is an ArrayCollection object:

224 lines src/AppBundle/Entity/User.php
... 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:

42 lines src/AppBundle/Form/UserEditForm.php
... 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!

The Adder and Remover Methods

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, including setStudiedGenuses() - exist and have public access in the User class.

In less boring terms, the form system is trying to say:

Hey! I can't set the studiedGenuses back onto the User 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:

238 lines src/AppBundle/Entity/User.php
... 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:

238 lines src/AppBundle/Entity/User.php
... 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):

238 lines src/AppBundle/Entity/User.php
... 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.

Leave a comment!

  • 2017-05-08 jian su

    Thank you Ryan! u guys reply the comment so frequently which is awesome

  • 2017-05-08 weaverryan

    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!

  • 2017-05-08 Victor Bocharsky

    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!

  • 2017-05-07 jian su

    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?

  • 2017-04-24 Victor Bocharsky

    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!

  • 2017-04-23 Dominik

    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.

  • 2017-03-30 Victor Bocharsky

    Hm, then I'm confused... How did you solve it? What exactly adder/remover method names are you using? :)

    Cheers!

  • 2017-03-30 Thomas

    ... but Symfony did not recognized them.

  • 2017-03-29 Victor Bocharsky

    Yeah, you have to add addBosiContract()/removeBosiContract() methods for this property.

    Cheers!

  • 2017-03-29 Thomas

    Internally I used "$bosiContracts" but symfony was not able to determine.

  • 2017-03-24 Victor Bocharsky

    Ah, now I see. Really, it should be singular form. Just wonder what property name did you use? "varname"?

    Cheers!

  • 2017-03-23 Thomas

    You are right but Symfony simply was not "seeing" my adder/remover-methods dispite they exists. This was caused by the weird Singular-thing.

  • 2017-03-23 Victor Bocharsky

    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!

  • 2017-03-23 Thomas

    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 :)

  • 2017-03-23 Thomas

    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.