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.