Buy

User Serialization

There’s a problem.

I have to bother you quickly with a little issue of serialization. When a user logs in, the User entity is stored in the session. For this to work, PHP serializes the User object to a string at the end of the request and stores it. At the beginning of the request, that string is unserialized and turned back into the User object.

Note

If you’re feeling really curious, the class that serializes and deserializes the user information is called ContextListener.

This is great! And it’s obviously working great - we’re surfing around as Wayne the admin. But there’s a “gotcha” in Doctrine. Sometimes, Doctrine will stick some extra information onto our entity, like the entity manager: that big important object we used to save things.

Normally, we don’t care about this, but when the User object is serialized, having that big object hidden in our entity causes serialization to fail. The entity manager contains a database connection and other information that just can’t be serialized.

Using the Serializable Interface

We need to help Doctrine out. Start by adding the Serializable interface to the User class. This core PHP interface has two methods: serialize and unserialize:

// src/Yoda/UserBundle/Entity/User.php
// ...

use Serializable;

class User implements AdvancedUserInterface, Serializable
{
    // ...

    public function serialize()
    {
        // todo - do some mad serialization
    }

    public function unserialize($serialized)
    {
        // todo - and some equally angry de-serialization
    }
}

When the User object is serialized, it’ll call the serialize method instead of trying to do it automatically. When the string is deserialized, the unserialize method is called. This may seem odd, but let’s just return the id, username and password inside an array for serialize. For unserialize, just put those 3 values back on the object:

// src/Yoda/UserBundle/Entity/User.php
// ..

public function serialize()
{
    return serialize(array(
        $this->id,
        $this->username,
        $this->password,
    ));
}

public function unserialize($serialized)
{
    list (
        $this->id,
        $this->username,
        $this->password,
    ) = unserialize($serialized);
}

If you think about it, this should kinda break everything. When Symfony gets the User object from the session and deserializes it, our User will have lost some of its data, like roles and isActive. That’s not cool!

Clearly that’s not the case: Symfony’s security system is smart enough to take the id and query for a full fresh copy of the User object on each request.

We can see this right in the web debug toolbar: once a user is logged in, each request has a query that grabs the current user from the database. So, we’re good!

Leave a comment!

  • 2016-06-09 weaverryan

    Hey BondashMaster!

    Hmm, this looks weird to me - especially because it's random :/. At the end of every request, your User object is serialized to the session (actually, a "token" object is serialized to the session, and this contains your User - but effectively, your User is serialized to the session). Then, at the beginning of the next request, it's deserialized. This error is coming from the line that deserializes the User.

    This is super weird - and makes me wonder if your specific PHP version might have a bug in it that's causing this. It basically looks like the serialized user data becomes corrupted, or PHP thinks it's corrupted for some reason.

    Unfortunately, that's just my best guess.

  • 2016-06-09 BondashMaster

    I'm getting this error. But appears randomly :/
    "CRITICAL - Uncaught PHP Exception Symfony\Component\Debug\Exception\ContextErrorException: "Warning: Erroneous data format for unserializing 'MenugenBundle\Entity\User'" at C:\Users\AAlvarez\SECURITY\vendor\symfony\symfony\src\Symfony\Component\Security\Core\Authentication\Token\AbstractToken.php line 164 "

  • 2015-07-15 weaverryan

    Hey Richard!

    Yes, this is effectively where the problems start indeed. It's with proxies - the magic way that `$user->getEvents()` is able to work (since in reality, the whole giant Entity manager is inject into User, that can't be serialized, etc etc etc).

    It's a big unfortunate side effect of the Doctrine magic. But I don't think that Doctrine can fix it. For them - it's a feature (and they're right). I would love a "smoother" path around this problem, but I can't think of anything...

    Cheers!

  • 2015-07-15 Richard

    Hi Ryan,
    Doesn't the serialization problem starts when you point the entity Event to the entity User (I mean, when you give them the one-to-many relationship)? Maybe the "Symfony2 Security Manager" doesn't like that.
    I would like to know, because otherwise the elderlies of Doctrine should fix this.

  • 2015-04-19 Łukasz Zaroda

    Duh, I sit down to this just now and instantly found out a rouge piece of code in my controller, I had to be literally blind to miss this.

    $userRepo = $em->getRepository('UserBundle:User');
    $user = $userRepo->findOneByUsernameOrEmail('darth@deathstar.com');

    So the first query was an authentication and second was this. I'm embarassed, too sleepy then I guess, but wanted to close this case :) . Thanks for this amazing tutorial, it is what literally allows me to start with symfony2.

  • 2015-04-13 weaverryan

    Hey Łukasz!
    Ah, the controller! That's very interesting! Are you doing a security check? And if so, if you remove it, does the extra query go away? If there is a security check, and removing it removes the extra query, then the cause *is* related to Symfony's security system not properly seeing your user as non-refreshed. Let me know what you find.

    Cheers!

  • 2015-04-12 Łukasz Zaroda

    It didn't make it go away. http://i.imgbox.com/gSkc6SRv.p... This is my timeline, it happens during executing controllers as you can see. I'm too sleepy right now (it's about midnight here), but if I will find some time I can try to debug internals for this, to know what is actually happening. :)

  • 2015-04-12 weaverryan

    Hmm, I'm not sure then. I'm guessing if you check the timeline of the profiler, you'll see bot queries (their 2 little bars) show up way before your controller, probably during the Firewall listener. That would prove that it's an issue with "refreshing" the user.

    Another thing you can try: implement EquatableInterface in your User, and just return true. This is related to the "refreshing" step - Symfony tries to see if your user has been modified, and if you implement this interface, it'll call this method, instead of trying to compare certain fields in the serialized user with the one your queried for. If *this* makes the query go away, then we know the issue is related to the user refresh. If the query remains, then we will definitely know my theory is wrong :). And then the next question would be - where does the second query show up during the timeline?

    Cheers!

  • 2015-04-12 Łukasz Zaroda

    Unfortunately, it doesn't seem to be related to custom serialization, as even when I remove "Serialization" interface - the extra query is still there. When I added as serialized, one by one, the rest of fields - the problem also persisted. I'm using the 2.6.5 version of Symfony2. :)

  • 2015-04-12 weaverryan

    Hey Łukasz!

    I don't know immediately, but maybe we can figure it out :). I recognize the first as the query Symfony runs early when it's trying to "refresh" the user (i.e. take the user "id" stored in the session, and query for a new one). The second query is interesting, it's the one that *we* wrote that should be used on initial login - where it queries for username OR email. There is some weird behavior/a bug in Symfony, that I bet this might be hitting (btw, I can't repeat this behavior locally on my version of this project, which uses an older Symfony - 2.4.2). Specifically, the first query works, but the user appears "modified", which causes a second query. If I'm right, then the extra query will go away when/if you add the AdvancedUserInterface fields your serialize and unserialize methods. For us, specifically, we need to add isActive (just like we do in the docs here: http://symfony.com/doc/current....

    I believe that should fix it - but let me know :). Basically, without these fields, the user looks like it is not quite the same one between requests, and does the extra query. It honestly needs to be handled a little better in the Symfony core - it's been on my list for awhile.

    Cheers!

  • 2015-04-12 Łukasz Zaroda

    Hmm, anyone have any idea why after login there run TWO queries, fetching our user? I mean:

    "SELECT t0.id AS id1, t0.username AS username2, t0.email AS email3, t0.password AS password4, t0.roles AS roles5, t0.isActive AS isActive6 FROM yoda_user t0 WHERE t0.id = 7"

    AND:

    "SELECT y0_.id AS id0, y0_.username AS username1, y0_.email AS email2, y0_.password AS password3, y0_.roles AS roles4, y0_.isActive AS isActive5 FROM yoda_user y0_ WHERE y0_.username = 'darth@deathstar.com' OR y0_.email ='darth@deathstar.com'"

    Both queries are basically always return the same things. Any ideas? :)

  • 2015-03-26 Dizzy Bryan High

    Found the issue, i'd used phpstorm to override the methods, but had initially inserted into the wrong place, i had deleted the functions but had missed out deleting the annotations so i had:

    /**
    * @var integer
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    /**
    * (PHP 5 >= 5.1.0)

    * String representation of object
    * @link http://php.net/manual/en/seria...
    * @return string the string representation of the object or null
    */

    private $id;

    I deleted the extra annotations, and unstrangely it worked!

  • 2015-03-26 Dizzy Bryan High

    Having a problem implementing the serialize methods:

    As soon as i add the:
    class User implements AdvancedUserInterface, \Serializable

    and add the extra methods:

    /**
    * (PHP 5 >= 5.1.0)

    * String representation of object
    * @link http://php.net/manual/en/seria...
    * @return string the string representation of the object or null
    */
    public function serialize() {
    return serialize(array(
    $this->id,
    $this->username,
    $this->password
    ));
    }

    /**
    * (PHP 5 >= 5.1.0)

    * Constructs the object
    * @link http://php.net/manual/en/seria...
    * @param string $serialized


    * The string representation of the object.
    *


    * @return void
    */
    public function unserialize($serialized) {
    list(
    $this->id,
    $this->username,
    $this->password
    ) = unserialize($serialized);
    }

    I get the following error when i refresh the page:

    MappingException in MappingException.php line 52:No identifier/primary key specified for Entity "Yoda\UserBundle\Entity\User". Every Entity must have an identifier/primary key.

  • 2015-03-11 two_mart_toe

    WOW, super fast detailed response to my simple request. Definitely cleared the fog. Cool! keep up the good stuff.

  • 2015-03-10 weaverryan

    Hey there!

    Yes, there is something odd that happens here. Here are some details:

    1) You should serialize the password, username salt AND all the values from the AdvancedUserInterface (isEnabled, isAccountNonExpired, etc). I'm not showing this exactly in the tutorial, and it causes this weird behavior. But it's not a big deal.

    2) When Symfony loads, it checks to see if the user in the session "has changed". I won't go into details why (it's complex and I'm not sure I totally understand it). If It *has* changed (meaning any of the properties in (1) are different... or you didn't serialize one of them, so things look different), then the user becomes "not authenticated".

    3) A few parts in Symfony - one of them being the part executes the `access_control` areas - re-authenticate the user if the user looks "not authenticated". This makes everything "green" and ok again.

    So what you're seeing is a cause of not serializing all the fields in (1) and an idiosyncrasy of Symfony. But, as far as I know (and I've seen a lot of Symfony sites), it's harmless, and it's probably something we should fix in core. The takeaway is to (a) add the extra fields to serialize in (1) and (b) not worry about it :).

    Cheers!

  • 2015-03-10 two_mart_toe

    To add, authenticated green and yes only appears for the paths defined in access_control

  • 2015-03-10 two_mart_toe

    I have the same problem. Strange. If i add $is_active = true, no problem. If it's just $is_active; (in the user model) then I get the yellow, logged in ok, authenticated no. Some voodoo is going on here. I dont think removing access control is the answer

  • 2015-01-23 Diego Aguiar

    Actually you are right!
    I just removed my access_control entirely, cleared cache and tried again. It worked

    I don't know what were causing it, but anyway thanks for your help ;]

  • 2015-01-23 weaverryan

    Hey Diego!

    That's odd! It doesn't make sense to me .... Usually, "Authenticated: No" happens only when you have a firewall that doesn't cover the URL that you're going to. Adding the `access_control` shouldn't make a difference. Did you have any other `access_control` entries?

    But overall, I wouldn't worry about it - this seems like one of those times when something is *barely* wrong, and you might sink a lot of time into finding out what it is, only to find out it was some silly configuration issue. If it works, cool :).

    Cheers!

  • 2015-01-23 Diego Aguiar

    Oh God!

    You need to specify this in order to get Authenticated no matter what.

    #app/config/security.yml
    access_control:
    - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

    I feel kinda (Insert your favorite insult here) ...

  • 2015-01-23 Diego Aguiar

    Hi there!

    I was re-doing login section (after loading users to DB) and when I try to login I get logged but not authenticated, profile bar is yellow and says "Authenticated: No"

    In the logs I got this line:

    #security.INFO: User "Diego" has been authenticated successfully [] []

    I have everything just like in this episode (Security, User Entity, UserRepository). I have been searching for a while and I just can't fix this problem. Any advice on this is really appreciatted

    Thanks in advance.