Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Persist Form Data Across Multiple Steps in Laravel

Tags:

forms

php

laravel

When I've made multistep forms in the past I would generally store the form data in the session before returning it to the view, that way the data persists if the user refreshes the page or clicks the browser's native back buttons.

Transferring my past logic to Laravel I built the following form consisting of three stages:

[Input -> Confirm -> Success]

Routes.php

Route::group(array('prefix' => 'account'), function(){
    Route::get('register', array(
        'before'  => 'guest',
        'as'      => 'account-create',
        'uses'    => 'AccountController@getCreate'
    ));

    Route::post('register', array(
        'before'  => 'guest|csrf',
        'as'      => 'account-create-post',
        'uses'    => 'AccountController@postCreate'
    ));

    Route::get('register/confirm', array(
        'before'  => 'guest',
        'as'      => 'account-create-confirm',
        'uses'    => 'AccountController@getCreateConfirm'
    ));

    Route::post('register/confirm', array(
        'before'  => 'guest|csrf',
        'as'      => 'account-create-confirm-post',
        'uses'    => 'AccountController@postCreateConfirm'
    ));

    Route::get('register/complete', array(
        'before'  => 'guest',
        'as'      => 'account-create-complete',
        'uses'    => 'AccountController@getCreateComplete'
    ));
});

AccountController.php

<?php
class AccountController extends BaseController {

  private $form_session = 'register_form';

  public function getCreate() 
  {
      if(Session::has($this->form_session)) 
      {
          // get forms session data
          $data = Session::get($this->form_session);

          // clear forms session data
          Session::forget($this->form_session);

          // load the form view /w the session data as input
          return View::make('account.create')->with('input',$data);
      }

      return View::make('account.create');
  }

  public function postCreate() 
  {
      // set the form input to the session
      Session::set($this->form_session, Input::all());

      $validation_rules = array(
          'email'         => 'required|max:50|email|unique:users',
          'password'      => 'required|max:60|min:6',
          'password_conf' => 'required|max:60|same:password'                    
      );

      $validator = Validator::make(Input::all(), $validation_rules);

      // get forms session data
      $data = Session::get($this->form_session);

      // Return back to form w/ validation errors & session data as input
      if($validator->fails()) {
        return  Redirect::back()->withErrors($validator);
      } 

      // redirect to the confirm step
      return Redirect::route('account-create-confirm');
  }

  public function getCreateConfirm() 
  {
      // prevent access without filling out step1
      if(!Session::has($this->form_session)) {
        return Redirect::route('account-create');
      }

      // get forms session data
      $data = Session::get($this->form_session);

      // retun the confirm view w/ session data as input
      return View::make('account.create-confirm')->with('input', $data);
  }

  public function postCreateConfirm() 
  {
      $data = Session::get($this->form_session);

      // insert into DB
      // send emails 
      // etc.

      // clear forms session data
      Session::forget($this->form_session);

      // redirect to the complete/success step
      return Redirect::route('account-create-complete');
  }

  public function getCreateComplete() {
      return View::make('account.create-complete');
  }
}

create.blade.php

<form action="{{ URL::route('account-create-post') }}" method="post">

    Email: <input type="text" name="email" value="{{ (isset($input['email'])) ? e($input['email']) : '' }}">
    @if($errors->has('email'))
        {{ $errors->first('email') }} 
    @endif
    <br />

    Password: <input type="text" name="password" value="">
    @if($errors->has('password'))
        {{ $errors->first('password') }} 
    @endif
    <br />

    Password Confirm: <input type="text" name="password_conf" value="">
    @if($errors->has('password_conf'))
        {{ $errors->first('password_conf') }} 
    @endif     
    <br />

    {{ Form::token() }}

    <input type="submit" value="Confirm">

</form>

create-confirm.blade.php

Email: {{ $input['email']; }}
Password: {{ $input['password']; }}

<form action="{{ URL::route('account-create-confirm-post') }}" method="post">
    {{ Form::token() }}
    <a href="{{ URL::previous() }}">return</a> 
    <input type="submit" name="submit_forward" value="Submit">
</form>

The above works fine, however I am wondering if this is the best way to approach multi-step forms in Laravel?

like image 485
Jeemusu Avatar asked Mar 13 '14 07:03

Jeemusu


2 Answers

When I have created multi-part forms, I have always done it in a way so that the user can always come back and finish the form later, by making each form persist what it has to the database.

For instance


Step 1 - Account Creation

I would have the user create their authentication details at this step, create the user account (with password) here and also log the user in, redirecting to the dashboard. There I can do a check to see if the user has a profile and if they don't, redirect them to the profile creation form.

Step 2 - Profile Creation

Because we have an authenticated user, the profile creation form can save its data to the currently logged in user. Subsequent sections follow the same process but check the existence of the previous step.


Your question seems to be about confirming whether a user wishes to create an account. What I would do in your situation would be, on the form you created to confirm the user account, I would keep the user's data in hidden input fields.

Email: {{ $input['email'] }}
Password: {{ $input['password'] }}

<form action="{{ URL::route('account-create-confirm-post') }}" method="post">
    <input type="hidden" name="email" value="{{ $input['email'] }}">
    <input type="hidden" name="password" value="{{ $input['password'] }}">
    {{ Form::token() }}
    <a href="{{ URL::previous() }}">return</a> 
    <input type="submit" name="submit_forward" value="Submit">
</form>

Although displaying the user's chosen password back to them on this page seems to be a bit superfluous when you ask them to confirm their password on the previous page, plus some users might question why their password is being shown in plaintext on the screen, especially if they are accessing the site from a public computer.


The third option I would suggest would be to create the user account and soft-delete it (Laravel 4.2 Docs / Laravel 5 Docs), returning the user's account number to the new form:

Email: {{ $input['email'] }}
Password: {{ $input['password'] }}

<form action="{{ URL::route('account-create-confirm-post') }}" method="post">
    <input type="hidden" name="id" value="{{ $user_id }}">
    {{ Form::token() }}
    <a href="{{ URL::previous() }}">return</a> 
    <input type="submit" name="submit_forward" value="Submit">
</form>

then undo the soft-delete when the user confirms their account. This has the added bonus that you could track people trying to sign up multiple times for an account and not completing the process and see if there's a problem with your UX.


Conclusion

Of course, you could also still do it the way you always have with a session, all I have tried to do here is show you some other ways you can approach it, as with everything to do with the best way of doing something, this is a highly opinionated subject and is likely to get many opposing views on how it should be done. The best way to do it is the way that works best for you and your users... mainly your users.

like image 172
Andrew Willis Avatar answered Oct 21 '22 10:10

Andrew Willis


There are two ways to do it (that i can think of). I prefer second one.

  1. Client side - everything can be handled by javascript. Basic validation (if field is email, if field has enough characters etc.) would be checked with javascript. After confirmation, AJAX request would go through server side validation and if anything went wrong you could highlight invalid inputs. "check if email is available" button (via AJAX) would be great too.
  2. Server side - pretty much what you did but I would move it to service - it would make it much cleaner.

 public function getCreate() {
      if ($this->formRememberService->hasData()) {
           return View::make('account.create')
                ->with('input', $this->formRememberService->getData());
      }
      return View::make('account.create');
 }

 public function postCreate() {
      $this->formRememberService->saveData(Input::all());
      // ...
 }

 public function postCreateConfirm() {
      // ...
      $this->formRememberService->clear();
      return Redirect::route('account-create-complete');
 }

Adding "forget me" action would be nice (especially if form requires more private data).

Why getCreate() has Session::forget()? If someone goes back to change something and accidently leaves your site his data will be lost.

like image 42
Daniel Antos Avatar answered Oct 21 '22 08:10

Daniel Antos