If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
The serializer is... mostly working. But we have some test failures:
Error reading property "tagLine" from available keys (id, nickname
avatarNumber, powerLevel)
Huh. Yea, if you look at the response, tagLine
is mysteriously absent!
Where did you go dear tagLine???
So the serializer works like this: you give it an object, and it serializes
every property on it. Yep, you can control that - just hang on a few minutes.
But, if any of these properties is null
, instead of returning that key
with null
, it omits it entirely.
Fortunately, that's easy to change. Go into BaseController
. In serialize()
create a new variable called $context
and set that to a new SerializationContext()
.
Call setSerializeNull()
on this and pass it true
. To finish this off,
pass that $context
as the third argument to serialize()
:
... lines 1 - 123 | |
protected function serialize($data, $format = 'json') | |
{ | |
$context = new SerializationContext(); | |
$context->setSerializeNull(true); | |
return $this->container->get('jms_serializer') | |
->serialize($data, $format, $context); | |
} | |
... lines 132 - 133 |
Think of the SerializationContext
as serialization configuration. It doesn't
do a lot of useful stuff - but it does let us tell the serializer to actually
return null fields.
So run the whole test suite again and wait impatiently:
phpunit -c app
ZOMG! They're passing!
But something extra snuck into our Response - let me show you. In testGETProgrammer()
,
at the end, add $this->debugResponse()
. Copy that method name and run just
it:
phpunit -c app --filter testGETProgrammer
Ah, the id
field snuck into the JSON. Before, we only serialized the other
four fields. So what if we didn't want id
or some property to be serialized?
The solution is so nice. Go back to the homepage of the bundle's docs. There's one documentation gotcha: the bundle is a small wrapper around the JMS Serializer library, and most of the documentation lives there. Click the documentation link to check it out.
This has a great page called Annotations: it's a reference of all of the ways that you can control serialization.
One useful annotation is @VirtualProperty.
This lets you create a method and have its return value serialized. If you
use that with @SerializedName
, you can control the serialized property
name for this or anything.
For controlling which fields are returned, we'll use
@ExclusionPolicy.
Scroll down to the @AccessType
code block and copy that use
statement.
Open the Programmer
entity, paste this on top, but remove the last part
and add as Serializer
:
... lines 1 - 5 | |
use JMS\Serializer\Annotation as Serializer; | |
... lines 7 - 189 |
This will let us say things like @Serializer\ExclusionPolicy
. Add that
on top of the class, with "all"
.
... lines 1 - 5 | |
use JMS\Serializer\Annotation as Serializer; | |
... line 7 | |
/** | |
... lines 9 - 12 | |
* @Serializer\ExclusionPolicy("all") | |
*/ | |
class Programmer | |
... lines 16 - 189 |
This says: "Hey serializer, don't serialize any properties by default,
just hang out in your pajamas". Now we'll use @Serializer\Expose()
to
whitelist the stuff we do want. We don't want id
- so leave that.
Above the $name
property, add @Serializer\Expose()
. Do this same thing
above $avatarNumber
, $tagLine
and $powerLevel
:
... lines 1 - 14 | |
class Programmer | |
{ | |
... lines 17 - 25 | |
/** | |
... lines 27 - 29 | |
* @Serializer\Expose | |
*/ | |
private $nickname; | |
... line 33 | |
/** | |
... lines 35 - 37 | |
* @Serializer\Expose | |
*/ | |
private $avatarNumber; | |
... line 41 | |
/** | |
... lines 43 - 45 | |
* @Serializer\Expose | |
*/ | |
private $tagLine; | |
... line 49 | |
/** | |
... lines 51 - 53 | |
* @Serializer\Expose | |
*/ | |
private $powerLevel = 0; | |
... lines 57 - 186 | |
} |
And my good buddy PhpStorm is telling me I have a syntax error up top. Whoops,
I doubled my use
statements - get rid of the extra one.
With this, the id
field should be gone from the response. Run the test!
phpunit -c app --filter testGETProgrammer
No more id
! Take out the debugResponse()
. Phew! Congrats! We only have
one resource, but our API is kicking butt! We've built a system that let's
us serialize things easily, create JSON responses and update data via forms.
Oh, and the serializer can also deserialize. That is, take JSON and turn it back into an object. I prefer to use forms instead of this, but it may be another option. Of course, if life gets complex, you can always just handle incoming data manually without forms or deserialization. Just keep that in mind.
We also have a killer test setup that let's us write tests first without any headache. We could just keep repeating what we have here to make a bigger API.
But, there's more to cover! In episode 2, we'll talk about errors: a fascinating topic for API's and something that can make or break how usable your API will be.
Ok, seeya next time!
Hi, how could we implement the ExclusionPolicy with FOSUserBundle ? I can't "hide" in the serialization, the password, salt etc... :'(
Great question actually! Try this out and let me know what you find: https://github.com/schmittj...
Cheers!
Hi :) Thanks for the answer. Well i've just made this after my question and that's correct, it's works :)
Thank's you for the screencast !
Hi,
Having trouble getting the User entity serialized with exclusion policy. We extend it from FOSUserBundle, desopite saying ExclusionPolicy(all), all fields are serialized. Its as if JMS serializer is ignoring annotations. I searched the web and found config changes to be done for FOSUserBundle entity, I implemented those, but no change. Only thing that seems to be working is by configuring groups and setting them in the context. JMS even exposed a FK collection I defined under a different group. So, if you do not set a group, all other annotations are simply ignored! I am using Symfony 3.4 and JMS bundle: 2.3.
I changed things as per this: http://bit.ly/2mEZwBw And http://bit.ly/2DwcOrp
/**
* @Serializer\ExclusionPolicy("all")
*/
class Therapist extends User
{
/**
* @Serializer\Expose()
* @Serializer\Groups("{Deep}")
*/
private $clinics;
}
Despite configuring "deep" group above, if I do not set group, JMS also exposes this property. Am I doing something wrong ?
Hey Mrugendra Bhure!
Oh boy, this is a mess! JMSSerializer does not play well when you extend a base class: it seems that, under normal situations, *you* can only control the serialization rules for the fields in *your* class, not the parent class.
It's clear to me from looking around that, for some reason, the fix for this will involve you using YAML serialization rules instead of annotations: Check out this thread for details: https://github.com/schmittj...
And honestly, due to all this craziness, you could also decide to NOT use the serializer for just this one object, and instead turn it into JSON manually (or, turn it into an array and the put that through the serializer, which should allow embedded objects [if you want them in your JSON] to still be serialized through JMS).
I hope that helps! Cheers!
Hi Ryan,
When you make updates to courses, how do I know what's been updated?
I've finished some of the courses, but periodically see "Updated 3 days ago", or so, don't see any new videos, so don't know what's been updated.
Thank you!
Hey Vlad!
This is something we're still working on - right now the "Updated" status (unless a course is being actively released still) usually is not significant (we probably tweaked something insignificant). We plan to publish an actual "changelog" whenever we make significant changes in the future. It's on the list!
Cheers!
I notice that you always use the Annotation way, I'm trying to avoid in order to use configuration files in yml, I think that way is more decoupled from the framework or tool, but I don't really know for sure, can you tell me why are you using Annotations?
Hi Roberto!
Yes, very fair question :). First, there is no performance difference or flexibility between the formats. So, it *does* come down to developer preference. I like annotations because I like having my configuration right next to the thing it's configuring. For serializing, you can immediately see what properties are being serialized, without needing to find another configuration file. Route & Doctrine annotations also have that same advantage.
About the coupling idea, I tend to think that idea is over-hyped. You *are* using Symfony, and it's massively unlikely that you'll need to switch suddenly to using something else. And even if you *did* need this, having your configuration in YAML instead of annotations won't make much difference - it would be pretty easy to quickly delete the annotations from your class if you decided to use a different library for serializing. Decoupling from your framework is really important if you're sharing your code (you don't want to force your serialization configuration on another use, just so they can use your class) - but purposefully *coupling* your code to your framework - along with use a nice layer of services, that's key - is a great way to be pragmatic and stay productive. That's subjective of course - but that at least shows you why I've made these choices :).
Cheers!
Hey Ryan
It is me again ;)
I try to use the Symfony's serializer and use the Groups for handling the response.
I tried this:
protected function serialize($data)
{
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizer = new ObjectNormalizer();
$serializer = new Serializer([$normalizer], $encoders);
return $serializer->serialize($data, 'json', ['groups' => ['group1']]);
}
And in my model I put the @Groups({"group1"})
But as you can imagine it doesn't work ;)
If I put $normalizer->setIgnoredAttributes(['user', 'id']); it is ok I don't have the user and id but in the doc it write is better to user the annotation instead the ignoredAttributes.
Do you know what is my mistake ?
Thanks again for your work
Hi Greg!
I might know the issue :). Obviously, as you found out, there is a Serializer component in Symfony, which means you can create a Serializer object from scratch, adding in whatever normalizers, encoders, etc that you want. But, when you use the Symfony Framework, we do this for you. As soon as you uncomment this line (https://github.com/symfony/symfony-standard/blob/bba96b98623851b0ce9331c6fbe9ab2b7e57ae27/app/config/config.yml#L21), you will have a serializer
service... which means you can just do:
return $this->container->get('serializer')->serialize($data, 'json' ['groups' => ['group1']]);
Now, your approach should also work, but I think you're missing the extra setup needed to tell the Serializer to read the annotations. Check out this section: http://symfony.com/doc/current/components/serializer.html#attributes-groups
So, you're totally free to keep creating the Serializer yourself - but you may not need to! But if you do continue to do this, just make sure to make the tweaks in that section so that your annotations are loaded :).
Cheers!
Hi Ryan,
Exactly I definitely forgot to initialize the ClassMetadataFactory.
So I just need to initialize it just above ?
Thanks
You got it :). The 4th code block on that link shows everything all put together - that ClassMetadataFactory is passed to your ObjectNormalizer.
Have fun!
You mention at the end of this tutorial that you prefer to use forms instead of the Serializer to turn the json back into an object. Can you elaborate on why you prefer to use the form method over the serializer?
Hey Tom!
Definitely - good question :). There are a few reasons:
1) Data transformers - the idea that your user might send you an "id", but on your object, that property is itself an object. The EntityType is built to do this type of transformation
2) Similar to the above, I find that your output doesn't always match your *input*. I mean, if I literally looked at the JSON for a GET /blog/{id} endpoint and compared it with the JSON that I send to *create* a blog post, they will differ more than you might think. So, at first, it seems kind of awesome to have one model class that you can serialize for output and deserialize for input. But in reality, it's not often that simple, and the form gives you that layer to hide/show fields or add other transformations.
If I summarize these two reasons, it comes down to this: the serializer is "stupid" by design: it simply takes the JSON (when deserializing) and puts it onto an object. Unless that JSON and your object match perfectly, it gets tough. On the other hand, we going from your model to JSON is often easier: we typically design our model classes to match the JSON we want with little or no effort.
But, not everyone agrees - that's why the deserializer exists! But I like having a model class that models my output, and a form class that models my input.
Cheers!
Hey! Thanks a lot for this, you guys are doing one hell of a job.. I didn't go through the hole code, but it showed me some tricks that were really useful.
Hey Matias,
There're a lot of tricks in REST ;) We are really glad you like this course and it's helpful for you!
Cheers!
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.*", // v2.6.11
"doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
"doctrine/dbal": "<2.5", // v2.4.4
"doctrine/doctrine-bundle": "~1.2", // v1.4.0
"twig/extensions": "~1.0", // v1.2.0
"symfony/assetic-bundle": "~2.3", // v2.6.1
"symfony/swiftmailer-bundle": "~2.3", // v2.3.8
"symfony/monolog-bundle": "~2.4", // v2.7.1
"sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
"sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"hautelook/alice-bundle": "0.2.*", // 0.2
"jms/serializer-bundle": "0.13.*" // 0.13.0
},
"require-dev": {
"sensio/generator-bundle": "~2.3", // v2.5.3
"behat/behat": "~3.0", // v3.0.15
"behat/mink-extension": "~2.0.1", // v2.0.1
"behat/mink-goutte-driver": "~1.1.0", // v1.1.0
"behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
"phpunit/phpunit": "~4.6.0" // 4.6.4
}
}
Did she just say, "and wait impatiently?"
Ha!