Do not abuse sessions
Sun 29 Mar 2020
We focus a lot on features and functionality as developers. We sometimes take shortcuts. I've been making a small shortcut. It might not seem like a big deal at first but it went under the radar because "it works". I've paid my technical debt today by going down the rabbit hole.
TL;DR Do not abuse session storage for large objects. Storing large objects might give you trouble later on. Objects get serialized when persisted in session storage. They also need to be unserialized when called from storage. This is where the issue lies. In my case, the user object got more complex and I ran into unserialize issues.
I need to give a little context. I was adding two features to my platform. One that would allow the user to favorite items for their profiles. The other, private messaging between two users of the platform. All my entities are Doctrine 2 entities and the application is written in Laminas (formerly ZF3).
As said before, I've been storing the user object into session storage. This normally would not be an issue with smaller objects, however, you start to run into issues when you add complexity.
Tracking down the issue
It took me around 6 to 7 hours to track down the core of the problem. At first, it gave me some weird exceptions. After stepping through the code with Xdebug I quickly realized it had something to do with the user in the session. The specific error message that leads me to my current solution was this one:
session_start(): failed to decode session object. session has been destroyed
The weird thing, to me at first at least, was that it broke on
session_start(). I couldn't figure it out. Then I referenced the good ol' php.net and saw the following:
When session_start() is called or when a session auto starts, PHP will call the open and read session save handlers. These will either be a built-in save handler provided by default or by PHP extensions (such as SQLite or Memcached); or can be custom handler as defined by session_set_save_handler(). The read callback will retrieve any existing session data (stored in a special serialized format) and will be unserialized and used to automatically populate the $_SESSION superglobal when the read callback returns the saved session data to PHP session handling.
I also saw in a Magento2 post that
session_start() also appeared to break when for example using the session variable to save binary data. This, i gandered, also had to do with the "special serialized format" that PHP uses to save objects into session storage. This lead me to my User object that had gotten more complex as more features are added to the application.
Funny enough my object always got stored into session storage. It always broke on the page after login. It would redirect to the next page and read session storage. That would be the moment that it broke. As a test I tried serializing and unserializing the object; this did not work. I also saw that the entity serialized takes about 740kiB. It has ManyToMany's and a couple of normal OneToOne relations, properties and common logic.
My conclusion was that this object simply is too large and serializing the whole object does not make much sense.
I started integrating Redis. This was planned to be sharing sessions over multiple instances anyway. Why would I do this? This allows me to simply be putting the bare information in the session cookie like an Id and some additional stuff. Now I can lazy load the user, store it in Redis and call it from the cache whenever I need it. My session storage is cleaner, my login page faster and information is relayed and stored in a more logical way for the application. As a side effect, it will be ready for multiple PHP instances to process requests.
Don't abuse sessions. I think every developer has read or learned that session storage is not meant for large objects. It does not make sense either. I accidentally ran into this issue as I've never made an application this robust and never ran into an issue where complexity would break serializing/unserializing objects.
The harsh debt of searching for the source of this problem also made me realize that the authenticated user simply should not be stored in session storage as a user object, from a security standpoint it does not make sense to do this.
It also does not make sense to do it this way because it might be harder to write additional logic to speed up your application, for example caching layers. It "worked" before as my user object only contained username, password, name and so forth. It did not exceed beyond a few fields.
Take this advice into account and don't abuse session storage.