Formulaire multi-étapes

J’ai eu une question récemment concernant un formulaire multi-étapes, autrement dit un formulaire qui nécessite plusieurs pages ou une extension à chaque soumission pour récolter des informations supplémentaires. J’ai rassemblé le code d’exemple de ce chapitre ici. Il suffit de le télécharger, de le décompresser dans un dossier d’un serveur, et de lancer composer install. Normalement tout devrait bien fonctionner !

Il y a plusieurs façons d’envisager un formulaire multi étapes, je suis parti sur la plus simple à mon sens. Je propose un formulaire d’enregistrement pour un utilisateur dans lequel j’ai prévu le choix du pays. S’il choisit un pays autre que la France l’enregistrement est immédiat. Par contre s’il choisit la France je charge un nouveau formulaire pour demander la région et une information. J’ai opté pour un rechargement complet de la page mais il serait évidemment possible de faire une régénération partielle en Ajax.

Pour la structure de base de l’authentification je suis parti de mon package scafold. De cette façon l’intendance de base est déjà en place, je n’ai eu plus qu’à apporter les modifications utiles pour le formulaire en deux étapes.

Avec requête au serveur

Voyons comment procéder avec questionnement du serveur pour le second formulaire.

Le fonctionnement

Voyons déjà le fonctionnement global que vous pouvez expérimenter en installant l’archive comme je l’ai indiqué ci-dessus. J’ai ajouté un lien pour la connexion sur la page d’accueil standard de Laravel :

img08En cliquant on arrive sur la page de connexion classique :

img09En cliquant sur « Register » on a enfin le formulaire d’enregistrement :

img10Au données classique j’ai ajouté le pays dans une liste de choix. Si le pays est autre que la France c’est l’enregistrement classique par contre si c’est la France on reçoit un nouveau formulaire :

img11Lorsque ces informations sont données l’enregistrement se fait.

Vous avez vu le principe ? Bon alors on va voir un peu comment ça se passe dans la machinerie.

La base de données

Pour simplifier j’ai prévu une base de donnée sqlite. Le fichier est présent ici :

img12

Il est présent dans l’archive et vous n’avez rien à faire de spécial. La migration pour les utilisateurs est celle-ci :

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->text('infos')->nullable();
            $table->string('country', 2);
            $table->string('region', 255)->nullable();
            $table->string('password', 60);
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('users');
    }
}

Personnellement j’utilise le module SQLite Manager de Firefox pour gérer facilement les bases sqlite :

img13

Le module est bien fait et permet de gérer efficacement la base. Il va nous permettre de suivre le processus d’enregistrement.

Les champs que j’ai adoptés ne sont pas vraiment efficaces, c’est juste pour l’exemple.

Les routes

Le package scafold a déjà toutes les routes de base pour l’authentification (vendor/bestmomo/scafold/src/Http/routes.php) :

// Authentication routes...
Route::get('auth/login', 'Auth\AuthController@getLogin');
Route::post('auth/login', 'Auth\AuthController@postLogin');
Route::get('auth/logout', 'Auth\AuthController@getLogout');

// Registration routes...
Route::get('auth/register', 'Auth\AuthController@getRegister');
Route::post('auth/register', 'Auth\AuthController@postRegister');

Mais comme on va avoir deux étapes pour l’enregistrement je vais prévoir deux autres routes dans app/Http/routes.php :

Route::get('auth/registerbis', 'Auth\AuthController@getRegisterBis');
Route::post('auth/registerbis', 'Auth\AuthController@postRegisterBis');

Je n’ai pas été très original dans l’appellation, je me suis contenté d’ajouter « bis ».

Le contrôleur

Le contrôleur AuthController va avoir tout le travail. Voici le code complet :

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Registration & Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users, as well as the
    | authentication of existing users. By default, this controller uses
    | a simple trait to add these behaviors. Why don't you explore it?
    |
    */

    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    /**
     * Create a new authentication controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest', ['except' => 'getLogout']);
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|confirmed|min:6',
        ]);
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validatorBis(array $data)
    {
        return Validator::make($data, [
            'infos' => 'required|max:1000'
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'country' =>$data['country'],
            'password' => bcrypt($data['password']),
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function createBis(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'country' =>$data['country'],
            'infos' => $data['infos'],
            'region' => $data['region'],
            'password' => bcrypt($data['password']),
        ]);
    }

    /**
     * Show the application registration form.
     *
     * @return \Illuminate\Http\Response
     */
    public function getRegister()
    {
        return view('auth.register', ['countries' => getCountries()]);
    }

    /**
     * Show the application registration form.
     *
     * @return \Illuminate\Http\Response
     */
    public function getRegisterBis()
    {
        return view('auth.registerbis', ['regions' => getRegions()]);
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function postRegister(Request $request)
    {
        $validator = $this->validator($request->all());

        if ($validator->fails()) {
            $this->throwValidationException(
                $request, $validator
            );
        }

        if($request->country == 'FR') {
            session()->put('regist', $request->only(['name', 'email', 'country', 'password']));
            return view('auth.registerbis', ['regions' => getRegions()]);
        }

        auth()->login($this->create($request->all()));

        return redirect($this->redirectPath());
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function postRegisterBis(Request $request)
    {
        $validator = $this->validatorBis($request->all());

        if ($validator->fails()) {
            return redirect('auth/registerbis')
                ->withInput()
                ->withErrors($validator);
        }

        $request->merge(session('regist'));

        auth()->login($this->createBis($request->all()));

        return redirect($this->redirectPath());
    }

}

On va voir tout ça en détail…

Le formulaire d’enregistrement

On a vu plus haut que dans le formulaire d’enregistrement il est prévu la liste des pays du monde. J’ai prévu un fichier helpers :

img14

Avec une fonction pour renvoyer un tableau des pays :

if (!function_exists('getCountries')) {
	function getCountries()
	{
		return [
		"AF" => "Afghanistan (‫افغانستان‬‎)",
		"AX" => "Åland Islands (Åland)",
		"AL" => "Albania (Shqipëri)",

                ...

		"YE" => "Yemen (‫اليمن‬‎)",
		"ZM" => "Zambia",
		"ZW" => "Zimbabwe"
		];
	}
}

J’ai prévu le code du pays comme clé et son nom comme valeur.

Pour mémoire pour que ce fichier soit connu de Laravel il faut le prévoir dans le chargement automatique, donc dans composer.json :

"autoload": {
    "classmap": [
        "database"
    ],
    "files": [
        "app/helpers.php"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},

Il a fallu surcharger la fonction getRegister du contrôleur pour envoyer ce tableau dans la vue :

public function getRegister()
{
    return view('auth.register', ['countries' => getCountries()]);
}

Et dans la vue on parcourt le tableau pour créer la liste :

<div class="form-group">
	<label class="col-md-4 control-label">Country</label>
	<div class="col-md-6">
		<select class="form-control" name="country">
			@foreach($countries as $key => $value)
				<option value="{{ $key }}" {{ old('country') == $key ? 'selected' : '' }}>{{ $value }}</option>
			@endforeach
		</select> 
	</div>
</div>

Notez le code pour rendre l’option choisie active en cas d’erreur de validation avec renvoi des informations.

img10

Enregistrement classique

La fonction postRegister du contrôleur est chargée du traitement de la soumission :

public function postRegister(Request $request)
{
    $validator = $this->validator($request->all());

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    if($request->country == 'FR') {
        session()->put('regist', $request->only(['name', 'email', 'country', 'password']));
        return view('auth.registerbis', ['regions' => getRegions()]);
    }

    auth()->login($this->create($request->all()));

    return redirect($this->redirectPath());
}

La partie intéressante se situe au niveau du test du pays. Si ce n’est pas la France on a le code standard. Par contre si c’est la France on mémorise les données saisies en session et on envoie le second formulaire.

Par exemple on a cette saisie :

img15

On le retrouve bien dans la base :

img16

Et évidemment les champs infos et region ne sont pas renseignés.

Le second formulaire

Dans le fichier helpers j’ai aussi mis les régions françaises :

if (!function_exists('getRegions')) {
	function getRegions()
	{
		return [
			'Alsace',
			'Aquitaine',
			'Auvergne',
			'Basse-Normandie',
			'Bourgogne',
			'Bretagne',
			'Centre',
			'Champagne-Ardenne',
			'Corse',
			'Franche-Comté',
			'Haute-Normandie',
			'Ile-de-France',
			'Languedoc-Roussillon',
			'Limousin',
			'Lorraine',
			'Midi-Pyrénées',
			'Nord-Pas-de-Calais',
			'Pays de la Loire',
			'Picardie',
			'Poitou-Charentes',
			'Provence-Alpes-Côte-d\'Azur',
			'Rhône-Alpes',
			'DOM'
		];
	}
}

Le formulaire est construit avec ce code :

<form class="form-horizontal" role="form" method="POST" action="{{ url('/auth/registerbis') }}">
	{!! csrf_field() !!}

	<div class="form-group">
		<label class="col-md-4 control-label">Comment nous avez-vous connus ?</label>
		<div class="col-md-6">
			<textarea class="form-control" name="infos"></textarea>
		</div>
	</div>

	<div class="form-group">
		<label class="col-md-4 control-label">Quelle est votre région ?</label>
		<div class="col-md-6">
			<select class="form-control" name="region">
				@foreach($regions as $value)
					<option value="{!! $value !!}" {{ old('region') == $value ? 'selected' : '' }}>{{ $value }}</option>
				@endforeach
			</select> 
		</div>
	</div>

	<div class="form-group">
		<div class="col-md-6 col-md-offset-4">
			<button type="submit" class="btn btn-primary">
				Register
			</button>
		</div>
	</div>
</form>

Il y a encore une boucle pour créer la liste de choix des régions avec la même précaution pour sélectionner la bonne en cas de retour après erreur de validation.

img11

Enregistrement avec le second formulaire

La fonction postRegisterBis du contrôleur est chargée du traitement de la soumission :

public function postRegisterBis(Request $request)
{
    $validator = $this->validatorBis($request->all());

    if ($validator->fails()) {
        return redirect('auth/registerbis')
            ->withInput()
            ->withErrors($validator);
    }

    $request->merge(session('regist'));

    auth()->login($this->createBis($request->all()));

    return redirect($this->redirectPath());
}

Si la validation est correcte on ajoute les données qu’on avait gardées en session, on crée l’utilisateur et on le connecte.

Par exemple on a cette saisie avec le premier formulaire :

img17

A la soumission on reçoit ce formulaire que l’on complète aussi :

img18

A la soumission on a maintenant les deux enregistrements dans la base :

img19

Cette fois les champs infos et region sont renseignés pour le deuxième utilisateur enregistré.

Traitement côté client

Il n’est pas toujours judicieux de multiplier les requêtes au serveur alors voyons comment réaliser le même exemple mais en traitant les deux étapes directement dans le client. Il suffit de disposer de toutes les informations nécessaires et d’adapter le formulaire en conséquence. Voici la nouvelle vue pour l’enregistrement :

@extends('app')

@section('content')
<div class="container-fluid">
	<div class="row">
		<div class="col-md-8 col-md-offset-2">
			<div class="panel panel-default">
				<div class="panel-heading">Register</div>
				<div class="panel-body">
					@if (count($errors) > 0)
						<div class="alert alert-danger">
							<strong>Whoops!</strong> There were some problems with your input.<br><br>
							<ul>
								@foreach ($errors->all() as $error)
									<li>{{ $error }}</li>
								@endforeach
							</ul>
						</div>
					@endif

					<form class="form-horizontal" role="form" method="POST" action="{{ url('/auth/register') }}">
						{!! csrf_field() !!}

						<div class="form-group">
							<label class="col-md-4 control-label">Name</label>
							<div class="col-md-6">
								<input type="text" class="form-control" name="name" value="{{ old('name') }}">
							</div>
						</div>

						<div class="form-group">
							<label class="col-md-4 control-label">E-Mail Address</label>
							<div class="col-md-6">
								<input type="email" class="form-control" name="email" value="{{ old('email') }}">
							</div>
						</div>

						<div class="form-group">
							<label class="col-md-4 control-label">Country</label>
							<div class="col-md-6">
								<select class="form-control" name="country" id="country">
									@foreach($countries as $key => $value)
										<option value="{{ $key }}" {{ old('country') == $key ? 'selected' : '' }}>{{ $value }}</option>
									@endforeach
								</select> 
							</div>
						</div>

						<div id="plus">
							<div class="form-group">
								<label class="col-md-4 control-label">Comment nous avez-vous connus ?</label>
								<div class="col-md-6">
									<textarea class="form-control" name="infos"></textarea>
								</div>
							</div>


							<div class="form-group">
								<label class="col-md-4 control-label">Quelle est votre région ?</label>
								<div class="col-md-6">
									<select class="form-control" name="region">
										@foreach($regions as $value)
											<option value="{!! $value !!}" {{ old('region') == $value ? 'selected' : '' }}>{{ $value }}</option>
										@endforeach
									</select> 
								</div>
							</div>
						</div>

						<div class="form-group">
							<label class="col-md-4 control-label">Password</label>
							<div class="col-md-6">
								<input type="password" class="form-control" name="password">
							</div>
						</div>

						<div class="form-group">
							<label class="col-md-4 control-label">Confirm Password</label>
							<div class="col-md-6">
								<input type="password" class="form-control" name="password_confirmation">
							</div>
						</div>

						<div class="form-group">
							<div class="col-md-6 col-md-offset-4">
								<button type="submit" class="btn btn-primary">
									Register
								</button>
							</div>
						</div>
					</form>
				</div>
			</div>
		</div>
	</div>
</div>
@endsection

@section('scripts')

<script>

    $(function(){
          
        if($("#country").val() == "FR") {
            $("#plus").show();
        } else {
            $("#plus").hide();
        }
                   
         $("#country").change(function() {
          if($(this).val() == "FR") {
              $("#plus").show('slow');
          } else {
              $("#plus").hide('slow');
          }
        });
 
    })

</script>

@endsection

Le formulaire complet est présent et l’apparition des deux contrôles supplémentaires si on choisit la France est traitée simplement avec JQuery.

Au niveau du contrôleur il faut envoyer toutes les informations :

public function getRegister()
{
    return view('auth.register', [
        'countries' => getCountries(),
        'regions' => getRegions()]);
}

Et au retour il faut bien gérer les informations dans le contrôleur pour les deux cas, aussi bien au niveau de la validation que de la création. Par exemple ainsi :

public function postRegister(Request $request)
{
    $inputs = $request->country == 'FR' ? $request->only(['name', 'email', 'country', 'password', 'password_confirmation']) : $request->all();
    
    $validator = $this->validator($inputs);

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    auth()->login($this->create($inputs));

    return redirect($this->redirectPath());
}

J’espère que ce petit exemple sera utile !

Laisser un commentaire