FOSUserBundle User Entity Serialization Improvements


Coming towards the end of the particular project we have some tidy up to do - a little polish, and I do mean only a little :) There's still plenty of rough edges to smooth.

In this video we cover a problem where you want to output your FOSUserBundle entity as JSON.

But when converting your User entity to JSON - a process called serialization, or spelled correctly ;), serialisation - you end up with ... unexpected output.

This is best illustrated with an example.

Without restricting what gets serialized, returning a $user object is going to output similar to this:

{
  "id": 1,
  "username": "peter",
  "username_canonical": "peter",
  "email": "Peter@Test.Com",
  "email_canonical": "peter@test.com",
  "enabled": true,
  "salt": "jdo5081aybcws8ck4sgk4c0ssg4c8s0",
  "password": "$2y$13$CVnDR/WDbTbRIOtEblcDg.UCOTu.FADIyix93W/q2xxvXMxOAsaJC",
  "last_login": "2016-11-05T10:57:50+0000",
  "groups": [],
  "locked": false,
  "expired": false,
  "roles": [],
  "credentials_expired": false
}

Highly unlikely to be what you want.

It would be more realistic to restrict this down to a small subset of fields. Expose only as much as necessary, and no more.

There's this concept of canonical representations of some data:

{
  "email": "Peter@Test.Com",
  "email_canonical": "peter@test.com",
}

What does this even mean? As best I know, the canonical representation is a lowercased string representation of its similarly named field.

Again, as best I understand it, this is to ensure compatibility in the DBAL layer.

Back to the task in hand. Serialization should expose only some of those fields.

Let's start by enabling the serializer, if you haven't already done so:

# /app/config/config.yml

jms_serializer: ~

I have always worked on the principles of working from a whitelist when it comes to security. Blanket ban everything, and open only what is strictly necessary.

To 'connect' the serializer to our configuration, we first need to decide which entity we are supplying configuration for.

To add a layer of complexity to this issue, we are changing the serialization of a file we don't directly control.

I strongly advise when serializing your own entities that you use annotations. They are the easiest way to get started. If you want to go for raw speed, use XML. For developer experience, use annotations.

But yeah, we can't just stick our annoations all over FOSUserBundle's User model.

Instead, we can do this multiple ways.

As we are already extending the User model provided by FOSUserBundle, we could explicitly pull up some of the class properties from the BaseUser, and annotate:

<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use JMS\Serializer\Annotation as JMSSerializer;

/**
 * @ORM\Entity
 * @ORM\Table(name="app_user")
 *
 * @UniqueEntity("email")
 * @UniqueEntity("username")
 * @JMSSerializer\ExclusionPolicy("all")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     * @JMSSerializer\Expose
     * @JMSSerializer\Type("string")
     */
    protected $id;

    /**
     * @JMSSerializer\Expose
     * @JMSSerializer\Type("string")
     */
    protected $username;

    /**
     * @var string The email of the user.
     *
     * @JMSSerializer\Expose
     * @JMSSerializer\Type("string")
     */
    protected $email;

    /**
     * User constructor.
     */
    public function __construct()
    {
        parent::__construct();
    }
}

Or, if you don't want to do this, you can provide the same config via a .yml file.

In this case, we can start by excluding / hiding everything from serialization, and then selectively start exposing certain fields:

# /var/serializer/FOSUB/Model.User.yml

FOS\UserBundle\Model\User:
    exclusion_policy: all

And we must tell our jms_serializer config to take an active interest in this config:

# /app/config/config.yml

# JMS Serializer
jms_serializer:
    metadata:
        directories:
            FOSUB:
                namespace_prefix: FOS\UserBundle
                path: "%kernel.root_dir%/../var/serializer/FOSUB"

If we hit our profile endpoint now though, we aren't exposing anything, so we get:

{}

Polar opposites of the previous problem.

Let's expose the User's id:

# /var/serializer/FOSUB/Model.User.yml

FOS\UserBundle\Model\User:
    exclusion_policy: all
    properties:
    id:
        exclude: false

You may need to clear the environment cache at this point, so php bin/console cache:clear -e=dev, or whatever env you are working in.

Then now you should be seeing:

{
  "id": 1
}

Much better.

Let's go back to the issue of exposing the canonical version of the email_canonical.

It could be that our user joined with the email of "Peter@Test.Com", because he just likes to watch the world burn.

We don't want to display such a monstrosity on our beautiful front end. The lowercased canonical version is much more visually appealing.

Honestly, in my opinion you should expose it back to them as they gave at registration. This is their own information, don't mess with it.

If the designer must mess with it, let them use a JavaScript .toLowerCase(), or whatever. Pass that damn buck.

# /var/serializer/FOSUB/Model.User.yml

FOS\UserBundle\Model\User:
    exclusion_policy: all
    properties:
    id:
        exclude: false
    emailCanonical:
        exclude: false
        serialized_name: email

Giving the output:

{
  "id": 1,
  "email": "peter@test.com"
}

There's quite a number of possibilities opened up with serialization. I would point you towards serialization groups as another immediately useful concept.

Code For This Course

Get the code for this course.

Episodes