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 SubscribeWe're turning Programmers into JSON by hand inside serializeProgrammer()
:
... lines 1 - 15 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 18 - 147 | |
private function serializeProgrammer(Programmer $programmer) | |
{ | |
return array( | |
'nickname' => $programmer->getNickname(), | |
'avatarNumber' => $programmer->getAvatarNumber(), | |
'powerLevel' => $programmer->getPowerLevel(), | |
'tagLine' => $programmer->getTagLine(), | |
); | |
} | |
} |
That's pretty ok with just one resource, but this will be a pain when we have a lot more - especially when resources start having relations to other resources. It'll turn into a whole soap opera. To make this way more fun, we'll use a serializer library: code that's really good at turning objects into an array, or JSON or XML.
The one we'll use is called "JMS Serializer" and there's a bundle for it called JMSSerializerBundle. This is a fanstatic library and incredibly powerful. It can get complex in a few cases, but we'll cover those. You should also know that this library is not maintained all that well anymore and you'll see a little bug that we'll have to work around. But it's been around for years, it's really stable and has a lot of users.
Symfony itself ships with a serializer, Symfony 2.7 has a lot of features that JMS Serializer has. There's a push inside Symfony to make it eventually replace JMS Serialize for most use-cases. So, keep an eye on that. Oh, and JMS Serializer is licensed under Apache2, which is a little bit less permissive than MIT, which is Symfony's license. If that worries you, look into it further.
With all that out of the way, let's get to work. Copy the composer require
line and paste it into the terminal:
composer require jms/serializer-bundle
While we're waiting, copy the bundle line and add this into our AppKernel
:
... lines 1 - 5 | |
class AppKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
$bundles = array( | |
... lines 11 - 19 | |
new \JMS\SerializerBundle\JMSSerializerBundle(), | |
); | |
... lines 22 - 31 | |
return $bundles; | |
} | |
... lines 34 - 40 |
This gives us a new service calld jms_serializer
, which can turn any object into JSON or XML. Not unlike a Harry Potter wizarding spell.... accio JSON! So in the controller, rename serializeProgrammer
to serialize
and make the argument $data
, so you can pass it anything. And inside, just return $this->container->get('jms_serializer')
and call serialize()
on that, passing it $data
and json
:
... lines 1 - 15 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 18 - 144 | |
private function serialize($data) | |
{ | |
return $this->container->get('jms_serializer') | |
->serialize($data, 'json'); | |
} | |
} |
PhpStorm is angry, just because composer hasn't finished downloading yet: we're working ahead.
Find everywhere we used serializeProgrammer()
and change those. The only trick is that it's not returning an array anymore, it's returning JSON. So I'll say $json = $this->serialize($programmer)
. And we can't use JsonResponse
anymore, or it'll encode things twice. Create a regular Response
instead. Copy this and repeat the same thing in showAction()
. Use a normal Response
here too:
... lines 1 - 21 | |
public function newAction(Request $request) | |
{ | |
... lines 24 - 33 | |
$json = $this->serialize($programmer); | |
$response = new Response($json, 201); | |
... lines 36 - 39 | |
$response->headers->set('Location', $programmerUrl); | |
return $response; | |
} | |
... lines 44 - 48 | |
public function showAction($nickname) | |
{ | |
... lines 51 - 61 | |
$json = $this->serialize($programmer); | |
$response = new Response($json, 200); | |
return $response; | |
} | |
... lines 68 - 151 |
For listAction
, life gets easier. Just put the $programmers
array inside the $data
array and then pass this big structure into the serialize()
function:
... lines 1 - 72 | |
public function listAction() | |
{ | |
$programmers = $this->getDoctrine() | |
->getRepository('AppBundle:Programmer') | |
->findAll(); | |
$json = $this->serialize(['programmers' => $programmers]); | |
$response = new Response($json, 200); | |
return $response; | |
} | |
... lines 84 - 151 |
The serializer has no problem serializing arrays of things. Make the same changes in updateAction()
:
... lines 1 - 88 | |
public function updateAction($nickname, Request $request) | |
{ | |
... lines 91 - 108 | |
$json = $this->serialize($programmer); | |
$response = new Response($json, 200); | |
return $response; | |
} | |
... lines 114 - 151 |
Great! Let's check on Composer. It's done, so let's try our entire test suite:
phpunit -c app
Ok, things are not going well. One of them says:
Error reading property "avatarNumber" from available keys
(id, nickname, avatar_number, power_level)
The responses on top show the same thing: all our properties are being underscored. The JMS Serializer library does this by default... which I kinda hate. So we're going to turn it off.
The library has something called a "naming strategy" - basically how it transforms property names into JSON or XML keys. You can see some of this inside the bundle's configuration. They have a built-in class for doing nothing: it's called the "identical" naming strategy. Unfortunately, the bundle has a bug that makes this not configurable in the normal way. Instead, we need to go kung-foo on it.
Open up config.yml
. I'll paste a big long ugly new parameter here:
... lines 1 - 5 | |
parameters: | |
# a hack - should be configurable under jms_serializer, but the property_naming.id | |
# doesn't seem to be taken into account at all. | |
jms_serializer.camel_case_naming_strategy.class: JMS\Serializer\Naming\IdenticalPropertyNamingStrategy | |
... lines 10 - 79 |
This creates a new parameter called jms_serializer.camel_case_naming_strategy.class
. I'm setting this to JMS\Serializer\Naming\IdenticalPropertyNamingStrategy
. That is a total hack - I only know to do this because I went deep enough into the bundle to find this. If you want to know how this works, check out our Journey to the Center of Symfony: Dependency Injection screencast: it's good nerdy stuff. The important thing for us is that this will leave our property names alone.
So now if we run the test:
phpunit -c app
we still have failures. But in the dumped response, our property names are back!
Hello
I install jms with
composer require jms/serializer-bundle
but when I add
new \JMS\SerializerBundle\JMSSerializerBundle(),
into AppKernel.php
in the
public function registerBundles()
{
$bundles = [
new \JMS\SerializerBundle\JMSSerializerBundle(),
]
it's highlights and say "undefined namespace for SerializerBundle"
"undefined class JMSSerializerBundle"
How can I to fix this?
Hey Nina!
Hmm. Do you actually get an error when you run the code? Or is it simply that your editor isn't happy? If you get an error, it seems to me that somehow, the installation of the bundle was *not* successful. If the error is only in your editor, try clearing your browser's cache (I assume PhpStorm? One trick is to right click on vendor/ and select Synchronize 'vendor' to make sure it sees the new files).
Cheers!
I think installation was successful
> composer require jms/serializer
Using version ^1.8 for jms/serializer
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 5 installs, 0 updates, 0 removals
- Installing phpoption/phpoption (1.5.0): Loading from cache
- Installing phpcollection/phpcollection (0.5.0): Loading from cache
- Installing jms/parser-lib (1.0.0): Loading from cache
- Installing jms/metadata (1.6.0): Loading from cache
- Installing jms/serializer (1.8.1): Loading from cache
Writing lock file
Generating autoload files
> Incenteev\ParameterHandler\ScriptHandler::buildParameters
Updating the "app/config/parameters.yml" file
> Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::buildBootstrap
> Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::clearCache
// Clearing the cache for the dev environment with debug true
[OK] Cache for the "dev" environment (debug=true) was successfully cleared.
> Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::installAssets
Trying to install assets as relative symbolic links.
-- -------- ----------------
Bundle Method / Error
-- -------- ----------------
[OK] All assets were successfully installed.
> Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::installRequirementsFile
> Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::prepareDeploymentTarget
I try "to right click on vendor/ and select Synchronize 'vendor'" but it isn't help
editor highlights
public function registerBundles()
{
$bundles = [
new \JMS\SerializerBundle\JMSSerializerBundle(),
]
and
in the controller when I try to use
$user = $this->container->get('jms_serializer')
->serialize($user, 'json');
'jms_serializer' also highlights
and in the browser I have:
Whoops, looks like something went wrong.
(1/1) ClassNotFoundException
Attempted to load class "JMSSerializerBundle" from namespace "JMS\SerializerBundle".
Did you forget a "use" statement for another namespace?
in AppKernel.php (line 19)
at AppKernel->registerBundles()
in Kernel.php (line 450)
at Kernel->initializeBundles()
in Kernel.php (line 116)
at Kernel->boot()
in Kernel.php (line 168)
at Kernel->handle(object(Request))
in app_dev.php (line 29)
at require('C:\\OpenServer\\domains\\testovoe.rich\\web\\app_dev.php')
in router.php (line 42)
Hey Nina,
I see in the output you execute "$ composer require jms/serializer" but that's not a bundle, that is just a standalone library. So instead, you need to execute "$ composer require jms/serializer-bundle" and only then register your bundle with "new JMS\SerializerBundle\JMSSerializerBundle()," in AppKernel.php. Btw, notice that there's no leading slash before JMS namespace - I see you have it. It shouldn't cause problems, but just in case. So try to stick this installation: http://jmsyst.com/bundles/J...
Let us know if it doesn't help you.
Cheers!
The hack for identical property error in JMS Serializer not worked for me somehow. I found another hack somewhere:
Add this in services.yml:
jms_serializer.naming_strategy:
alias: jms_serializer.identical_property_naming_strategy
Hey! Is symfony serializer robust enough now?
Only started using serializers (jms ans symfony) recently.
Hey Mike!
First, LOVE your comments below - made our morning!
The Symfony serializer *is* robust enough now. Though, the JMSSerializer is a bit more configurable. For example, if you have a class and only want to expose *some* of the properties and/or maybe have a few "extra" properties (where the serializer calls a get* method to get the value for a field that is not a real value), you can do that in both (you'll use the Group annotation with the Symfony serializer to only serialize some fields). But that's about as far as the customizations go with the Symfony serializer... and I think that's by design (and kind of a good thing). If you have a resource (to use our fancy REST terms) and it begins to look *quite* different than your entity class, it might be better to create a new, "model" (non-entity) class and serialize this (e.g. create your new object and manually take the data from your entity and put it onto your model class - it's a bit of manual labor, but clear). The Symfony serializer pushes this approach a bit more. It's more work... but you also end up with clean model classes that exactly match your API (versus an entity class with a ton of annotations to modify it).
Honestly, I'm still on the fence a bit between the two. I'm tending to use the Symfony serializer more... but recently, the JMS serializer appears to have become "unabandoned" and is active again. Oh choices!
Cheers!
Hey Kaizoku!
Haha, yea, it's interesting now :). JMSSerializer was abandoned for a long time. Now it is definitely not. However, the Symfony serializer is still more actively developed in my opinion, mainly because the lead developer of it is also the lead developer of API Platform (which uses it). I think both are solid choices, but I would go with the Symfony serializer at this point. I would also recommend looking into API Platform itself - we are definitely planning a tutorial on it at some point - it's amazing.
Cheers!
Thank you Ryan for your insight.
The fact that it's used by API Platform is a strong point. I found this interesting article on Github where API Platform explain their choices.
https://github.com/api-plat...
So unless you need a specific feature of JMS or you are already used to, I would go with Symfony Serialiser.
Hey Kaizoku, for the record, in our latest tutorial (Symfony4 forms) Ryan is using Symfony's Serializer instead of JMS. Anyway. I'll ping Ryan because hearing his opinion on this matter is interesting :)
Hey KnpUniversity
In the first time, Happy new year and all my best wishes for this new year (love, peace and more symfony code :p )
I have just a little question about the serialization, now with sf 3 is it better to use the Symfony serializer ?
And if it is yes is it correct to replace this method like that ?
private function serialize($data)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
return $serializer->serialize($data, 'json');
}
Thanks again for your good work.
Hey Greg!
Wow - happy new year to you too - and all the same wonderful wishes :).
This is a GREAT question... and one that I struggle with myself. The JMSSerializer is still more powerful in my opinion than the Symfony serializer, with many more annotations to help you customize things. However, it's also basically abandoned, and much more complex. The Symfony serializer is still under active development and is a very high quality library!
So, right now, the best option comes down to a case-by-case basis, based on the developer. With JMSSerializer, since it has so many annotations, you can usually serialize your entities directly. If you need to tweak the name of a property (e.g. firstName should be "user_first_name" on JSON), that's easy to do. For more junior developers, I think its flexibility through all the annotations gives it a lower barrier to entry (the Symfony serializer does however have the @Groups annotation, which handles most cases - but isn't as user-friendly as the @Expose in JMS). For more senior developers, I recommend using the Symfony serializer. The trick is that if you have a JSON representation that looks sufficiently different than your entity, there are no annotations you can use that will help you "tweak" things so that you can serialize your entity directly. Instead, when I use the Symfony serializer, I usually create specific "model" classes and serialize them. For example, instead of serializing a Product entity, I'll create an API\Product class with the exact properties that I want in my JSON. I will then manually create the API\Product class from my Product entity's data and then serialize it. This is more work (and can seem like a lot to some developers). But, it also means that you have a really clean API: you can just open a directory full of simple PHP classes that each perfectly model a resource.
Phew! I hope that helps! About your code question, it's kind of right... but you're doing too much work! In the Symfony framework, Symfony comes with a pre-configured serializer service (you just need to active it in config.yml). Then you can just $this->get('serializer')->serialize($data, 'json')
.
Cheers!
Hey Ryan
Thank you for your answer, it is exactly what I am thinking. When I tried to use Symfony serializer in this tuto I got back all my entity with the entire user's relation (so included the password). JMSSerializer has a great powerful annotation to handle this problem.
I saw in the documentation the @Groups annotation for SF Serializer but I don't know yet how it is working ;)
thanks for the tip about the SF Serializer's configuration.
Again have a great year for all the KnpUniversity's team and make us like usually worderful tutos
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
}
}
"It'll turn into a whole soap opera". Great lines like this every video, but this is my favorite!
You guys are so stinkin' funny!
And cute.
And smart.
Love your tuts!