I'm implementing an API in Laravel using JSON:API specification.
In it I have a resource, let's call it Ponds, with many-to-many relationships with another resource, let's call it Ducks.
According to JSON:API specs in order to remove such relationship i should use DELETE /ponds/{id}/relationships/ducks endpoint, with request of following body:
{
"data": [
{ "type": "ducks", "id": "123" },
{ "type": "ducks", "id": "987" }
]
}
This is handled by PondRemoveDucksRequest, which looks as follows:
<?php
...
class PondRemoveDucksRequest extends FormRequest
{
public function authorize()
{
return $this->allDucksAreRemovableByUser();
}
public function rules()
{
return [
"data.*.type" => "required|in:ducks",
"data.*.id" => "required|string|min:1"
];
}
protected function allDucksAreRemovableByUser(): bool
{
// Here goes the somewhat complex logic determining if the user is authorized
// to remove each and every relationship passed in the data array.
}
}
The problem is that if I send a body such as:
{
"data": [
{ "type": "ducks", "id": "123" },
{ "type": "ducks" }
]
}
, I get a 500, because the authorization check is triggered first and it relies on ids being present in each item of the array. Ideally I'd like to get a 422 error with a standard message from the rules validation.
Quick fix I see is to add the id presence check in the allDucksAreRemovableByUser() method, but this seems somewhat hacky.
Is there any better way to have the validation rules checked first, and only then proceed to authorization part?
Thanks in advance!
1 - Create abstract class called "FormRequest" inside App\\Requests directory and override the validateResolved() method:
<?php
namespace App\Http\Requests;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
abstract class FormRequest extends BaseFormRequest
{
/**
* Validate the class instance.
*
* @return void
* @throws AuthorizationException
* @throws ValidationException
*/
public function validateResolved()
{
$validator = $this->getValidatorInstance();
if ($validator->fails())
{
$this->failedValidation($validator);
}
if (!$this->passesAuthorization())
{
$this->failedAuthorization();
}
}
}
2 - Extend your FormRequests with custom FormRequest
<?php
namespace App\Http\Requests\Orders;
use App\Http\Requests\FormRequest;
class StoreOrderRequest extends FormRequest
{
}
add $this->getValidatorInstance()->validate();
at beggining of authorize()
method
Here is a slightly different approach than what you are attempting, but it may accomplish the desired outcome for you.
If you are trying to validate whether the given duck id belongs to the user, this can be done in the rule itself as follows:
"data.*.id" => "exists:ducks,id,user_id,".Auth::user()->id
This rule asks if a record exists in the ducks table which matches the id and where the user_id is the current logged in user_id.
If you chain it to your existing rules (required|string|min:1), using 'bail', then it wouldn't run the query unless it had passed the other three rules first:
"data.*.id" => "bail|required|string|min:1|exists:ducks,id,user_id,".Auth::user()->id
The most cleanest solution I found to solve it was by creating a small trait for the FormRequest
and use it anytime you want to run validation before the authorization, Check the example bellow:
<?php
namespace App\Http\Requests\Traits;
/**
* This trait to run the authorize after a valid validation
*/
trait AuthorizesAfterValidation
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Set the logic after the validation
*
* @param $validator
* @return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if (! $validator->failed() && ! $this->authorizeValidated()) {
$this->failedAuthorization();
}
});
}
/**
* Define the abstract method to run the logic.
*
* @return void
*/
abstract public function authorizeValidated();
}
Then in your request class:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Traits\AuthorizesAfterValidation;
class SomeKindOfRequest extends FormRequest
{
use AuthorizesAfterValidation;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorizeValidated()
{
return true; // <---- Set your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}
Source https://github.com/laravel/framework/issues/27808#issuecomment-470394076
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.