Buy
Buy

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

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!

  • 2018-10-13 weaverryan

    Hey Krzysztof Krakowiak!

    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!

  • 2018-10-09 Krzysztof Krakowiak

    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.

  • 2018-10-08 weaverryan

    Hey Krzysztof Krakowiak!

    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!

  • 2018-10-08 Krzysztof Krakowiak

    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.

  • 2018-10-07 weaverryan

    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!

  • 2018-10-05 Krzysztof Krakowiak

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