RobFos
RobFos

Reputation: 961

Laravel Request does not return the modified request on validation fail

I have recently updated my controllers to use Requests to validate data before saving, originally I used $request->validate() within the controller route, but I am now at a stage where I really need to seperate it out in to a request.

Issue

Before validation takes place, I need to alter some of the parameters in the request, I found out this can be done using the prepareForValidation() method, and this works great, during validation the values in the request have been altered. My issue comes if the validation fails. I need to be able to return the request I've altered back to the view, at the moment, after redirection it appears to be using the request as it was before I ran prepareForValidation(). (i.e. returns title as 'ABCDEFG' instead of 'Changed The Title').

After some reading on other SO posts and Laravel forum posts, it looks as though I need to overwrite the FormRequest::failedValidation() method (which I've done, see code below), however I'm still struggling to find a way to pass my altered request back. I've tried to edit the failedValidation() method, I've provided details further down.

Expectation vs Reality

Expectation

  1. User enters 'ABCDEFG' as the title and presses save.
  2. The title is altered in the request (using prepareForValidation()) to be 'Changed The Title'.
  3. Validation fails and the user is redirected back to the create page.
  4. The contents of the title field is now `Changed The Title'.

Reality

  1. User enters 'ABCDEFG' as the title and presses save.
  2. The title is altered in the request (using prepareForValidation()) to be 'Changed The Title'.
  3. Validation fails and the user is redirected back to the create page.
  4. The contents of the title field shows `ABCDEFG'.

What I've tried

Passing the request over to the ValidationException class.

After digging through the code, it looks as though ValidationException allows a response to be passed over as a parameter.

    protected function failedValidation(Validator $validator)
    {
        throw (new ValidationException($validator, $this))
            ->errorBag($this->errorBag)
            ->redirectTo($this->getRedirectUrl());
    }

However this results in the error Call to undefined method Symfony\Component\HttpFoundation\HeaderBag::setCookie() in Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::addCookieToResponse.

Flashing the request to the session

My next attempt was to just flash my request to the session, this doesn't seem to work, instead of my modified request being in the session, it looks to be the request before I ran prepareForValidation().

    protected function failedValidation(Validator $validator)
    {
        $this->flash();

        throw (new ValidationException($validator))
            ->errorBag($this->errorBag)
            ->redirectTo($this->getRedirectUrl());
    }

Returning a response instead of an exception

My final attempt to get this to work was to return a response using withInput() instead of the exception.

    protected function failedValidation(Validator $validator)
    {
        return redirect($this->getRedirectUrl())
        ->withErrors($validator)
        ->withInput();
    }

However it looks as though the code continues in to the BlogPostController::store() method instead of redirecting back to the view.

At this point I'm out of ideas, I just can't seem to get the altered request back to the view if validation fails!

Other Notes

  1. I am pretty much a Laravel newbie, I have experience with a custom framework loosely based on Laravel, but this is my first CMS project.
  2. I fully understand I may well be going down the wrong route, perhaps there's a better way of altering a request and passing it back when validation fails?
  3. What am I trying to achieve by doing this? The main thing is the active checkbox. By default it is checked (See the blade below), if the user unchecks it and presses save, active is not passed over in the HTTP request, therefore active does not exist in the Laravel request object and when the user is returned back, the active checkbox has been checked again when it shouldn't be.
  4. Then why have you used title in your example? I am using title in my post because I think it's easier to see what I am trying to achieve.

Any help is apreciated as I've currently burnt quite a few hours trying to solve this. 😣

Related Code

BlogPostController.php

<?php

namespace App\Http\Controllers;

use App\BlogPost;
use Illuminate\Http\Request;
use App\Http\Requests\StoreBlogPost;

class BlogPostController extends Controller
{

    /**
     * Run the auth middleware to make sure the user is authorised.
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('admin.blog-posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  StoreBlogPost    $request
     * @return \Illuminate\Http\Response
     */
    public function store(StoreBlogPost $request)
    {
        $blogPost = BlogPost::create($request->all());

        // Deal with the listing image upload if we have one.
        foreach ($request->input('listing_image', []) as $file) {
            $blogPost->addMedia(storage_path(getenv('DROPZONE_TEMP_DIRECTORY') . $file))->toMediaCollection('listing_image');
        }

        // Deal with the main image upload if we have one.
        foreach ($request->input('main_image', []) as $file) {
            $blogPost->addMedia(storage_path(getenv('DROPZONE_TEMP_DIRECTORY') . $file))->toMediaCollection('main_image');
        }

        return redirect()->route('blog-posts.edit', $blogPost->id)
            ->with('success', 'The blog post was successfully created.');
    }
}

// Removed unrelated controller methods.

StoreBlogPost.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;

class StoreBlogPost extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'         => 'required',
            'url'           => 'required',
            'description'   => 'required',
            'content'       => 'required',
        ];
    }

    /**
     * Get the error messages for the defined validation rules.
     *
     * @return array
     */
    public function messages()
    {
        return [
            'title.required'        => 'The Title is required',
            'url.required'          => 'The URL is required',
            'description.required'  => 'The Description is required',
            'content.required'      => 'The Content is required',
        ];
    }

    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $this->merge([
            'active' => $this->active ?? 0,
            'title' => 'Changed The Title',
        ]);
    }

    /**
     * @see FormRequest
     */
    protected function failedValidation(Validator $validator)
    {
        throw (new ValidationException($validator))
            ->errorBag($this->errorBag)
            ->redirectTo($this->getRedirectUrl());
    }
}

create.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="edit">
    <form action="{{ route('blog-posts.store') }}" method="POST" enctype="multipart/form-data">
        @method('POST')
        @csrf

        <div class="container-fluid">
            <div class="row menu-bar">
                <div class="col">
                    <h1>Create a new Blog Post</h1>
                </div>
                <div class="col text-right">
                    <div class="btn-group" role="group" aria-label="Basic example">
                        <a href="{{ route('blog-posts.index') }}" class="btn btn-return">
                            <i class="fas fa-fw fa-chevron-left"></i>
                            Back
                        </a>

                        <button type="submit" class="btn btn-save">
                            <i class="fas fa-fw fa-save"></i>
                            Save
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <div class="container-fluid">
            <div class="form-group row">
                <label for="content" class="col-12 col-xl-2 text-xl-right col-form-label">Active</label>
                <div class="col-12 col-xl-10">
                    <div class="custom-control custom-switch active-switch">
                      <input type="checkbox" name="active" value="1" id="active" class="custom-control-input" {{ old('active', '1') ? 'checked' : '' }}>
                      <label class="custom-control-label" for="active"></label>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="title" class="col-12 col-xl-2 text-xl-right col-form-label required">Title</label>
                <div class="col-12 col-xl-10">
                    <input type="text" name="title" id="title" class="form-control" value="{{ old('title', '') }}">
                </div>
            </div>
        </div>
    </form>
</div>
@endsection

Upvotes: 3

Views: 1949

Answers (1)

rabian
rabian

Reputation: 46

I had the same issue and here is the solution I found after digging through laravel code.

It seems that Laravel creates a different object for the FormRequest, so you can do something like this.

protected function failedValidation(Validator $validator)
{
    // Merge the modified inputs to the global request.
    request()->merge($this->input());

    parent::failedValidation($validator);
}

Upvotes: 3

Related Questions