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

VirtualProperty: Add Crazy JSON Fields

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

The test passes, but let's see what the response looks like. Add $this->debugResponse():

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 35
$this->debugResponse($response);
// todo for later
//$this->assertTrue($response->hasHeader('Location'));
}
}

and re-run the test:

./vendor/bin/phpunit --filter testPOSTCreateBattle

Check it out! It has the fields we expect, but it's also embedding the entire programmer and project resources. That's what the serializer does when a property is an object. This might be cool with you, or maybe not. For me, this looks like overkill. Instead of having the Programmer and Project data right here, it's probably enough to just have the programmer's nickname and the project's id.

But hold on: I want to mention something really important. Whenever you need to make a decision about how your API should work, the right decision should always depend on who you're making the API for. If you're building your API for an iPhone app, will having these extra fields be helpful? Or, if you're API is for a JavaScript frontend like ReactJS, then build your API to make React happy.

Adding an ExclusionPolicy

Let's assume that we do not want those embedded objects. First, hide them! In the Battle entity, we need to add some serialization exclusion rules. Since we do this via annotations, we need a use statement. Here's an easy way to get the correct use statement without reading the docs. I know that one of the annotations is called ExclusionPolicy. Add use ExclusionPolicy and let it autocomplete. Now, remove the ExclusionPolicy ending and add as Serializer:

... lines 1 - 6
use JMS\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="battle_battle")
* @ORM\Entity(repositoryClass="AppBundle\Repository\BattleRepository")
... line 12
*/
class Battle
... lines 15 - 114

Now, above the class, add @Serializer\ExclusionPolicy("all"):

... lines 1 - 6
use JMS\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="battle_battle")
* @ORM\Entity(repositoryClass="AppBundle\Repository\BattleRepository")
* @Serializer\ExclusionPolicy("all")
*/
class Battle
... lines 15 - 114

now no properties will be used in the JSON, until we expose them. Expose id, skip programmer and project, and expose didProgrammerWin, foughtAt and notes:

... lines 1 - 13
class Battle
{
/**
... lines 17 - 19
* @Serializer\Expose()
*/
private $id;
... lines 23 - 27
private $programmer;
... lines 29 - 33
private $project;
/**
... line 37
* @Serializer\Expose()
*/
private $didProgrammerWin;
/**
... line 43
* @Serializer\Expose()
*/
private $foughtAt;
/**
... line 49
* @Serializer\Expose()
*/
private $notes;
... lines 53 - 112
}

Run the same test

./vendor/bin/phpunit --filter testPOSTCreateBattle

Ok, awesome - the JSON has just these 4 fields.

Adding Fake Properties

Let's go to the next level. Now, I do want to have a programmer, but set to the username instead of the whole object. And I also do want a project field, set to its id.

Update the test to look for these. Use $this->asserter()->assertResponsePropertyEquals() and pass it $response. Look for a project field that's set to $project->getId():

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 33
$this->asserter()
->assertResponsePropertyExists($response, 'didProgrammerWin');
$this->asserter()
->assertResponsePropertyEquals($response, 'project', $project->getId());
... lines 38 - 42
}
}

Copy that line and do the same thing for programmer: it should equal Fred:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 33
$this->asserter()
->assertResponsePropertyExists($response, 'didProgrammerWin');
$this->asserter()
->assertResponsePropertyEquals($response, 'project', $project->getId());
$this->asserter()
->assertResponsePropertyEquals($response, 'programmer', 'Fred');
... lines 40 - 42
}
}

We could also have this return the id - it's up to you and what's best for your client.

But, how can we bring this to life? We're in a weird spot, because these fields do exist on Battle, but they have the wrong values. How can we do something custom?

By using something called a virtual property. First, create a new public function called getProgrammerNickname(). It should return $this->programmer->getNickname():

... lines 1 - 13
class Battle
{
... lines 16 - 117
public function getProgrammerNickname()
{
return $this->programmer->getNickname();
}
... lines 122 - 130
}

VirtualProperty

Simple. But that will not be used by the serializer yet. To make that happen, add @Serializer\VirtualProperty above the method. As soon as you do this, it will be exposed in your API. But it will be called programmerNickname:

... lines 1 - 13
class Battle
{
... lines 16 - 113
/**
* @Serializer\VirtualProperty()
... line 116
*/
public function getProgrammerNickname()
{
return $this->programmer->getNickname();
}
... lines 122 - 130
}

the serializer generates the field name by taking the method name and removing get.

SerializedName

Since we want this to be called programmer add another annotation: @Serializer\SerializedName() and pass it programmer:

... lines 1 - 13
class Battle
{
... lines 16 - 113
/**
* @Serializer\VirtualProperty()
* @Serializer\SerializedName("programmer")
*/
public function getProgrammerNickname()
{
return $this->programmer->getNickname();
}
... lines 122 - 130
}

Now we have a programmer field set to the return value of this method.

Do the same thing for project: public function getProjectId(). This will return $this->project->getId():

... lines 1 - 13
class Battle
{
... lines 16 - 126
public function getProjectId()
{
return $this->project->getId();
}
}

Above this, add the @Serializer\VirtualProperty to activate the new field and @Serializer\SerializedName("project") to control its name:

... lines 1 - 13
class Battle
{
... lines 16 - 122
/**
* @Serializer\VirtualProperty()
* @Serializer\SerializedName("project")
*/
public function getProjectId()
{
return $this->project->getId();
}
}

Head to the terminal and try the test:

./vendor/bin/phpunit --filter testPOSTCreateBattle

We've got it! This trick is a wonderful way to take control of exactly how you want your representation to look.

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