b166er's avatar

validate request and raise exception on undefined-parameters

Hi folks,

i am trying to solve a problem but i am missing an idea how to do it elegantly. I would like to trigger an exception / 404 http error if a parameter was passed that was not defined in the validator. i don't want to duplicate the code for every controller, so i moved the code to a parent controller, but i have no access to the specific request validator there, so i have to call the parent controller in each method in the specific controller. is there anything more elegant, perhaps a filter or middleware solution? i already tried to check in middleware but i can't access to specific request with validator, or i dont found how.

here is my TestController:

<?php

namespace App\Http\Controllers\API;

use App\Http\Requests\TestGetRequest;
use App\Http\Controllers\Controller;
use App\Exceptions\API\ParameterNotFoundException;
use App\Models\API\Test;

class TestController extends Controller {

    /**
     *
     * Display a listing of the resource.
     *
     * @param TestGetRequest $request
     * @return \Illuminate\Http\Response
     */
    public function index(TestGetRequest $request) {
        
        // this block is ugly because we need them in every method of every controller
        try {
            $this->prepareInput($request);
        } catch (ParameterNotFoundException $ex) {
            return $this->notFoundResponse($ex->getMessage());
        } catch (\Exception $ex) {
            return $this->notValidParameterResponse('UNDEFINED: ' . $ex->getMessage());
        }

        $queryBuilder = Test::clone();

        // check filter "active"
        if (true === isset($this->params['is_active'])) {
            $active = (bool) $this->params['is_active'];
            $queryBuilder->where('is_active', $active);
        }

        // check filter "name"
        if (true === isset($this->params['name'])) {
            $name = $this->params['name'];
            $queryBuilder->where('name', 'LIKE', '%' . $name . '%');
        }

        $output = new TestResourceCollection($queryBuilder->paginate());

        return $this->successResponse($output);
    }

and my parent Controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Exceptions\API\ParameterNotFoundException;
use App\Models\API\Test;

class Controller extends BaseController {

    use AuthorizesRequests,
        DispatchesJobs,
        ValidatesRequests;

    protected $page = 1;
    protected $params = [];

    /**
     *
     * @param FormRequest $request
     * @return bool
     * @throws ParameterNotFoundException
     */
    protected function prepareInput($request): bool {
        $all = $request->all();

        $this->params = $request->validated();
        $diff = array_diff($all, $this->params);

        if (count($diff) > 0) {
            throw new ParameterNotFoundException('Parameter: \'' . key($diff) . '\' not found');
        }

        // set pagination parameter default for all controllers
        if (isset($this->params['page'])) {
            $this->page = (int) $this->params['page'];
        }
        return true;
    }

    /**
     *
     * @param array $data
     * @return JsonResponse
     */
    protected function successResponse($data, $header = []) { //: JsonResponse
        return self::createResponse(200, $data, $header);
    }

    /**
     *
     * @param array $data
     * @return JsonResponse
     */
    protected function notFoundResponse($data): JsonResponse {
        return self::errorResponse(404, $data);
    }

    /**
     *
     * @param array $data
     * @return JsonResponse
     */
    protected function notValidParameterResponse($data): JsonResponse {
        return self::errorResponse(422, $data);
    }

    /**
     *
     * @param array $data
     * @return JsonResponse
     */
    protected function errorResponse($code, $data): JsonResponse {
        return self::createResponse($code, ['error' => $data]);
    }

    /**
     *
     * @param int $code
     * @param array $data
     * @return JsonResponse
     */
    protected static function createResponse($code, $input, $headers = []) {//: JsonResponse
        $output = null;
        if ($input instanceof JsonResource) {
            $output = $input;
        } else {
            $output = response()->json($input, $code)->withHeaders($headers);
        }

        return $output;
    }

is there a way to not use this method ($this->prepareInput($request);) in every controller method?

i am happy for any idea or suggestions for improvement!

thanks!

0 likes
6 replies
b166er's avatar

i dont want repeat this code in every controllers-methods


        try {
            $this->prepareInput($request);
        } catch (ParameterNotFoundException $ex) {
            return $this->notFoundResponse($ex->getMessage());
        } catch (\Exception $ex) {
            return $this->notValidParameterResponse('UNDEFINED: ' . $ex->getMessage());
        }

is there a way to get the specific Request (\App\Http\Requests\TestGetRequest) from my controller to middleware? then i would do this validation in middleware.

Snapey's avatar

no, why do you want to throw an error if there are additional data?

b166er's avatar

because this is a compliance-specification, the application is allowed to expose only defined parameter, and for all other parameter the application need to throw an exception/error-message, just ignoring is not allowed.

Snapey's avatar

I would probably be consistent in using form request class and then include a trait in every form request class.

You can also publish the stubs and edit the form request stub to ensure any new form requests get the trait.

b166er's avatar
b166er
OP
Best Answer
Level 1

thanks for the tip, i have solved my problem! for this I have created a parent Request-class with my required validation check of undefined parameters in ->withValidator(). and in ->failedValidation(Validator $validator) i throw an exception when the validation fails. now i have a central place for validation of undefined parameters, and i dont need to re-check it in every method of new controller. just get request-parameters from specific request-class (extened from my parent class) if my parent class don't raise exception.

here is my solution:

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Foundation\Http\FormRequest;

abstract class GetRequest extends FormRequest {

    /**
     * Configure the validator instance.
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @return void
     */
    public function withValidator($validator) {

        $validator->after(function ($validator) {
            $diff_keys = array_diff(array_keys($this->input()), array_keys($this->rules()));

            if (count($diff_keys) > 0) {
                $values = array_map(function ($value) {
                    return "Parameter: '$value' is not defined!";
                }, $diff_keys);

                $messages = array_combine($diff_keys, [$values]);
                $validator->errors()->merge($messages);
            }
        });
    }

    /**
     * Handle a failed validation attempt.
     *
     * source: https://stackoverflow.com/questions/46350307/disable-request-validation-redirect-in-laravel-5-4
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function failedValidation(Validator $validator) {
        throw new HttpResponseException(response()->json([
                            'error' => $validator->errors()->first()], 422));
    }

}
1 like

Please or to participate in this conversation.