Form Voodoo: property_path
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeRemember, we want to design our API to work well with whomever is using it: whether that's a third-party API client, a JavaScript front end, or another PHP app that's talking to us. That's why we just changed how our Battle output looks.
But you might also want to control how the input looks: what the client needs to send to your API. For example, right now, to create a new battle, you send a project
field and a programmer
field: each set to their ID:
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 15 | |
public function testPOSTCreateBattle() | |
{ | |
// ... lines 18 - 22 | |
$data = array( | |
'project' => $project->getId(), | |
'programmer' => $programmer->getId() | |
); | |
// ... lines 27 - 42 | |
} | |
} |
But what if we wanted to call these fields projectId
and programmerId
?
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 15 | |
public function testPOSTCreateBattle() | |
{ | |
// ... lines 18 - 22 | |
$data = array( | |
'projectId' => $project->getId(), | |
'programmerId' => $programmer->getId() | |
); | |
// ... lines 27 - 42 | |
} | |
} |
After all, those are IDs that are being sent. If we change this in the test, everything will explode. Prove it by running things:
./vendor/bin/phpunit --filter testPOSTCreateBattle
Yep, a big validation error: the form should not contain extra fields: these two new fields are not in the form we built.
The easiest fix is to simply rename these fields in the form to projectId
and programmerId
:
// ... lines 1 - 9 | |
class BattleType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->add('programmerId', EntityType::class, [ | |
// ... lines 17 - 18 | |
]) | |
->add('projectId', EntityType::class, [ | |
// ... lines 21 - 22 | |
]) | |
; | |
} | |
// ... lines 26 - 33 | |
} |
But then, we would also need to change the property names in BattleModel
to match these:
// ... lines 1 - 7 | |
class BattleModel | |
{ | |
private $project; | |
private $programmer; | |
// ... lines 13 - 32 | |
} |
And that sucks: because these properties do not hold ID's: they hold objects. I'd rather not need to make my class ugly and confusing to help out the API.
Using property_path
Here is the very simple, elegant, amazing solution. In the form, you do need to update your fields to projectId
and programmerId
so they match what the client is sending. But then, add a property_path
option to projectId
set to project
:
// ... lines 1 - 10 | |
class BattleType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->add('programmerId', EntityType::class, [ | |
'class' => 'AppBundle\Entity\Programmer', | |
'property_path' => 'programmer' | |
]) | |
// ... lines 20 - 23 | |
; | |
} | |
// ... lines 26 - 33 | |
} |
Do the same thing to the programmerId
field: 'property_path' => 'programmer'
:
// ... lines 1 - 10 | |
class BattleType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
// ... lines 16 - 19 | |
->add('projectId', EntityType::class, [ | |
'class' => 'AppBundle\Entity\Project', | |
'property_path' => 'project' | |
]) | |
; | |
} | |
// ... lines 26 - 33 | |
} |
That's the key! The form now expects the client to send projectId
and programmerId
. But when it sets the final data on BattleModel
, it will call setProject()
and setProgrammer()
.
This is a little known way to have a field name that's different than the property name on your class. Bring on the test!
./vendor/bin/phpunit --filter testPOSTCreateBattle
Awesome! Another useful option I want you to know about is called mapped
. You can use this to allow an extra field in your input, without needing to add a corresponding property to your class.