Laravel FormRequest Validation for Rest API get calls with 2 query json params not working properly

huangapple go评论65阅读模式
英文:

Laravel FormRequest Validation for Rest API get calls with 2 query json params not working properly

问题

我有一个 Laravel FormRequest 类,应该验证 GET 路由上的两个 JSON 查询字符串(嵌套对象和数组)在我的 JSON Rest API 上。我获取 JSON 查询字符串,将其解码为 PHP 对象,并在请求的验证准备时间内将其存储以进行验证。

由于某种原因,这似乎不能正常工作。'sorters' JSON 验证正常工作,'sorters_decoded' 的 'required' 和 'array' 验证也正常工作。数组元素验证和后续所有内容似乎都不起作用,因为即使发送了无效数据,我也能到达控制器函数。请求中的查询输入包已被修改(无效数据被设置为 null),但未生成验证 422 响应。你可能看到这段代码有什么问题吗?

{
    // 代码略
}
英文:

I got a Laravel FormRequest class that should validate two JSON query strings (nested objects and arrays) on a GET route on my JSON Rest API. I pick up the json query strings, decode them to php objects and store them during validation prepare time on the request for validation.

For some reason, this seems not to work properly. The 'sorters' json validation is working, as is the 'sorters_decoded' validation for 'required' and 'array'. The array element validation and everything after seems not to work since i reach the controller function even if invalid data is sent. The query input bag collection on the request is modified (invalid data is set to null) but no validation 422 response is generated. Do you maybe see something wrong with this code?

class RecipeFindRequest extends FormRequest
{

    protected function prepareForValidation(): void
    {
        try {
            $sorters = null;
            if ($this->query->has('sorters')) {
                $sorters = json_decode($this->query->get('sorters'));
                $this->merge([
                    'sorters_decoded' => $sorters,
                ]);
            }

            $filters = null;
            if ($this->query->has('filters')) {
                $filters = json_decode($this->query->get('filters'));
                $this->merge([
                    'filters_decoded' => $filters,
                ]);
            }

            $this->merge([
                'language' => $this->headers->get('language'),
            ]);
        } catch(\Throwable $e) {
            //die silently, fail response will get raised on validation time
        }
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'sorters' => ['json'],
            'sorters_decoded' => ['required', 'array'],
            'sorters_decoded.*.name' => ['required', 'string', Rule::in(['likes', 'ratings', 'calories', 'carbs', 'protein', 'fat', 'created'])],
            'sorters_decoded.*.direction' => ['required', 'string', Rule::in(['asc', 'desc'])],
            'sorters_decoded.*.order' => ['required', 'integer'],
            'filters' => ['json'],
            'filters_decoded.duration_high' => ['integer', 'min:0'],
            'filters_decoded.duration_low' => ['integer', 'min:0'],
            'filters_decoded.title' => ['string'],
            'filters_decoded.difficulty' => ['array'],
            'filters_decoded.difficulty.*' => ['string', 'exists:difficulties,id'],
            'filters_decoded.ingredients' => ['array'],
            'filters_decoded.ingredients.*.id' => ['string', 'exists:ingredients,id'],
            'filters_decoded.ingredients.*.relation' => ['string', Rule::in(['include', 'exclude'])],
            'filters_decoded.liked_by_me' => ['boolean'],
            'filters_decoded.cookbooks' => ['array'],
            'filters_decoded.cookbooks.*' => ['string', 'exists:cookbooks,id'],
            'filters_decoded.nutritions' => ['array'],
            'filters_decoded.nutritions.*.category_id' => ['string', 'exists:nutrition_categories,id'],
            'filters_decoded.nutritions.*.nutrition_high' => ['numeric', 'min:0'],
            'filters_decoded.nutritions.*.nutrition_low' => ['numeric', 'min:0'],
            'language' => ['string', 'size:2', 'exists:i18n_languages,short'],
            'page' => ['integer', 'min:1'],
            'per_page' => ['integer', 'min:1'],
        ];
    }
}

Both params are sent as json query strings and the decoding step works, so its not something to do with url de/encoding.

Laravel FormRequest Validation for Rest API get calls with 2 query json params not working properly

I tried to change the sorters.*.name array element validation from string to integer, and when i sent in some data like [{'name':'a'}] the 'integer' validation changed the data in $response->query->sorters_decoded to [{'name':null}]. But no 422 validation fail response came up.

Thank you for considering my problem,
Mikkey

答案1

得分: 0

以下是要翻译的内容:

There are a number of problems here:

  • In prepareForValidation() you have a try/catch block that does nothing with the caught exception. Contrary to what your comment suggests, it will not be returned as a validation error because you're catching it. Even if it went uncaught, I'm not sure it would show up as a 422 error on the client side.
  • json_decode() does not throw errors by default, so there's nothing for your try/catch statement to work with anyway!
  • json_decode() also decodes to objects by default; your subsequent validation rules will not work without arrays.
  • You have a validation rules for data that you send in the query string. Form request validation only works on the body of the message, not the query string. Anything you want validated needs to be manually merged.

Here's what I would try:

<?php

namespace App\Http\Requests;

use Illuminate\Validation\ValidationException;
use Throwable;

class RecipeFindRequest extends FormRequest
{

    protected function prepareForValidation(): void
    {
        try {
            if ($this->query->has('sorters')) {
                $sorters = json_decode($this->query->get('sorters'), associative: true, flags: JSON_THROW_ON_ERROR);
            }

            if ($this->query->has('filters')) {
                $filters = json_decode($this->query->get('filters'), associative: true, flags: JSON_THROW_ON_ERROR);
            }

            $this->merge([
                // user cannot easily change the headers of a request
                // so you should set a sensible default for this value
                'language' => $this->headers->get('language', 'en'),
                'page' => $this->query->get('page'),
                'per_page' => $this->query->get('per_page'),
                'sorters_decoded' => $sorters ?? null,
                'filters_decoded' => $filters ?? null,

            ]);
        } catch(Throwable $e) {
            if (isset($sorters)) {
                // if this is set, the error was with the second decode
                $messages = ['filters' => 'An invalid filter value was passed'];
            } else {
                $messages = ['sorters' => 'An invalid sort value was passed'];
            }
            throw ValidationException::withMessages($messages);
        }
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'sorters_decoded' => ['required', 'array'],
            'sorters_decoded.*.name' => ['required', 'string', Rule::in(['likes', 'ratings', 'calories', 'carbs', 'protein', 'fat', 'created'])],
            'sorters_decoded.*.direction' => ['required', 'string', Rule::in(['asc', 'desc'])],
            'sorters_decoded.*.order' => ['required', 'integer'],
            'filters_decoded.duration_high' => ['integer', 'min:0'],
            'filters_decoded.duration_low' => ['integer', 'min:0'],
            'filters_decoded.title' => ['string'],
            'filters_decoded.difficulty' => ['array'],
            'filters_decoded.difficulty.*' => ['string', 'exists:difficulties,id'],
            'filters_decoded.ingredients' => ['array'],
            'filters_decoded.ingredients.*.id' => ['string', 'exists:ingredients,id'],
            'filters_decoded.ingredients.*.relation' => ['string', Rule::in(['include', 'exclude'])],
            'filters_decoded.liked_by_me' => ['boolean'],
            'filters_decoded.cookbooks' => ['array'],
            'filters_decoded.cookbooks.*' => ['string', 'exists:cookbooks,id'],
            'filters_decoded.nutritions' => ['array'],
            'filters_decoded.nutritions.*.category_id' => ['string', 'exists:nutrition_categories,id'],
            'filters_decoded.nutritions.*.nutrition_high' => ['numeric', 'min:0'],
            'filters_decoded.nutritions.*.nutrition_low' => ['numeric', 'min:0'],
            'language' => ['string', 'size:2', 'exists:i18n_languages,short'],
            'page' => ['integer', 'min:1'],
            'per_page' => ['integer', 'min:1'],
        ];
    }
}

The size of these URLs must be massive; this is the sort of thing that post requests are best used for. (But this isn't a hard and fast rule, just my opinion.)

英文:

There are a number of problems here:

  • In prepareForValidation() you have a try/catch block that does nothing with the caught exception. Contrary to what your comment suggests, it will not be returned as a validation error because you're catching it. Even if it went uncaught, I'm not sure it would show up as a 422 error on the client side.
  • json_decode() does not throw errors by default, so there's nothing for your try/catch statement to work with anyway!
  • json_decode() also decodes to objects by default; your subsequent validation rules will not work without arrays.
  • You have a validation rules for data that you send in the query string. Form request validation only works on the body of the message, not the query string. Anything you want validated needs to be manually merged.

Here's what I would try:

&lt;?php

namespace App\Http\Requests;

use Illuminate\Validation\ValidationException;
use Throwable;

class RecipeFindRequest extends FormRequest
{

    protected function prepareForValidation(): void
    {
        try {
            if ($this-&gt;query-&gt;has(&#39;sorters&#39;)) {
                $sorters = json_decode($this-&gt;query-&gt;get(&#39;sorters&#39;), associative: true, flags: \JSON_THROW_ON_ERROR);
            }

            if ($this-&gt;query-&gt;has(&#39;filters&#39;)) {
                $filters = json_decode($this-&gt;query-&gt;get(&#39;filters&#39;), associative: true, flags: \JSON_THROW_ON_ERROR);
            }

            $this-&gt;merge([
                // user cannot easily change the headers of a request
                // so you should set a sensible default for this value
                &#39;language&#39; =&gt; $this-&gt;headers-&gt;get(&#39;language&#39;, &#39;en&#39;),
                &#39;page&#39; =&gt; $this-&gt;query-&gt;get(&#39;page&#39;),
                &#39;per_page&#39; =&gt; $this-&gt;query-&gt;get(&#39;per_page&#39;),
                &#39;sorters_decoded&#39; =&gt; $sorters ?? null,
                &#39;filters_decoded&#39; =&gt; $filters ?? null,

            ]);
        } catch(Throwable $e) {
            if (isset($sorters)) {
                // if this is set, the error was with the second decode
                $messages = [&#39;filters&#39; =&gt; &#39;An invalid filter value was passed&#39;];
            } else {
                $messages = [&#39;sorters&#39; =&gt; &#39;An invalid sort value was passed&#39;];
            }
            throw ValidationException::withMessages($messages);
        }
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array&lt;string, mixed&gt;
     */
    public function rules(): array
    {
        return [
            &#39;sorters_decoded&#39; =&gt; [&#39;required&#39;, &#39;array&#39;],
            &#39;sorters_decoded.*.name&#39; =&gt; [&#39;required&#39;, &#39;string&#39;, Rule::in([&#39;likes&#39;, &#39;ratings&#39;, &#39;calories&#39;, &#39;carbs&#39;, &#39;protein&#39;, &#39;fat&#39;, &#39;created&#39;])],
            &#39;sorters_decoded.*.direction&#39; =&gt; [&#39;required&#39;, &#39;string&#39;, Rule::in([&#39;asc&#39;, &#39;desc&#39;])],
            &#39;sorters_decoded.*.order&#39; =&gt; [&#39;required&#39;, &#39;integer&#39;],
            &#39;filters_decoded.duration_high&#39; =&gt; [&#39;integer&#39;, &#39;min:0&#39;],
            &#39;filters_decoded.duration_low&#39; =&gt; [&#39;integer&#39;, &#39;min:0&#39;],
            &#39;filters_decoded.title&#39; =&gt; [&#39;string&#39;],
            &#39;filters_decoded.difficulty&#39; =&gt; [&#39;array&#39;],
            &#39;filters_decoded.difficulty.*&#39; =&gt; [&#39;string&#39;, &#39;exists:difficulties,id&#39;],
            &#39;filters_decoded.ingredients&#39; =&gt; [&#39;array&#39;],
            &#39;filters_decoded.ingredients.*.id&#39; =&gt; [&#39;string&#39;, &#39;exists:ingredients,id&#39;],
            &#39;filters_decoded.ingredients.*.relation&#39; =&gt; [&#39;string&#39;, Rule::in([&#39;include&#39;, &#39;exclude&#39;])],
            &#39;filters_decoded.liked_by_me&#39; =&gt; [&#39;boolean&#39;],
            &#39;filters_decoded.cookbooks&#39; =&gt; [&#39;array&#39;],
            &#39;filters_decoded.cookbooks.*&#39; =&gt; [&#39;string&#39;, &#39;exists:cookbooks,id&#39;],
            &#39;filters_decoded.nutritions&#39; =&gt; [&#39;array&#39;],
            &#39;filters_decoded.nutritions.*.category_id&#39; =&gt; [&#39;string&#39;, &#39;exists:nutrition_categories,id&#39;],
            &#39;filters_decoded.nutritions.*.nutrition_high&#39; =&gt; [&#39;numeric&#39;, &#39;min:0&#39;],
            &#39;filters_decoded.nutritions.*.nutrition_low&#39; =&gt; [&#39;numeric&#39;, &#39;min:0&#39;],
            &#39;language&#39; =&gt; [&#39;string&#39;, &#39;size:2&#39;, &#39;exists:i18n_languages,short&#39;],
            &#39;page&#39; =&gt; [&#39;integer&#39;, &#39;min:1&#39;],
            &#39;per_page&#39; =&gt; [&#39;integer&#39;, &#39;min:1&#39;],
        ];
    }
}

The size of these URLs must be massive; this is the sort of thing that post requests are best used for. (But this isn't a hard and fast rule, just my opinion.)

huangapple
  • 本文由 发表于 2023年6月9日 03:51:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/76435274.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定