Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Remember, 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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice