Persist Form Data Across Multiple Steps in Laravel - php

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() }}
return
<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?

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() }}
return
<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() }}
return
<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.

There are two ways to do it (that i can think of). I prefer second one.
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.
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.

1st) Create a custom hidden field in the form containing a random md5 character set to submit it with the form... (it can be the timestamp, the user ip address, and country concatenated together as 3 md5 strings separated by whatever character , or #, so it can be working as a token of the form)
2nd) pass the hidden field into your controller and validate it after getting the user input from the form by generating the same values in your controller, encrypting these values as md5 too, then concatenate them all together, and compare the values that is coming from the user input form with the values you are generating in your controller.
3rd) Put the values of the form in your controller in a session then regenerate the session id every visit to every view the user is going to visit.
4th) update the timestamp in your session according the timestamp the user is visiting every page.

Just because you know Laravel, does not mean you have to do everything in Laravel.
Multi-step forms should never involve server-side magic. The best and easiest you can do is to hide certain steps with display:none; and switch to the next step using javascript toggling visibilities only.

Related

Disable multiple button clicks works but is not updating my data

I am trying to prevent a button to be clicked multiple times to avoid resending requests. The disabling works but my data to be sent or updated is not executed.
<form class="detail_form" method="POST" action="{{ url('update', $id) }}" enctype="multipart/form-data">
#csrf
<button class="btn btn-update accntfrm_btn" type="submit" id="btn-update">Update</button>
</form>
$("#btn-update").on('click', function (event) {
event.preventDefault();
$(this).prop('disabled', true);
setTimeout(function(){el.prop('disabled', false); }, 3000);
});
How can I execute my updates and disallow the multiple clicks at the same time?
Use like this in action attribute of Form,
{{ route('update', ['id'=>$id]) }}
I guess it is your route,
Route::post('/update/{id}','YourController#your_function')->name('update');
and in your controller,
public function your_function(Request $request, $id){ // your code }
and if you want to go pure laravel,
use Form class
{!! Form::open(['route' => ['update', 'id'=>$id], 'files' => true, 'class' => 'detail_form']) !!}
event.preventDefault();
prevents default action of the form, this means that your form is not going to submit to the server. what you can do is use ajax or maybe axios if you have it installed to send your information to the server. Since you obviously have jquery, you can make an ajax request to your server to update your information like so
`const route = "{{ route('update', $id)}}";`
or
const route = "/route/to/your/server";
`$.post(route,
{//add a body if you need to send some information to the server
//it is optional},
function(data, status){// in this callback you can create a feedback
//from a successful trip to and from the server
//for your users
})`
.fail(function(error){
//in this callback you can handle errors from a failed trip
//to and from the server
});

Laravel Prevent Form from Re submitting

I have a form of Adding Album in database
{!! Form::open(['method' => 'POST', 'route' => ['admin.album.store'], 'enctype' => 'multipart/form-data', 'id' => 'CreateAlbumForm']) !!}
<input type="hidden" name="_token" value="{{ csrf_token() }}">
// other fields
{!! Form::submit(trans('global.app_save'), ['class' => 'btn btn-danger']) !!}
{!! Form::close() !!}
It is working perfectly.
What i need is to prevent user from clicking submit button multiple times. which i know is possible with jquery ( disabling submit button on click).
But i want to make it using csrf protection(Server side) when user does not have javascript enabled.
After a lot of search i found below solution :
What i have tried
Adding Below function in VerifyCsrfToken.php
protected function tokensMatch($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (!$token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header);
}
$tokensMatch = ($request->session()->token() == $token) ? TRUE : FALSE;
if($tokensMatch) $request->session()->regenerateToken();
return $tokensMatch;
}
And adding _token inside $dontFlash array in file app\Http\Requests\FormRequest.php
protected $dontFlash = ['password', 'password_confirmation', '_token'];
It gives me Token Mismatch error But when i click on submit button more than 2 times. And record is inserted 2 times which is unwanted behaviour.
It should give me error on 2nd attempt on submit at same time.
So in short What i need is if a user clicks on submit button single time it should insert record. and if he clicks on submit more than one time than it should give TokenMismatch Error.
You could set a token when you serve the form and check that against the database. When you submit the form, the token is checked and you can't submit it any more. Of course, it is still a good idea to do it front-end too as it is more visual for the user.
https://laracasts.com/discuss/channels/laravel/stopping-multiple-form-submission
Just searching for relevant answer and found this. Hope it will help in some way.

Symfony3 change form field type in the controller

I have a form builder which builds a form
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->
add('typeTask',TextType::class,array('label'=>"Вид заявка"))->
add('description',TextareaType::class,array('label'=>"Описание"))->
add('term',DateType::class, array(
'widget' => 'choice',
'label'=>"Краен срок"
))->
add('designer',TextareaType::class,array('label'=>"Дизайнер",
"required"=>false))->
add('executioner',TextareaType::class,array('label'=>"Под изпълнител",
"required"=>false))->
add("file",TextType::class,array('label'=>"Файл",
"required"=>false))->
add("ergent",CheckboxType::class,array('label'=>"Спешно",
"required"=>false))->add("approved",HiddenType::class,array(
"required"=>false
))->add("rejected",HiddenType::class,array(
'required'=>false
));
}
As you see I have 2 fields which are "approved" which can be true or false and rejected which can also be true and false. Usually they are hidden because only 1 type of user can access them - ROLE_ADMIN and the rest is for ROLE_EDITOR. In my case the ADMIN needs to only approve or reject it and the EDITOR can't do that. The biggest issue is that I don't need a whole form, but rather 2 buttons - "Approve" and "Reject" when the Project is shown ("show" action), but the action which changes the Project is "edit" and so what I tried so far is from "show" to send a form to "edit" and then when the edit action is over to load the "show" action again.I tried achieving this by creating 2 forms - approveForm and rejectForm which can hold only 1 property each and send and flush them to "edit" function, but the edit function doesn't accept the form and also if it did it would have deleted everything else. Here is my code so far
In show action -
$projectFormApprove = $this->createForm('AppBundle\Form\ProjectType', $project,array(
"method"=>"post"
));
$projectFormApprove->remove("description");
$projectFormApprove->remove("designer");
$projectFormApprove->remove("executioner");
$projectFormApprove->remove("term");
$projectFormApprove->remove("typeTask");
$projectFormApprove->remove("file");
$projectFormApprove->remove("ergent");
$projectFormApprove->remove("approved");
$projectFormApprove->remove("rejected");
$projectFormApprove->add("approved",HiddenType::class,array(
"data"=>true
));
$projectFormReject = $projectFormApprove;
$projectFormReject->remove("approved");
$projectFormReject->add("rejected",HiddenType::class,array(
'data'=>true
));
This will create 2 forms each having 1 property and here is what happens in my twig template
<tr>
<td>
{{ form_start(approveForm, {'action': path('project_edit', { 'id': project.id })}) }}
{{ form_widget(approveForm) }}
<input type="submit" value="Approve" />
{{ form_end(approveForm) }}
</td>
</tr>
<tr>
<td>
{{ form_start(rejectedForm,{'action': path('project_edit', { 'id': project.id })}) }}
{{ form_widget(rejectedForm) }}
<input type="submit" value="Reject" />
{{ form_end(rejectedForm) }}
</td>
</tr>
I need two forms since there are 2 buttons which simply submit them and no one actually changes the value ( this is the reason why in "show" function the created property have "data"=>true. If the form is submitted it will do it automatically.
Here is what is in my "edit" function -
/** #var $user User */
$user = $this->getUser();
$project = new Project();
$form = $this->createForm('AppBundle\Form\ProjectType', $project);
if($user->getType() != "LittleBoss"){
$form->remove("designer");
$form->remove("executioner");
}
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$project->setFromUser($user->getUsername());
$project->setDepartment($user->getDepartment());
$project->setIsOver(false);
$project->setDate(new \DateTime());
$project->setSeenByDesigner(false);
$project->setSeenByExecutioner(false);
$project->setSeenByLittleBoss(false);
$project->setSeenByManager(false);
$em = $this->getDoctrine()->getManager();
$em->persist($project);
$em->flush();
return $this->redirectToRoute('project_show', array('id' => $project->getId()));
}
return $this->render('project/new.html.twig', array(
'project' => $project,
'form' => $form->createView(),
));
Now to my actual problem - As you see I first remove "approved" field and then I add new one with predefined value. What I want is to change not the values, but the type of description and the rest fields. Is there a way to say $form->change(); or anything that can change the types of the fields without having to remove them. The type I want them to be is HiddenType and set their data so that when I submit one of the 2 forms it will be accepted as valid in the "edit" action then flushed to the database and everything will be fine. So far when one of the buttons - "Approve" or "Reject" is clicked in the "edit" action $edit_form->IsSubmited() returns false.
I suggest you to create seperate forms, one for editor and another for admin. Then in controller use the form you need by permissions of the logged in user.
if ($this->authorizationChecker->isGranted('ROLE_EDITOR')) {
$form = $this->createForm(EditorType::class);
} elseif ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
$form = $this->createForm(AdminType::class);
}
$form->handleRequest($request);
In both forms you can use same entity, but different fields.

What is the correct way to persist input through a confirmation page and into a submission page in Laravel?

I've been working on migrating several of our forms to Laravel, but there's one last step I'm not entirely sure on how to go about. I have a form that does an Insert into a database, but instead of just having 2 pages--the form and the submission page--I have 3: the form, a confirmation and a submission page.
Here is what I have at the moment:
Routes:
Route::any('application/housing-form', array('as'=>'application.form', 'uses'=>'ApplicationController#form'));
Route::post('application/confirmation', array('as'=>'application.confirmation', 'uses'=>'ApplicationController#confirmation'));
Route::post('application/submit', array('as'=>'application.submit', 'uses'=>'ApplicationController#submit'));
ApplicationController:
public function form()
{
$application = new Application;
return View::make('application/form')->with(array('application'=>$application));
}
public function confirmation()
{
$input = Input::all();
//More here?
return View::make('application/confirmation')->with(array('input'=>$input));
}
public function submit() {
$input = Input::all();
DB::table('application')->insert(
array(
<field1> => $input('field1')
...
)
);
return View::make('application/submit');
}
Views:
//form
{{ Form::model($application, array('route'=>'application.confirmation')
//inputs
{{ Form::submit('Continue') }}
{{ Form::close() }}
//confirmation
{{ Form::open(array('route'=>'application.form') }}
{{ Form::submit('Back to my information') }}
{{ Form::close() }}
{{ Form::open(array('route'=>'application.submit') }}
{{ Form::submit('Submit') }}
{{ Form::close() }}
//submission
<p>Thank you for your submission!</p>
What I am unsure about is how to persist the data from the form through the confirmation page and into the submission page. From what I can tell, I can see a few options:
Reflash all of the input
Use a hidden field (or fields) to send the information
Insert the information into the database in the confirmation page and just do an update with an in-between query with the information.
I'm pretty sure it would be the first one: reflashing the data. But if so, I'm not sure where you're actually supposed to call Session::flash or Session::reflash. Or how many times I need to do it to get it through all of the requests. Any suggestions on how to go about that, or how to streamline the rest of the form would be greatly appreciated.
One extra note as well is that this particular form deals with a large number of input fields (around 60). That's part of why I want to avoid having to request each individual field to a minimum.
What I would do is to flash the input to the session in order to repopulate the form. This can be achieved by using the Input::flash() method like so:
public function confirmation(){
Input::flash(); //this will store the input to the session
return View::make('application/confirmation');
}
Then in your view, use the Input::old() method to retrieve input data from the previous request:
{{ Form::text('fieldname', Input::old('fieldname')) }}

validator rules for a database select / dropdownmenu (in a form) laravel 4.1

I have a problem with laravel, and my form.
In my form (createBand.blade.php), i made a form with a dropdownlist wich call a database table: musicalGenre.
It should be noted that the dropdownmenu/list (select form) calls to another table in the database called MusicalGenre, and the form where I want the dropdownlist is to register a band in the database table Bands.
The select form works, i could choose in a dropdownlist all musicalGenre_name in my table after seeding them. Here's the blade page code (of course i have open, close the form and the section like laravel requires):
<p>
{{Form::label('name','Name of the Band: ')}}
{{Form::text('name',Input::old('name'))}}
</p>
#if ($errors->has('name'))
<p class='error'> {{ $errors->first('name')}}</p>
#endif
<br>
<br>
<p>
{{Form::label('email','Band Email: ')}}
{{Form::text('email',Input::old('email'))}}
</p>
#if ($errors->has('email'))
<p class='error'> {{ $errors->first('email')}}</p>
#endif
<br>
<br>
<p>
{{Form::label('musicalGenre','your style: ')}}
{{Form::select('musicalGenre_name', $genre_options = array('' => 'select your style') + musicalGenre::lists('name'), Input::old('musicalGenre_name')) }}
</p>
#if ($errors->has('musicalGenre'))
<p class='error'> {{ $errors->first('musicalGenre_name')}}</p>
#endif
<br>
<br>
I have a controller named createBandController, where i made some rules for validators for the blade form. The poblem is:
i can't pass the validators, that is to say, even if i choose a musical genre in my dropdownlist, for laravel there no choice made.
I have the error "musical genre is required". I don't understand the validator rules for a select form in my controller, or i don't know what i'm suposed to input in the rules for musical genre. Here's the controller code:
public function createBand() {
$result = MusicalGenre::all();
$genre = $result->lists('name');
$inputs = Input::all();
$rules = array(
'name' => 'required|between:1,64|unique:bands,name',
'email' => 'between:1,128|email|unique:bands,email',
'musicalGenre' => 'integer|required|in:musicalGenre'
);
$validation = Validator::make($inputs, $rules);
if ($validation->fails()) {
return Redirect::to('createBand')->withInput()->withErrors($validation)
->with('alert_error', 'you have some mistakes');
Don't pay attention to the names, (i'm french, i changed them in order to be clear for you), and i'm sure that is the validator of my dropdownlist who make problems when i fill out my form, because i can't pass it. In my original project code, there are no spelling mistakes.
All my validators and variable names work. I think i need to find the correct rules for a select form input and validators in order to laravel knows i made a choice when i choose in my dropdownlist.
At first i thought to specify that the dropdownlist use the database table musicalGenre. Like i specified that some fields are in the database table Bands, like this:
'name' => 'required|between:1,64|unique:bands,name'
Here, "name" is a field of the database table Bands. But it didn't work too.
If anyone have a solutions or wants to help, i'm interested.
Thank you (and sorry if my english seems so bad).
The validation rule in: on the field musicalGenre won't work the way that you have implemented it. It expects a comma delimited list of strings which the field value is scrubbed against.
For example if the field was 'Gender' the validation would be:
'gender' => 'in:male,female'
To validate against musicalGenre against a model you will need to write a custom Validator. See http://laravel.com/docs/validation#custom-validation-rules
I am currently writing a custom validator for this 'belongs_to' validation and when I have it working I'll post it here.
UPDATE
I have written a custom validation rule that should help. Firstly create a validators.php file and include it in global.php
require app_path().'/validators.php';
Within validators.php create the custom rule:
Validator::extend('exists_in', function($attribute, $value, $parameters)
{
$result = $parameters[0]::find($value);
return ($result == null) ? FALSE : TRUE;
});
In your validations you could now have:
'musicalGenre' => 'integer|required|exists_in:musicalGenre'
The exists_in validation takes a parameter of the Model Class name
And to give this validation an error message, add the following to the array in app/lang/en/validation.php:
"exists_in" => "The selected :attribute is invalid."
Does exists:musicalGenre instead of in:musicalGenre help?

Categories