Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Serializer & API Endpoint

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

In addition to our login form authentication, I also want to allow users to log in by sending an API token. But, before we get there, let's make a proper API endpoint first.

Creating the API Endpoint

I'll close a few files and open AccountController. To keep things simple, we'll create an API endpoint right here. Add a public function at the bottom called accountApi():

... lines 1 - 11
class AccountController extends BaseController
{
... lines 14 - 27
public function accountApi()
{
... lines 30 - 32
}
}

This new endpoint will return the JSON representation of whoever is logged in. Above, add @Route("/api/account") with name="api_account":

... lines 1 - 11
class AccountController extends BaseController
{
... lines 14 - 24
/**
* @Route("/api/account", name="api_account")
*/
public function accountApi()
{
... lines 30 - 32
}
}

The code here is simple - excitingly simple! $user = $this->getUser() to find who's logged in:

... lines 1 - 11
class AccountController extends BaseController
{
... lines 14 - 24
/**
* @Route("/api/account", name="api_account")
*/
public function accountApi()
{
$user = $this->getUser();
... lines 31 - 32
}
}

We can safely do this thanks to the annotation on the class: every method requires authentication. Then, to transform the User object into JSON - this is pretty cool - return $this->json() and pass $user:

... lines 1 - 11
class AccountController extends BaseController
{
... lines 14 - 24
/**
* @Route("/api/account", name="api_account")
*/
public function accountApi()
{
$user = $this->getUser();
return $this->json($user);
}
}

Let's try it! In your browser, head over to /api/account. And! Oh! That's not what I expected! It's JSON... but it's totally empty!

Installing the Serializer

Why? Hold Command or Control and click into the json() method. This method does two different things, depending on your setup. First, it checks to see if Symfony's serializer component is installed. Right now, it is not. So, it falls back to passing the User object to the JsonResponse class. I won't open that class, but all it does internally is called json_encode() on that data we pass in: the User object in this case.

Do you know what happens when you call json_encode() on an object in PHP? It only... sorta works: it encodes only the public properties on that class. And because we have no public properties, we get back nothing!

This is actually the entire point of Symfony's serializer component! It's a kick butt way to turn objects into JSON, or any other format. I don't want to talk too much about the serializer right now: we're trying to learn security! But, I do want to use it. Find your terminal and run:

composer require "serializer:^1.0"

This installs the serializer pack, which downloads the serializer and a few other things. As soon as this finishes, the json() method will start using the new serializer service. Try it - refresh! Hey! It works! That's awesome!

Serialization Groups

Except... well... we probably don't want to include all of these properties - especially the encoded password. I know, I said we weren't going to talk about the serializer, and yet, I do want to fix this one thing!

Open your User class. To control which fields are serialized, above each property, you can use an annotation to organize into "groups". I won't expose the id, but let's expose email by putting it into a group: @Groups("main"):

... lines 1 - 6
use Symfony\Component\Serializer\Annotation\Groups;
... lines 8 - 11
class User implements UserInterface
{
... lines 14 - 20
/**
... line 22
* @Groups("main")
*/
private $email;
... lines 26 - 159
}

When I auto-completed that annotation, the PHP Annotations plugin added the use statement I need to the top of the file:

<?php
... lines 2 - 6
use Symfony\Component\Serializer\Annotation\Groups;
... lines 8 - 161

Oh, and I totally invented the "main" part - that's the group name, and you'll see how I use it in a minute. Copy the annotation and also add firstName and twitterUsername to that same group:

... lines 1 - 11
class User implements UserInterface
{
... lines 14 - 20
/**
... line 22
* @Groups("main")
*/
private $email;
... lines 26 - 31
/**
... line 33
* @Groups("main")
*/
private $firstName;
... lines 37 - 42
/**
... line 44
* @Groups("main")
*/
private $twitterUsername;
... lines 48 - 159
}

To complete this, in AccountController, we just need to tell the json() method to only serialize properties that are in the group called "main". To do that, pass the normal 200 status code as the second argument, we don't need any custom headers, but we do want to pass one item to "context". Set groups => an array with the string main:

... lines 1 - 11
class AccountController extends BaseController
{
... lines 14 - 24
/**
* @Route("/api/account", name="api_account")
*/
public function accountApi()
{
... lines 30 - 31
return $this->json($user, 200, [], [
'groups' => ['main'],
]);
}
}

You can include just one group name here like this, or tell the serializer to serialize the properties from multiple groups.

Let's try it! Refresh! Yes! Just these three fields.

Ok, we are now ready to take on a big, cool topic: API token authentication.

Leave a comment!

50
Login or Register to join the conversation
Jim C. Avatar
Jim C. Avatar Jim C. | posted 3 years ago | edited

As of Symfony 5, use composer require symfony/serializer-pack instead of composer require serializer, otherwise when you try to use groups, nothing will happen. Everthing else is the same.

2 Reply

Hey Jim C.!

Hmm. The serializer Flex alias is still symfony/serializer-pack. This means that composer require serializer should be identical to composer require symfony/serializer-pack (I just double-checked this in a project to be 100% sure)!

There is one small difference starting in Symfony Flex 1.9 (but it will happen in both cases and it doesn't affect the functionality): after the pack is installed, it will be "unpacked" so that - instead of seeing symfony/serializer-pack in your composer.json, you'll see 5 libraries inside that pack, which is nice :).

Anyways, let me know if you're seeing different behavior - something could be amiss ;). Oh, but it is true that if you ran composer require symfony/serializer (to only install that ONE component), the Groups functionality wouldn't work unless you already had doctrine/annotations installed (that package is included in the pack).

Cheers!

Reply
Jim C. Avatar
Jim C. Avatar Jim C. | weaverryan | posted 3 years ago | edited

Hi,

Thanks for the speedy response.

doctrine/annotations was (and is) installed.

With the fourth argument present in the call to the JSON serializer...

`

    return $this->json($user, 200, [], [
        'groups' => ['main'],
    ]);

`

...it returned an empty object.

Did a quick Google to see if anything had changed in this regard between Symfonys 4 and 5, and found the <a href="https://symfony.com/doc/current/serializer.html&quot;&gt;How to Use the Serializer</a> page with the instruction to use serializer-pack.

Removed serializer, installed serializer-pack, and the returned object contained the three properties.

No changes to the code, just the package.

Cheers,

Jim

2 Reply
Joao P. Avatar

Me too

1 Reply
Sushil M. Avatar

Same for me.

Reply

Thanks for posting the extra comments - it looks like this is a pretty common issue!

To give a bit more background:

A) composer require symfony/serializer will give you just one package
B) composer require serializer will expand to composer require symfony/serializer-pack, which will give you 5 packages - https://github.com/symfony/serializer-pack/blob/61173947057d5e1bf1c79e2a6ab6a8430be0602e/composer.json#L7-L11

The serializer is a bit unique in the way that it will work with only symfony/serializer, but it will work better and with more features as you install more things. I'm not sure which one of those packages the Groups must be relying on them, but clearly it is one of them :).

Cheers!

Reply
Diana E. Avatar
Diana E. Avatar Diana E. | posted 2 years ago

Hi Ryan,

Is there a way to download the course files per chapter, please? If not, would it be possible to download the files as of chapter 23 in the course? I've run into an issue I am not able to recover from and I am not able to finish the course without working code.

Thank you

Reply

Hey Diana,

When you download the course code - we give you 2 versions of the project: one is inside start/ directory snd contains the empty project ready to follow the tutorial, and another one is inside finish/ directory that contains the code of finished project you get at the end of the course. Unfortunately, we don't have a feature to download course code per chapter yet, but we will think about it in the future. For now, no any estimations when it might be released. But we have dynamic expandable code blocks on chapter page below each video - they are displaying the exact code we have on the video in the current spot, so if you want to download code for a file that is shown in the current video - file the file in the scripts below the video and expand its code, download it into your project. The downside is that you can only see files we're changing in the current video.

I hope this helps! If note and you still have problems with finishing this course - let us know in the comments what issue you have and we will try to help you.

Cheers!

Reply
Diana E. Avatar

Thanks for your Response Victor. I think the problem arose due to me having to use a more recent MakerBundle version as I couldn't use the one used in this course, but I could be wrong. I am aware of the start and finish folders, but that won't solve my problem unless I want to start the course all over again. I can't replicate the issue now as I've deleted my local repo but if i do a fresh clone from GitHub (of my previous code up to this chapter) I run into a similar problem (having to do with bundle versions and composer update). Unfortunately unless I have a working version up to this point, I am not able to carry on with the course :/

Reply

Hey Diana,

Did you try to take some files you're not sure are correct in your project from the finish/ folder? Copy/pasting them from the finish/ folder may do the trick.

Cheers!

Reply
Diana E. Avatar
Diana E. Avatar Diana E. | Diana E. | posted 2 years ago | edited

The error message has to do with failure to load classes. Some online resources suggested editing composer.json


"autoload": {
"psr-4": {
"App\\": "src/"
}

to


"autoload": {
"psr-4": {
"": "src/"
}

but that did not fix the issue.

Reply

Hey Diana,

It's still would be better to see the exact error message though. But in this case, I'd recommend you to take the composer.json and composer.lock files from the finish/ folder and put it into your project replacing your files. Then, let's remove the vendor/ folder completely just in case, you can do it with "rm -rf vendor/" command and next you would need to run "composer install" to install all the dependencies.

Then, try again and see if you still have the same error. If the error is the same - please, double check that all your PHP files in src/ directory have correct namespaces that match the file structure, i.e. src/Entity/User.php has "namespace App\Entity;" etc.

If namespaces looks good and match your actual file structure - please, share the exact error message with us, it will help a bit more :)

Cheers!

Reply
Diana E. Avatar

Yes! this seems to have done the trick. Up and running again. Thanks a lot, Victor :)

Reply

Hey Diana,

I'm happy to hear this! Thanks for confirming that helped.

Cheers!

Reply
Rupok chowdhury P. Avatar
Rupok chowdhury P. Avatar Rupok chowdhury P. | posted 2 years ago | edited

composer require serializer
is now:
composer require symfony/serializer-pack
Source: https://symfony.com/doc/current/serializer.html

Reply

Hey Rupok chowdhury P.

Yes and no :) Symfony has great composer plugin named symfony/flex which allows to install some packages with aliases and also provide autoconfiguration. BTW you can check it manually here https://flex.symfony.com/ You can find that symfony/serializer-pack has following aliases serialization, serializer, serializer-pack which can be used to install via composer

Cheers!

Reply
Rupok chowdhury P. Avatar
Rupok chowdhury P. Avatar Rupok chowdhury P. | sadikoff | posted 2 years ago | edited

sadikoff (not directly related to the discussion here but) I'm wondering, why the official symfony documentation didn't suggest using flex. I mean, there's nothing wrong mentioning the direct package name, but symfony has flex, so it could suggest "composer require serializer-pack" instead of "composer require symfony/serializer-pack", right? :) Do you know anything about that? (Just trying to learn if I missed some important aspect there). TIA

Reply

That IS a good question. I think the main reason is that it's a documentation so it should be very strict in recommendations, probably they should mention somehow the possibility of using aliases, maybe it's mentioned somewhere.

I don't think there is a 100% correct answer for such question. That is my thoughts :)

Cheers!

Reply
Rupok chowdhury P. Avatar
Rupok chowdhury P. Avatar Rupok chowdhury P. | sadikoff | posted 2 years ago

You are right. This is quite interesting. I checked the recipes server and found that "symfony/serializer-pack" has three aliases - "serialization" "serializer" "serializer-pack". Also, according to the flex site, "symfony/serializer" doesn't have any alias.

Surprisingly, when I ran "composer require serializer", that added only the "symfony/serializer" package while it's supposed to add the whole pack because the alias is for the pack. Because the universe wants to play with us more when we want to show something to somebody, I just ran the command again now, and this time it added the whole pack. :facepalm:

Reply

hm interesting behaviour, probably some versions or cache bug.... hm can you completely remove all packages added by serializer-pack and try installation again?

Reply
Cameron Avatar
Cameron Avatar Cameron | posted 2 years ago

Hi, I've spent some time trying to understand the purpose of CSRF and it doesn't appear that it's able to prevent a forged request when the attacker has javascript access (for example via XSS), so I have some questions that I hope someone might be able to help with:

#1: Why have a CSRF token when it can be overcome by a second axios / async request (that pulls down a HTML page and parses out the CSRF token?). This only seems to protect GET requests (for example: via a GET request triggered by a hidden image), not javasript / XSS attacks. Also I don't think you can make a POST/PATCH/DELETE request (without human intervention) without either: a human clicking a form "submit" buttion or without the ability to execute javascript. So only people that aren't being restful (i.e. performing delete and patch/put functions on a GET request) benefit from this.

#2: Why use CSRF tokens on a login form? The requirement of a confused-deputy attack like CSRF is to "confuse" the server by already having a valid sessionID - but one does not exist when trying to login. Is this more the realm of CORS? (which my guess: disables requests coming from clients/browsers outside the control of the PHP developer).

#3: How can a vue (or react) form get the CSRF token? I've seen people say you should create an endpoint to get this and incorporate it into the axios request data (to the PHP server).

Feedback:
it would have been great to have some more context around CSRF and tokens (what the problem / why they are important) - I didn't understand why you would want to use tokens, but it seems like they're good when you want to scale the number of servers you have?

Thanks for any help you're able to give.

Reply

Hi Fox C.!

This stuff can be frustratingly tricky. Let me do my best to help answer your questions :).

#1) ... Why have a CSRF token when it can be overcome by a second axios / async request (that pulls down a HTML page and parses out the CSRF token?). This only seems to protect GET requests (for example: via a GET request triggered by a hidden image), not javasript / XSS attacks.

I think part of the answer here is that CSRF and XSS are two different, distinct, attack vectors. You're totally right: if you implement CSRF perfectly, but you have an XSS vulnerability, then users will be able to inject whatever JavaScript they want onto your site... which means they will be able to make any AJAX requests they want. CSRF does not help here in any way.

You may already understand all of this, but to be sure: the purpose of CSRF is, primarily, to prevent BadSite.com from adding a &lt;form action=https://goodsite.com/delete-my-account method="post" &lt; to their site. In this situation, if the form has a "Delete my account" button, it would appear to me, because I'm on BadSite.com, that I was about to delete from account for BadSite.com. But in reality, when I click, assuming I'm logged in to GoodSite.com, I just deleted my account there! That is the classic CSRF attack: it's (primarily) all about a form that's put on another site that points to your site.... with the intention of tricking a user. Note: SameSite cookies eliminate this attack even without CSRF. If GoodSite.com uses a SameSite cookie (either lax or strict, lax is the default in Symfony), then when the form is submitted, the GoodSite.com session cookie is not sent... and so the user will not be logged in for the request.

#2) Why use CSRF tokens on a login form?

Yea, this is a weird one, right? You're correct in how you are thinking about CSRF. The attack vector here is not as bad. In this case, BadSite.com could create a form with hidden email & password fields that contain the attacker's credentials that points to the login form of GoodSite.com. That would cause them to be logged in as the bad user. Then, they could use a 2nd form - like a credit card form, that points to GoodSite.com - to trick the user isn't adding their credit card to the bad user's account. It's... kind of the reverse of a normal CSRF attack: instead of a bad user tricking a user into doing bad things to the good user's account, the bad user tricks the user into doing good (for the attacker) things to the attacker's account.

#3: How can a vue (or react) form get the CSRF token? I've seen people say you should create an endpoint to get this and incorporate it into the axios request data (to the PHP server).

Yep, that is basically correct :). Here is a bundle that does this in Symfony: https://github.com/dunglas/DunglasAngularCsrfBundle

Two notes about that bundle. First, Angular is in its name, but it has nothing to do with Angular. And second, notice it's deprecated: if you're using SameSite cookies, then you don't need CSRF protection anymore (unless you need to support old browsers and need to make sure those old browser are protected - https://caniuse.com/same-site-cookie-attribute ).

it would have been great to have some more context around CSRF and tokens (what the problem / why they are important) - I didn't understand why you would want to use tokens, but it seems like they're good when you want to scale the number of servers you have?

Fair feedback... this stuff is... annoyingly tricky. When it comes to API authentication, there are 2 main choices:

1) You're building an API for your own JavaScript. Cool: use session cookie and do not mess with tokens. Your frontend needs to live on the same domain/subdomain as your API.

2) You're building an API for external usage (public API, or servers will talk to it): use API tokens and make your API stateless - not session based. This is what you typically see / think of with big, famous API's. To make things more complex, API tokens can be "created" in many different ways - e.g. OAuth, via an endpoint where you send email/password and get a token back, by allowing a token to be created via a web interface, etc.

I hope this helps a bit!

Cheers!

Reply
Cameron Avatar
Cameron Avatar Cameron | weaverryan | posted 2 years ago | edited

weaverryan that's crazy that CSRF can be used in "reverse", thanks for the info on this.

My front-end is from a different domain. From the research I've one here, it looks like I can keep the site as session based (i.e. not using bearer tokens), however the cookies must be SameSite=none but the new chrome 80 change would enforce a secure connection: i.e https. This appears different from the advice you've given, so I will need to do some more research?

Also, it appears that the CORS policy addresses the issue of a javascript fetch (from a 3rd party, attacking site) that parses out a CSRF token before sending the acctual attack request, as the CORS policy would reject the request (or make any returned request unreadable by the client-side JS) - thus preventing the attack (if CORS headers are set correctly).

Reply

Hey Fox C.!

> that's crazy that CSRF can be used in "reverse", thanks for the info on this.

Attack/hacking vectors are *fascinating* - people are really smart :).

> My front-end is from a different domain. From the research I've one here, it looks like I can keep the site as session based (i.e. not using bearer tokens), however the cookies must be SameSite=none but the new chrome 80 change would enforce a secure connection: i.e https. This appears different from the advice you've given, so I will need to do some more research?

On a technical level, everything you just said makes sense. I just don't know if this is a common practice: specifically, using session-based authentication cross-domain by disabling SameSite. SameSite is (as you know) a security mechanism, so it makes me nervous to disable it, unless you can find some information that this is "ok" to do.

> Also, it appears that the CORS policy addresses the issue of a javascript fetch (from a 3rd party, attacking site) that parses out a CSRF token before sending the actual attack request, as the CORS policy would reject the request (or make any returned request unreadable by the client-side JS) - thus preventing the attack (if CORS headers are set correctly).

Again, on a technical level, this makes complete sense! I'm just... always weary when it comes to security stuff, because attack vectors can be quite clever. However, I am quite certain that you are correct here: you could use a CORS policy to allow AJAX access from your frontend's domain, but not any other domains. This is a good read on this topic: https://michaelzanggl.com/a...

Then, as far as I understand things, if you're not using CSRF protection, then the attack vector that you're vulnerable from is "only" the traditional embedded form on BadSite.com. Now, "in theory", if your API endpoints only allow JSON content, then there is no way for me to put a form on BadSite.com that submits to your endpoint and is processed correctly: you would look for a Content-Type: application/json header (which will not be there on a form submit) and reject my bad requests.

At this point, we're covered, right? We have SameSite=none, but thanks to CORS we have limited AJAX requests to our frontend domain AND thanks to checking the Content-Type for application/json on API requests, you have eliminated the risks of an HTML form on BadSite.com. We have solved everything!

Maybe :). The general consensus is that you should use SameSite cookies OR CSRF. Relying on the Content-Type trick is... kind of a "trick" and it's always possible there will be some vector that we're not thinking about. For example, there was (is? I'm not sure if this vector as been fixed) this odd little "Beacon" API that allowed you to make cross-site requests (effectively AJAX requests) with an application/json Content-Type header - https://medium.com/@longter....

So... how's that for a confusing mess of information! This is why, overall, I personally would want to try to find examples of other sites that are doing what you're doing before I ran full force in that direction. Btw, there is one super silly think about all of this: if your backend and frontend were on the same domain... then none of this would be a problem! You could use normal, SameSite cookies and not even fuss with CORS stuff. So, IF that is possible, it's always a nice shortcut.

Cheers!

1 Reply
Cameron Avatar

I agree, deviating from "proven" convention (i.e. sameSite cookies) is not preferrable - breaking from convention also introduces complexity that future developers may not appreciate (and may break due to their lack of understanding).

The front-end is Gatsby and is re-built by Netlify node.js robots regularly (i.e. it's not a static upload), it cannot (from my understanding) be housed within the symfony source - thus the front and back-end must be hosted seperately.

I think I have made the assumption that sameSite cookies have as strict a criteria as CORS (i.e. matching: subdomain, scheme, port etc), however it appears I can sub-domain the front-end (and use sameSite=Lax) as the Apex/TLD be be the same? I will try this!

Appreciate the thoughtful reply, it has been quite interesting learning and also reading the links.

Reply

Hey Fox C.!

> however it appears I can sub-domain the front-end (and use sameSite=Lax) as the Apex/TLD be be the same? I will try this!

Yes! You an do exactly this! Or keep the frontend on the main example.com domain and sub-domain the API - api.example.com - whatever you want. This is an absolutely normal, non-hacky, good SameSite cookie setup :).

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago | edited

Off topic. I got a question about Twig.
In base.html.twig:


{% block nav %}
{endblock}

{% block body %}
{endblock}

I want nav in nav.html.twig. Extend it and use parent() if needed. But it's not working. Do you know why? I coudn't really find an answer in the docs.

Reply

Hey Farry7,

If you have en empty nav block in your base.html.twig that you extend in another template - parent() will show you nothing, as your nav block is empty. Try this:


{# base.html.twig #}

{% block nav %}
    Parent content of this block
{% endblock %}

And then in another template:


{# homepage.html.twig #}

{% block nav %}
    {{ parent() }} {# Will render parent's content, i.e. literally will show this text "Parent content of this block" #}

    Another block content here...
{% endblock %}

And if in the controller you will render homepage.html.twig that extends that base template - you will see both: "Parent content of this block" and "Another block content here" strings :)

I hope this helps you to understand the Twig better.

Btw, we have a standalone course about Twig template engine: https://symfonycasts.com/screencast/twig - you may want to see it.

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | Victor | posted 2 years ago | edited

Hi Victor, thanks for the answer. However, I don't think you understood my question correctly.
I want the Nav to display in Base.html. At the same time I want the the Body to display underneath Nav in Base.html.twig. But the Nav is not showing up, the bodyis howing up. Eventhough the Body in Base.html.twig is empty as well.


{# base.html.twig #}

{% block nav %}
{endblock}

{% block body %}
{endblock}

-----------

{# nav.html.twig #}

{% extends 'base.html.twig' %}

{% block nav %}
    <nav><ul><li>Home</li></ul></nav>
{endblock}
Reply

Hey Farry7,

Yeah, I'm not sure I still understand it completely :) I bet if you will watch the Twig course I mentioned in the previous comment - you will understand things easily.

Since you're using template inheritance, i.e. extend base template in your nav template - everything will be in the final HTML output actually (not in base and not in nav - those will be rendered as an HTML page). What you should do - is render the final template instead of base template. I suppose in your controller you renders base.html.twig, but instead you should render your nav.html.twig - this way it will work, the "nav" block in that template will be rendered and inserted into the "nav" block of the base template, and only then that base template will be rendered as well to give you the final HTML page. That's how Twig inheritance works.

I hope this is clear for you now. If not, I really recommend to watch Twig course to know how Twig works.

Cheers!

Reply
Victor Avatar Victor | SFCASTS | posted 4 years ago | edited

Hey Avraham,

I suppose you successfully installed and activated the JMSSerializerBundle. If so, "JMS\Serializer\SerializerInterface" typehint should work. Actually, you can debug the autowiring with a special Symfony command:

$ bin/console debug:autowiring serializer

For me, the output is the next:


 Interface for array transformation.
 JMS\Serializer\ArrayTransformerInterface (jms_serializer.serializer)

 Serializer Interface.
 JMS\Serializer\SerializerInterface (jms_serializer.serializer)

So, as you can see, I can type hint with JMS\Serializer\SerializerInterface to get the object of JMS serializer. Do you have the same output in your project? Probably try to clear the cache?

Cheers!

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | Victor | posted 4 years ago

Hey Viktor,

$ bin/console debug:autowiring serializer
worked and I succeeded to autowire/inject

use JMS\Serializer\SerializerInterface;
_construct(SerializerInterface $serializer)

Thanks!!

Reply

Hey Avraham,

Glad it works now! Btw, I see you deleted the original comment in this thread... I understand that it was fixed for your case, but could you un-delete it? It might be helpful for others who will have similar problem. Thanks!

Cheers!

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | Victor | posted 4 years ago

Hey Victor!

Sorry for deleting original question, I was a bit confused.
Unfortunately I can't find option in Conversation UI to restore my original question.
Yet my question was about how to autowire/inject JMS serializer to constructor.
JMS serializer worked for me when creating instance
$serializer = \JMS\Serializer\SerializerBuilder::create()->build();
$json = $serializer->serialize($patient, 'json');

Yet I was curious how to install bundle and use dependency injection to inject to constructor
as reccomended in Symfony 4 best practices.

use JMS\Serializer\SerializerInterface;
_construct(SerializerInterface $serializer)

I had difficulties to follow JMSSerializerBundle installation instructions to be able
to inject JMS SerializationInterface,
finally it worked.

Thanks a lot!!

Reply

Hey Avraham,

Ah, ok, no problem! Fairly speaking I was not sure if author can restore them either. Anyway, thanks for trying to restore it this way!

Cheers!

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | posted 4 years ago

Hello!

I want to serialize Entity with relations, probably with many relations with Symfony serializer.
In my test, I have $patient has 2 employers,
also used group to focus on "firstName" field and "employers" relation only.
$json = $this->m_serializer->serialize( // maybe normalize as well
$patient,
'json', ['groups' => 'demographics']
);
Yet, I get empty relation objects.
{"firstName":"Sarah","employers":[[],[]]}

I want to get
{
"first_name":"Sarah",
"employers":
[
{"id":"1","name":"emp1 name","email":"emp1@a.com"},
{"id":"2","name":"emp2 name","email":"emp2@a.com"}
]
}

I have done it with JMS Serializer works great with entity with relations.
$serializer = \JMS\Serializer\SerializerBuilder::create()->build();
$json = $serializer->serialize($patient, 'json');
I have read conversation https://symfonycasts.com/sc...
I ask it here as I am working with Synfony 4.3

Is it possible to get same result
{
"first_name":"Sarah",
"employers":
[
{"id":"1","name":"emp1 name","email":"emp1@a.com"},
{"id":"2","name":"emp2 name","email":"emp2@a.com"}
]
}
with Symfony Serializer - not JMS ?
I suppose there is some configuration..

Thanks!

Reply

Hey Avraham M.

For getting data from a relationship you have to add the proper group to both sides. Let me show you with an example


Class User {
    /**
    * @Groups("user:read")
    */
    $address;
    ...
}

Class Address {
    /**
    * @Groups("user:read")
    */
    $street;
    ...
}

$json = $this->m_serializer->serialize(
    $user,
    'json', 
    ['groups' => 'user:read']
);

It should do the trick. Cheers!

Reply

Note: In Symfony 4.3, we fixed this problem by switching the serialization to a new class called PhpSerializer which uses PHP's native serialize() and unserialize() to serialize messages to a transport.

Reply

Hey tulik!

I think that's a great class too!... I wrote it ;). But that only helps in Messenger - you'll still need to install the serializer if you want $this->json() to be helpful. Or maybe I misunderstood - let me know!

Cheers!

Reply
Duilio P. Avatar
Duilio P. Avatar Duilio P. | posted 4 years ago | edited

Trying an alternative, I declared a toArray method in the User Entity to return the properties I wanted to publish:


    public function toArray()
    {
        return [
            'email' => $this->getEmail(),
            'firstName' => $this->getFirstName()
            'twitterUsername' => $this->getTwitterUsername(),
        ];
    }

And then, in the controller:


    return $this->json($this->getUser()->toArray());
Reply

Hey Duilio P.

For short representations of your objects it seems good but IMHO I prefer using the Serializer + groups. It's more robust and scalable

Cheers!

Reply

What Chrome extension are you using to format the json response in the browser? and what extensions do you advise to use?

Reply

Hey pjotrvanderhorst

I'm not sure what extesion Ryan is using but I'm using "Json Formatter" (https://github.com/callumlo... ) and it's great

Cheers!

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 4 years ago

Hello, I was not sure where to ask, hope this is a good place,

I have a problem with serializer under Symfony 4.1

First Error I got was:

- A circular reference has been detected when serializing the object of class "App\Entity\System" (configured limit: 1)

I have added a handler, and now I have this error:

- Maximum function nesting level of '256' reached, aborting!

I have no idea hot to fix it, I have tried MaxDeph but it didnt work.

[code]
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function($object){
return (string)$object;
});
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);

$json = $serializer->serialize($data, 'json', [
'enable_max_depth' => true
]);
[/code]

I have two entities, System and Line which refers to each other, I do not know how to configure serializer (or normalizer?) so it will not stuck in a infinitive loop, and will go just one level deep (from any relation). So if the line is a root level, then if he will go only to the System and ignore Lines from System.

I did some more debugging and it is trying to serialize whole object graph, instead just Line and System...

Reply

Hey @Krzysztof!

Ah... tricky! I’m sure we can debug this :). My guess is that you *did* solve the circular reference problem and the second error is something else. And, your last comment seems to support this: you said it’s serializing the entire object graph, not just Line and System. That *is* the default behavior of the serializer - to serializer all properties. Are there some properties that you do not want to serializer? And if so, are you using serialization Groups to limit the fields that you want to serializer?

I definitely think that just “too much” is being serialized... and probably more than you want/need is being serialized.

Let me know about the above questions :).

Cheers!

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | weaverryan | posted 4 years ago | edited

Thanks Ryan, yes it was serializing too much, but can this serializer be smart enough and just detect what data was passed and serialize only that? I was passing only Line and System and it was serializing everything as you said.

I have resolved this by passing $ignoredAttributes:


protected function createApiResponse($data, $statusCode = 200, $ignoredAttributes = [])
{
    $normalizer = new ObjectNormalizer();
    $normalizer->setCircularReferenceLimit(1);

    if ($ignoredAttributes) {
        $normalizer->setIgnoredAttributes($ignoredAttributes);
    }

    $normalizer->setCircularReferenceHandler(function($object){
        return (string)$object;
    });
    $serializer = new Serializer([$normalizer], [new JsonEncoder()]);

    $json = $serializer->serialize($data, 'json', [
        'enable_max_depth' => true,
    ]);

    return new JsonResponse($json, $statusCode, [], true);
}

Above was my first solution, after that I have realized that I do not need to pass objects, and I modified my query to return only arrays.

Reply

Hey Krzysztof K.!

Excellent! Happy you got it worked out! As you discovered, the serializer just serializes everything - the entire object graph of an object. The normal way to handle this is by using serialization groups, which we use in this chapter - https://symfonycasts.com/sc.... Basically, this allows you to tell the serializer which fields you do and don't want to serialize. It has the same effect (basically) as the ignoredAttributes, but I think it's a bit easier to use.

Oh, and by the way - I noticed that you're creating the normalize and serializer objects manually. Any reason for that? If you're using Symfony, there is already a serializer service (and SerializerInterface type-hint for autowiring) that you can use without any work. And if you use the serialization groups instead of the ignoredAttributes feature (which requires you to create your own normalizer), it should work perfectly.

Cheers!

Reply
Krzysztof K. Avatar

The reason is that I didn't know how to access ObjectNormalizer object from existing serializer service and I needed it to pass ignored attributes.

I was wondering now about this Group solution, but probably If I will put the same group across all of my Entities I will end up with nested loops again, and if I will create separate group per each entity it will not be a generic solution.

What I need it to somehow tell normallizer/serializer that it should go only one relation deeper from the root object, or only serialize objects which where given.

3 Reply

Hey Krzysztof K.!

Yea, I actually just has this conversation with someone last night :). There is a MaxDepth annotation you can use, but apparently it does not act like I would expect: it simply throws an exception if you reach the MaxDepth, it's not actually a way to *limit* the depth. I think this is something that may need some improvement in the serializer, to be honest.

So, you're right about the groups and nested loops. Because I don't think there is a way to tell the normalizer to only go *one* level deep beyond the root (I hope I'm wrong about this, but this is my impression - the serializer is something that I'm not an expert on), you would need to use groups in a "clever" way. For example, use a group called "output-system" on all the properties on System that should be serialized and all the properties on Lines that should be serialized WHEN System is the top-level. Then, when you are serializing System, you'll pass this "output-system" group. To avoid recursion, you would not put this group on the "system" property of the Lines entity.

It's honestly not a super satisfying answer - I'm going to ask around to see if I'm missing something :).

Cheers!

1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice