Potential Pitfall: Storing Models in Session Variables

Making my first ever post in an attempt to (hopefully) save someone else the hours of debugging I just went through!

TL;DR: If you use anonymous functions/closures as properties of your validation rules in a model, attempting to store an instance of that model in a session variable may silently fail and you will not see an exception; the object instance will simply vanish without warning!

I’ve been writing an e-commerce controller in Yii (the best framework I have ever used), and I needed to collect the user’s shipping information (name and address) to eventually pass along to a couple of 3rd-party APIs that will actually process the purchase. I don’t need to store this information in a database; I just need to “hold” it long enough to pass along for 3rd-party processing.

I generated the model/form classes to collect the shipping information (the model is surprisingly named [font=“Courier New”]ShippingAddress[/font]), and I added the necessary validation rules. Some of those rules needed to be conditional: the user needs to enter their First Name and Last Name, a Company name, or all of them. The First and Last Name can be left blank if a Company name is provided, and the Company name can be left blank if either a First or Last Name are provided. To establish those rules, I added functions/closures encapsulating the needed logic as the “[font=“Courier New”]when[/font]” property for the standard “[font=“Courier New”]required[/font]” validators I added to my ‘[font=“Courier New”]firstName[/font]’, ‘[font=“Courier New”]lastName[/font]’ and ‘[font=“Courier New”]company[/font]’ fields:




class ShippingAddress extends \yii\base\Model

{

  public $firstName;

  public $lastName;

  public $company;

/* ... snip ... */


  public function rules ( )

  {

	return [

 /* ... snip ... */

    	['firstName', 'required', 'when' => function ( $model ) {

        	return empty($model->company);

    	}, 'message'=>sprintf('{attribute} cannot be blank if %s is blank.', 

          	$this->getAttributeLabel('company')

    	)],

    	['lastName', 'required', 'when' => function ( $model ) {

        	return empty($model->company);

    	}, 'message'=>sprintf('{attribute} cannot be blank if %s is blank.', 

          	$this->getAttributeLabel('company')

    	)],

    	['company', 'required', 'when' => function ( $model ) {

        	return empty($model->firstName) && empty($model->lastName);

    	}, 'message'=>sprintf('{attribute} cannot be blank if %s and %s are blank.', 

          	$this->getAttributeLabel('firstName'),

          	$this->getAttributeLabel('lastName')

    	)],

    	[['address','city','state','zip'], 'required'],

  /* ... snip ... */



So far so good, right?

Now, I have to pass this information along to a subsequent controller action, so this object gets stored as a field in a [font="Courier New"]Cart[/font] object which I store as a session variable. This [font="Courier New"]Cart[/font] object has been bouncing happily along between requests in its session variable, along with several other necessary child objects. Its child objects get added and removed, and everything stays intact…

…Until I do a redirect from the action which receives and validates the shipping address information. When the redirect is performed and my next controller action attempts to read my [font="Courier New"]Cart[/font] object back from the session store — it has vanished completely without a trace!

I Googled and Googled and nothing was matching my problem. There was one page hit that pointed a finger at the attempt to redirect (although I my session had remained intact despite other redirects). I decided to add code to explicitly write and close my session right before the redirect where my [font=“Courier New”]Cart[/font] went “poof!”, and I did so by overriding the controller’s ‘[font=“Courier New”]redirect[/font]’ method:




public function redirect ( $url, $statusCode = 302 )

{

  \Yii::$app->session->close();

  return parent::redirect($url, $statusCode);

}



I re-ran my code, and that’s when I saw it. Yii now raised an exception at the point above where I explicitly closed my session: "Serialization of ‘Closure’ is not allowed."

My [font=“Courier New”]Cart[/font] contained a [font=“Courier New”]ShippingAddress[/font] instance which generated a rules array storing my conditional-validation functions as values. Functions/closures cannot be serialized, so the attempt to serialize my object in order to store it failed. (It didn’t matter that the array is supplied by a method call; stepping through with my debugger showed an array containing those closures being stored among my object’s instance variables.)

  • When PHP was left to implicitly write the session variables before closing my session, it silently ate this error (along with my [font="Courier New"]Cart[/font] object).
  • When I made the explicit request to write/close the session, the exception was properly thrown by PHP and was able to be caught by Yii.

I am about to rework the validations in my model to move all the validation code into class methods. ttfn…

I don’t know that a model should really be stored in a session variable. Skimmed your post, but maybe your not really storing a model? Your actually storing an Object such as $model = User::find(21); and saving $model in the session???

If you are doing this, then maybe you should refactor your code to pass the data more efficiently.

You could make an array of the values you need to pass, convert it to JSON, and store that. Or you could serialize it.

If what your storing is a LOT of information, then you could create a temp table for the data. Include a timestamp and have a cronjob run daily to delete anything older than 24 hours or something, and all you have to store in the session is the ID of the record in the temp table.

You could use jQuery and AJAX. Say it was a 2 step form. Step 1, enter your email. Step 2, enter a password. You could have 2 div sections, one for step 1 and one for step 2. When they click next, the page never actually reloads, it just hides step1 and shows step 2.

Honestly, my advice, if your storing a model in the session, then your doing something wrong and should come up with a better solution.