Summer Sale! All accounts are 50% off this week.

tk_'s avatar
Level 1

Form validation loses error bags and old input values on redirect

Hi there !

I have a view which contains a form

@extends('layouts.structure')

@section('content')
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<form class="row" action="{{ route('lists.rules.create.store', ['list' => $list, 'condition' => $condition]) }}" method="POST">
    @csrf
    <!-- some form fields -->
</form> 
@endsection

And for the form, I have a controller, defined like so :

<?php

namespace App\Http\Controllers\Lists\Rules;

use App\Http\Controllers\Controller;
use App\Models\ListUser;
use App\Models\ListUserRule;
use App\Models\ListUserRuleFlag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Validator;

class StoreListUserRuleController extends Controller
{
    private ListUser $list;

    private function handleCreateCollectivities(): ListUserRule|Validator|null
    {
        $request = request();
        $validator = ValidatorFacade::make($request->all(), [
            'level' => 'required|exists:App\Models\Level,id',
            'collectivities' => 'required|array|min:1',
            'collectivities.*' => 'exists:App\Models\Collectivity,id',
            'children' => 'required',
            'neighboors' => 'required',
        ]);

        if ($validator->fails()) {
            return $validator;
        }

        $rule = ListUserRule::create();
        $this->list->rules()->save($rule);
        $rule->collectivities()->saveMany($request->input('collectivities'));
        $rule->flags()->saveMany([
            new ListUserRuleFlag(['name' => 'children', 'value' => $request->input('children')]),
            new ListUserRuleFlag(['name' => 'neighboors', 'value' => $request->input('neighboors')])
        ]);

        return $rule;
    }

    public function __invoke(Request $request, ListUser $list, string $condition)
    {
        $this->list = $list;

        switch ($condition) {
            case 'collectivites':
                $ret = $this->handleCreateCollectivities();
                if ($ret instanceof Validator) {
                    return Redirect::back()->withErrors($ret)->withInput();
                }
                return redirect()->route('lists.show', ['list' => $list]);
            case 'mandats':
                break;
            case 'etiquettes':
                break;
            case 'nuances':
                break;
            case 'champs':
                break;
        }
    }
}

However, upon form submission, when I have errors the error bag is empty and so is the old input one.

I have noticed in the Laravel Debugbar that 2 requests are actually being made : the form's POST (the response of which contains the error bags etc) followed by a GET to the route where the form is.

This SO post (questions ID is 38058223, sorry can't link it on my first day !) showcases the exact same behavior, however I cannot rely on removing the web middleware (which didn't work anyways).

Do you guys have any idea of what the issue might be and how to solve it ?

Thank you in advance for your help ! Yours

0 likes
26 replies
Tray2's avatar

Validation failures always returns to the previous page in this case being the create view.

tk_'s avatar
Level 1

@Tray2 Hey !

It's not working either, which is weird. Even when adding ->with([...]).

Tray2's avatar

@tk_ The failed validation redirects back with any errors in the session so you don't send anything back.

When you do

$request->validate([
	'title' => 'required'
]);

and the request doesn't have have anything in title it redirects back. The code after the validation is never executed if the validation fails.

tk_'s avatar
Level 1

@Tray2 I think you're missing my point, sorry if I've previously been unclear !

My problem is not how to return error values to the view, but rather why two requests are made :

  • The reply to the POST request which correctly holds the bagged error messages
  • And a secondary GET to the route

The problem is that after the second one has happened, the values are missing and no errors are displayed to the page.

I'm sorry I can't post the debugbar images, but you can get the two screenshots like so :

First response to the POST (form submitted)

img

And second chained request from god knows where, without the error values

img

Tray2's avatar

@tk_

Is this what happens?

  1. You do a GET to the create page and fill in the form
  2. You send the POST request to the desired endpoint.
  3. Your validation fails and the errors are stored in the session
  4. A GET request is made to the previous page
  5. You display your validation errors on the create page
Tray2's avatar

@tk_ That is exactly what is supposed to happen.

Tray2's avatar

@tk_ Try this in your blade file without checking $errors->any()

<ul>
   @foreach ($errors->all() as $error)
       <li>{{ $error }}</li>
   @endforeach
</ul>
Tray2's avatar

@tk_ And if you create a feature test and replace all the information inside the <> with models and fields matching you code and run it. What is the result?

  /**
    * @test
    */
    public function a_valid_<your_model>_can_be_stored(): void
    {
        $<yourModel> = <YourModel::factory()->make();
        $this->post(action([≤YourModel>Controller::class, 'store'], $<yourModel->toArray()))
            ->assertStatus(302)
            ->assertSessionHasNoErrors();
        $this->assertDatabaseHas('<your_models>, ['<your field' => $<yourModel-><your_field]);
    }

 /**
     * @test
     */
    public function the_required_fields_are_validated(): void
    {
        $this->post(action([<YouModel>Controller::class, 'store'], []))
            ->assertSessionHasErrors([
                //The fields you want to validate
            ]);
        $this->assertDatabaseMissing('<your_tables>', ['<your_field>' => null]);
    }
Tray2's avatar

@tk_ Is there a particulr reason that you are using an invokable controller instead of a resource controller?

function store(Request $request)
{
	$validData = $request->validate($rules);
	ListRule::create($validData);
	return redirect()->route('lists.show')->withSuccess('List succesfully created')';
}
tk_'s avatar
Level 1

@Tray2 Hi ! So I've always used invokable controllers instead of method based ones. I have a very large application and like to keep each controller in charge of a single thing. I don't think this is a source of problem for my particular case though... does it ?

As for the tests you suggest, I will try and implement those and come back to you with the output.

Tray2's avatar

@tk_ Invokable controller are quite good for some things like changing statuses and such, while not so good when doing basic crud.

A PublishPostController that changes the status from unpublished to published makes sense, but StorePostController, EditPostController, CreatePostController, DeletePostController, ShowPostController and UpdatePostController makes absolutely no sense to me.

tk_'s avatar
Level 1

Hi @Tray2 !

So I've simplified my code for simplicity's sake and complying with your recommendation. I am however still facing the issue I was originally describing.

Here is my new controller :

class ListUserRuleController extends Controller
{
    public function store(Request $request, ListUser $list, string $condition)
    {
        $request->validate([
            'level' => 'required|exists:App\Models\Level,id',
            'collectivities' => 'required|array|min:1',
            'collectivities.*' => 'exists:App\Models\Collectivity,id',
            'children' => 'required',
            'neighboors' => 'required',
        ]);

        // Store the resulting models...
    }
}

I'll go and implement the tests you've suggested.

Thank your for you patient help !

tk_'s avatar
Level 1

So, @tray2, my test looks like :

    /**
     * @test
     */
    public function the_required_fields_are_validated(): void
    {
        $list = ListUser::factory()->make();
        $this->post(action([ListUserRuleController::class, 'store'], ['list' => $list->id, 'condition' => 'foo']))
            ->assertSessionHasErrors();
        $this->assertDatabaseMissing('list_user_rule_flags', ['name' => null]);
    }

and the php artisan test command output is :

• Tests\Feature\Lists\StoreListRuleTest > the required fields are validated      
Session is missing expected key [errors].
Failed asserting that false is true.

at C:\xampp\htdocs\mandat-senateur\tests\Feature\Lists\StoreListRuleTest.php:20                                               16▕         $list = ListUser::factory()->make();
17▕         $this->post(action([ListUserRuleController::class, 'store'], ['list' => $list->id, 'condition' => 'foo']))                                                                                                                                   ➜  18▕             ->assertSessionHasErrors()
19▕         $this->assertDatabaseMissing('list_user_rule_flags', ['name' => null]);            
20▕     }
21▕ }                                                               
22▕                                                                                                                                                             
Tray2's avatar

@tk_ change this line

->assertSessionHasErrors();

to

->assertSessionHasNoErrors();

And the happy path should pass.

tk_'s avatar
Level 1

@Tray2 Well the point is to have the test fail, no ? Since I store an invalid list rule ?

Tray2's avatar

@tk_ First you make sure that a valid one is stored then you create the falling ones.

tk_'s avatar
Level 1

@Tray2 Hi, I wish you a happy new year !

Sorry for the delay. Holidays have been keeping me busy :)

So I have the valid rule creation test, which works correctly.

However, the one with the invalid creation fails. (it is the one I've copy pasted right above).

Thanks for your help !

Tray2's avatar

@tk_ I try to keep things as kiss as possible. I have this for one of my controller's store methods

/**
     * @test
     */
    public function the_required_fields_are_validated(): void
    {
        $this->post(action([AuthorsController::class, 'store']), [])
            ->assertSessionHasErrors(['first_name', 'last_name']);
    }

Your's shouldn't need to be more complicated than that.

tk_'s avatar
Level 1

@Tray2 The route has a List parameter which is a model that needs to be created. This form is dependent on it. Cannot just post an empty array :)

tk_'s avatar
tk_
OP
Best Answer
Level 1

@Tray2 Okay so the problem wasn't related to my code at all.

At least not the controller nor the view :)

Someone in my team had added the StartSession middleware in the $middlewares array of the Kernel, removing it solved the error bag issue !

Thanks for you dedicated time and efforts :)

2 likes
Snapey's avatar

why not just let the code throw a validation exception instead of this multiple return types and having to check for returned instance of validator?

You seem to be making a lot more work for yourself?

tk_'s avatar
Level 1

@Tray2 Hi ! Thanks for your reply ! I did that, but turned to that over-engineered code because it had the same behavior I'm describing

Please or to participate in this conversation.