Friday, December 4, 2015

Flash Messages and Redirects in Yii2

Using Flash messages in Yii 1.1.* is something that I have done for so long, and so commonly, that I take it completely for granted. Whenever a user performs some action and the processing generates some information I want to convey back, I plug that information into a success, warning, info or error flash message, pop in a redirect for that situation and VOILA! After my processes are finished, the flash message is displayed to the user.

For example, in Yii 1.1.*, I have a handy action that is called if a user needs to retrieve their password from the database. It was written well back, around 1.0.6-ish, but still gets the job done nicely. Look up the user by the posted email, if the user is found, send the user an email and set a success message, otherwise, set an error message. Once that's done, redirect the user to the previous page. Otherwise, notify them that there was an error and keep going on.

When I converted this particular action to Yii2 it kept losing my flash messages. If I took out the redirect after successfully processing and sending the email, it was fine -- but I need the redirect to prevent inadvertent multiple form submissions etc. What to do?!? After scouring the internet and Yii forums, and finding multiple instances of people complaining about it not working but no good solutions or notes other than 'working as intended', I finally - FINALLY - realized that it is the redirect method itself that is, while stopping any output to the screen, not actually stopping the currently processed action. If I was more observant, I would have noticed it in the example of a controller redirect here: http://www.yiiframework.com/doc-2.0/guide-structure-controllers.html

If you are starting with Yii at version 2.* this may not trip you up at all. But if you're used to the old way, it's quite a sneaky gotcha!

So, the moral of the story is: $controller->redirect() DOES NOT exit the current action the way it did in Yii 1.1.*, in Yii 2.* you must return $this->redirect() instead!


The Yii 1.0.6+ (circa 2011) version (truncated for brevity :p):
/**
 * actionResetPassword
 * Send user a link to change their forgotten password
 */
 public function actionResetPassword()
 {
   $unknownUser = new User();

    if( isset($_POST['User']) )
    {
       /* ... do some processing here ... */                  
       if ( $found ){
          if( $registeredUser->sendStudentPassReminderEmail() )
             Yii::app()->user->setFlash('success', 'blah blah good');
          else
             Yii::app()->user->setFlash('error', 'blah blah bad');
          $this->redirect( Yii::app()->user->getReturnUrl() );
       }
       else
       {
          Yii::app()->user->setFlash('error', "Some Error Message");
       }
    }
  }

  Yii::app()->user->setReturnUrl(  Yii::app()->getRequest()->getUrlReferrer() );
  $this->render('sendPasswordResetCode', array('model'=>$unknownUser));
}


The Yii2 version becomes:
public function run()
{
  $unknownUser = new User();

  if ( $unknownUser->load(Yii::$app->request->post()) && $unknownUser->email )
  {
    if ( $registeredUser = User::find()->where(['email'=>$unknownUser->email])->one() )
    {
      if ( $this->sendStudentPassReminderEmail( $registeredUser ) )
      {
        Yii::$app->session->addFlash( 'success', 'blah blah good' );
        // THIS IS THE MAGIC LINE -- NOTE THE RETURN
        return $this->controller->redirect( Url::previous('before-forgot-password'));
      } else
      {
        Yii::$app->session->addFlash( 'error', 'blah blah bad' );
      }
    } else {
      Yii::$app->session->addFlash('error','Some Error Message');
    } 
  }

  Url::remember( Yii::$app->request->referrer ,'before-forgot-password' );
  return $this->controller->render( 'forgot-password' ,['model'=>$unknownUser]);
}

Sometimes, it's the little things in life that cause the biggest stumbles :p