I found method Request::replace, that allows to replace input parameters in Request.
But currently i can see only one way to implement it - to write same replacing input code in every controller action.
Is it possible somehow to group code, that will be executed after request successful validation, but before controller action is started?
For example, i need to support ISO2 languages in my api, but under the hood, i have to transform them into legacy ones, that are really stored in the database. Currently i have this code in controller:
// Controller action context
$iso = $request->input('language');
$legacy = Language::iso2ToLegacy($iso);
$request->replace(['language' => $legacy]);
// Controller action code starts
I think what you're looking for is the passedValidation() method from the ValidatesWhenResolvedTrait trait
How to use it:
Create custom Request: php artisan make:request UpdateLanguageRequest
Put validation rules into the rules() method inside UpdateLanguageRequest class
Use passedValidation() method to make any actions on the Request object after successful validation
namespace App\Http\Requests;
use App\...\Language;
class UpdateLanguageRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
// here goes your rules, f.e.:
'language' => ['max:255']
];
}
protected function passedValidation()
{
$this->replace(['language' => Language::iso2ToLegacy($this->language)]);
}
}
Use UpdateLanguageRequest class in your Controller instead Request
public function someControllerMethod(UpdateLanguageRequest $request){
// the $request->language data was already modified at this point
}
*And maybe you want to use merge not replace method since replace will replace all other data in request and the merge method will replace only specific values
This solution worked for me based on Alexander Ivashchenko answer above:
<?php
namespace App\Http\Requests\User;
class UserUpdateRequest extends UserRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(): array
{
return [
'name'=>'required|string',
'email'=>'required|string|email',
'password'=>'min:8'
];
}
}
Our parent UserRequest class:
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;
abstract class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Handle a passed validation attempt.
*
* #return void
*/
protected function passedValidation()
{
if ($this->has('password')) {
$this->merge(
['password' => Hash::make($this->input('password'))]
);
}
}
public function validated(): array
{
if ($this->has('password')) {
return array_merge(parent::validated(), ['password' => $this->input('password')]);
}
return parent::validated();
}
}
I am overriding validated method also. If we access each input element individually his answer works but in order to use bulk assignment in our controllers as follow we need the validated overriding.
...
public function update(UserUpdateRequest $request, User $user): JsonResource
{
$user->update($request->validated());
...
}
...
This happens because validated method get the data directly from the Validator instead of the Request. Another possible solution could be a custom validator wit a DTO approach, but for simple stuff this above it's enough.
Is it possible somehow to group code, that will be executed after
request successful validation, but before controller action is
started?
You may do it using a middleware as validator, for example:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
class InputValidator
{
public function handle($request, Closure $next, $fullyQualifiedNameOfModel)
{
$model = app($fullyQualifiedNameOfModel);
$validator = app('validator')->make($request->input(), $model->rules($request));
if ($validator->fails()) {
return $this->response($request, $validator->errors());
}
return $next($request);
}
protected function response($request, $errors)
{
if($request->ajax()) {
return new JsonResponse($errors, 422);
}
return redirect()->back()->withErrors($errors)->withInput();
}
}
Add the following entry in the end of $routeMiddleware in App\Http\Kernel.php class:
'validator' => 'App\Http\Middleware\InputValidator'
Add the rules method in Eloquent Model for example, app\Product.php is model and the rules method is declared as given below:
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(\Illuminate\Http\Request $request)
{
return [
'title' => 'required|unique:products,title,'.$request->route()->parameter('id'),
'slug' => 'required|unique:products,slug,'.$request->route()->parameter('id'),
];
}
Declare the route like this:
$router->get('create', [
'uses' => 'ProductController#create',
'as' => 'Product.create',
'permission' => 'manage_tag',
'middleware' => 'validator:App\Product' // Fully qualified model name
]);
You may add more middleware using array for example:
'middleware' => ['auth', 'validator:App\Product']
This is a way to replace the FormRequest using a single middleware. I use this middleware with model name as argument to validate all my models using a single middleware instead of individual FormRequest class for each controller.
Here, validator is the middleware and App\Product is the model name which I pass as argument and from within the middleware I validate that model.
According to your question, the code inside your controller will be executed only after input validation passes, otherwise the redirect/ajax response will be done. For your specific reason, you may create a specific middleware. This is just an idea that could be used in your case IMO, I mean you can add code for replacing inputs in the specific middleware after validation passes.
Use merge instead of replace
$iso = $request->merge('language');
$legacy = Language::iso2ToLegacy($iso);
$request->merge(['language' => $legacy]);
Related
A question has arisen.
Suppose I have an api route that is
get: api/v1/conferences/{conference_id}/languages
The purpose would be to get the languages linked to this conference.
My program, almost always starts with conferences/{conference_id} so the conference_id must always be real. In case the conference_id does not exist, I should throw an exception.
I want to do this without putting any logic in the controllers or any class that has any particular logic of mine. I would like it to be validated by default from the laravel kernel, is that possible?
I mean, i want that every time somebody access to a route which starts with conferences/{conference_id} the program would be able to check if this id is real
Thanks
Use exists in validation rules like:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ConferencesRequest extends FormRequest
{
/**
* #inheritDoc
*/
public function all($keys = null)
{
$data = parent::all();
$data['conference_id'] = $this->route('conference_id');
return $data;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'conference_id' => ['required', 'integer', 'exists:' . App\Models\Conference::class . ',id'],
];
}
}
Or, if you dont want to use a FormRequest class, use this:
p.s. param should be changed to conference instead of conference_id:
use App\Models\Conference;
Route::get('conferences/{conference}/languages', function (Conference $conference) {
//...
});
See: https://laravel.com/docs/9.x/routing#route-model-binding
On validation fail, I have 2 situations
one when I display a new error page (this is how it was done before in the app)
I have a form and I redirect back with input (my problem)
The app is catching the ValidationException in the exceptions Handler - so there is no back with errors for me if a request fails.
Now, I need to return back to the input with errors - from the request class, before it throws the standard ValidationException.
How do I do that from the request class pls?
The code is very basic
I have some rules...
I imagine I need a hook like - validationFailed().. in the request class?
Edit: - not API - regular monolithic PHP
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Auth;
class LocationCodeRequest 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()
{
$user = Auth::user();
return [
'new_location_code' => 'required|string|max:255|exists:xxx,code',
];
}
/**
* Get the error messages for the defined validation rules.
*
* #return array
*/
public function messages()
{
return [
'new_location_code.not_in' => 'xxx',
'new_location_code.exists' => 'yyy',
];
}
//I need something like this
public function validationFailed()
{
return redirect()->back()->withErrors($this->validator)->withInput();;
}
}
Normally the FormRequest Class will redirect back to the Form Page with inputs and validation errors. That's the default functionality.
public function validationFailed() {
return redirect()->back()->withErrors($this->validator)->withInput();;
}
You don't require the above code if you are passing the FormRequest class in the POST call of the Form like so
use App\Http\Requests\LocationCodeRequest;
public function store(LocationCodeRequest $request) {
// Code after validation
}
You can try by adding this in the LocationCodeRequest file.
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
protected function failedValidation(Validator $validator) {
// Add redirection here. Errorbag and Inputs shud be available in session or can be passed here
//throw new HttpResponseException(response()->json($validator->errors(), 422));
}
I am new in Laravel, what I try to achieve is very simple thing, I would like to use FormRequest provided by Laravel to do validation of the request, but I encounter some puzzles (which I am sure is easy things to solve if you are experienced in Laravel).
Here is what I tried:
I have route maps to controller:
Route::put('user/{name}', 'UserController#show');
I can get the name parameter in show function:
class UserController {
public function show($name)
{
// validtion rules to apply
...
}
}
I have validation rules to apply to the request, so I decided to create form request by php artisan make:request ShowRequest, which creates the form request class:
class ShowRequest extends FormRequest {
public function authorize()
{
return true;
}
public function rules()
{
return [
// my validation rules here
];
}
}
Since I have above request class, so I refactored the show function in controller to receive the ShowRequest .
class UserController {
public function show(ShowRequest $request)
{
// now I don't need validtion rules in this function
// but how can I access the 'name' parameter now
...
}
}
I have two questions to ask:
Inside the refactored show function, how can I now access the route parameter name ?
If we forget about the parameter is a name (please don't focus on what to validate for name, imaging it is an object or value to validate in general). How to add custom logic for handling validation error instead of using Laravel default behaviour. I want to inject code like dummy code below:
if (!$validator->pass())
{
//my custom code for handling validation failure
}
Where to put my custom code for handling validation error now? I mean I don't know where to have this logic, in controller? in the request class? how?
You still can add the parameter $name in the show() method of your controller as it's part of the routed url more than the validated form/data. (recommanded)
class UserController {
public function show(ShowRequest $request, $name)
{
//...
}
}
You can also access it from the request object
class UserController {
public function show(ShowRequest $request)
{
$request->input('name');
}
}
As for the error messages (not the exception) you can add the messages() method to your ShowRequest::class
class ShowRequest extends FormRequest
{
/**
* #return array
*/
public function messages()
{
return [
'name.required' => 'The name is required',
'name.numeric' => 'The name must be a number',
//...
];
}
}
If you instead need to validate that the name catched by the route is only composed of letter OR really exists as a field in your DB (like a slug of a post) you need to add some validation in your route declaration.
Setup a route that catches request only if it is composed of letters.
Route::get('/user/{name}', 'Controller#show')->where(['name' => '[a-z]+']);
Setup a route that catches request only if the "name" exists in DB:
User.php
Class User //..
{
/**
* Get the route key for the model.
*
* #return string
*/
public function getRouteKeyName()
{
return 'name';
}
}
web.php
//
Route::get('/user/{user:name}', 'Controller#show');
And adapt your controller to take a user directly
class UserController {
public function show(ShowRequest $request, User $user)
{
//...
}
}
You can access the values of the Form Request using this
$validated = $request->validated();
The $validated will have all the values which had been validated by the FormRequest.
To answer your second question, if you want to throw custom validation, you can always use the following
throw ValidationException::withMessages(['name' => 'Something is wrong']);
I'm in the process of developing a web app and I've run into a problem where I need to validate form input. I've created a validation class according to the laravel docs and type-hinted the class in my method. The problem is that I've already got the Illuminate\Http\Request class type-hinted in my method and type-hinting my validation class in the same method brings up a "Forbidden" message when I refresh the page.
Validation class
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ValidateCalculatorValues extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'price' => 'required|numeric',
'deposit' => 'required|numeric',
'months' => 'required|numeric',
'interest' => 'required|numeric',
];
}
}
Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Cookie\CookieJar;
use App\Http\Requests\ValidateCalculatorValues;
public function index($id, Request $request, ValidateCalculatorValues $calculatorInput, CookieJar $cookieJar)
{
// Code goes here
}
How do I go about doing this so that it works? Am I missing something?
As phobia82 mentioned in the comments, I needed to set the authorize method on my validation class to true.
But because the form is posting to the same url that the form is on it was creating an infinite redirect loop because my validation rules were set so that all fields were required. After looking at the documentation, I changed "required" to "filled" which solved the redirect loop.
Omit the Request $request in your parameter since your ValidateCalculatorValues will now handle the validation.
public function index($id, ValidateCalculatorValues $calculatorInput, CookieJar $cookieJar)
{
$allYourInputs = $calculatorInput->all();
}
or you could do this for more convention
public function index($id, ValidateCalculatorValues $request, CookieJar $cookieJar)
{
$allYourInputs = $request->all();
}
Consider this code inside a model:
public function rules() {
return [
[['company_name', 'first_name', 'last_name'], 'sanitize'],
//........
];
}
sanitize is a custom method inside the current class, which is:
public function sanitize($attribute) {
$this->{$attribute} = general::stripTagsConvert($this->{$attribute}, null, true);
}
Now this method obviously will come in handy in many models so I don't want to keep repeating the same code in every model. Is there a way I can reference another class in the rules in place of the current sanitize method name which is binded to the current class?
Yes, it's definitely possible.
Create separate validator. Let assume it's called SanitizeValidator and placed in common/components folder.
Your custom validator must extend from framework base validator and override validateAttribute() method. Put your logic inside this method:
use yii\validators\Validator;
class SanitizeValidator extends Validator
{
/**
* #inheritdoc
*/
public function validateAttribute($model, $attribute)
{
$model->$attribute = general::stripTagsConvert($model->$attribute, null, true);
}
}
Then in model you can attach this validator like this:
use common/components/SanitizeValidator;
/**
* #inheritdoc
*/
public function rules()
{
return [
[['company_name', 'first_name', 'last_name'], SanitizeValidator::className()],
];
}
Check the official documentation about custom validators here and there.