Some service makes HTTP request to my site and passes some input. This input has a little bit wrong structure for me, so I'm trying to modify it.
I made a middleware and attached this middleware to my route. The handle method looks like this:
public function handle($request, Closure $next)
{
$input = $request->all();
// Input modification
$request->replace($input);
\Log::info($request->all()); // Shows modified request
return $next($request);
}
However in my controller I got old input. Also I'm a little bit confused since I also use FormRequest, and as I realize these two requests are different entities. Then how can I modify the input in the middleware?
I don't know what's the exact problem in your case but I'll show you what I did to make it work and it might solve your problem:
app/Http/Middleware/TestMiddleware.php
<?php namespace App\Http\Middleware;
use Closure;
class TestMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$input = $request->all();
if (isset($input['mod'])) {
list($input['int'], $input['text']) = explode('-', $input['mod']);
unset($input['mod']);
// Input modification
$request->replace($input);
\Log::info($request->all()); // Shows modified request
}
return $next($request);
}
}
app/Http/Kernel.php
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'App\Http\Middleware\VerifyCsrfToken',
Middleware\TestMiddleware::class, // this line added
];
app/Http/routes.php
Route::get('/test', ['uses' => 'TestController#index']);
app/Http/Requests/SampleRequest.php
<?php namespace App\Http\Requests;
class SampleRequest extends Request
{
public function rules()
{
return [
'int' =>
[
'required',
'integer'
],
'text' => [
'max: 5',
]
];
}
}
app/Http/Controllers/TestController.php
<?php namespace App\Http\Controllers;
use App\Http\Requests;
class TestController extends \Illuminate\Routing\Controller
{
public function index(Requests\SampleRequest $request)
{
dd($request->all());
}
}
In console I've run composer dump-autoload.
Now when I run the following url:
http://testproject.app/test?mod=23-tav
I'm getting in controller from dd:
array:2 [▼
"text" => "tav"
"int" => "23"
]
as expected and when I run for example http://testproject.app/test?mod=abc-tav I'm being redirected to mainpage in my case because data doesn't pass validation from SampleRequest (int is not integer)
Related
Hi please help me with the following,
on Laravel 5.5
I have the following routes:
This one works:
Route::delete('/delete-comment/{id}', 'CommentController#destroy');
This one does not work as I'm using the same method as the above route and does not have the first parameter 're_id' which is not required:
Route::delete('/your-template/{re_id}/delete-comment/{id}', 'CommentController#destroy');
The method being:
public function destroy($id)
{
//do something
}
I want to use the same method without the first parameter for the sub route 're_id'.
I do not need this kind of solution, since I want to use the same function for both routes.
public function destroy($re_id= '' $id)
{
//do something
}
Is there a way to ignore the first parameter 're_id' on the route or a more generic way to use a slug on the first fragment on the route like:
Which btw does not work:
Route::delete('/{slug?}/delete-comment/{id}', 'CommentController#destroy');
In PHP generally the optional parameter/s MUST be at the end...
for example this will cause a Fatal Error in PHP v7.1^
function test($first = null, $second)
{
echo $first .' '.$second;
}
test('string');
In your case I would try it like this (not sure if it will work)
Route::delete('/delete-comment/{id}/{slug?}', 'CommentController#destroy');
public function destroy($id, $re_id = null)
{
//do something
}
If anyone gets stuck on this, I got a solution:
1.- Create a config file 'route.php'
<?php
return [
'filters' => [
// Routes
'your-template/{re_id}/delete-comment/{id}',
'your-template/{re_id}/update-comment/{id}',
'article' => [
// Route arguments {name}
're_id',
]
]
];
2.- Create a Middleware with the command:
php artisan make:middleware RouteArgumentsFilterMiddleware
<?php
namespace App\Http\Middleware;
use Closure;
class RouteArgumentsFilterMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$config = config('route.filters');
if (in_array($request->route()->uri, $config)) {
foreach ($config['article'] as $argument) {
$request->route()->forgetParameter($argument);
}
}
return $next($request);
}
}
3.- On your web.php file add the middleware to the needed routes:
Route::delete('/your-template/{re_id}/delete-comment/{id}', 'CommentController#destroy')->middleware('param_filter');
Route::put('/your-template/{re_id}/update-comment/{id}', 'CommentController#update')->middleware('param_filter');
4.- run composer dump-autoload
Then the desired parameter will be ignored when sent to the controller.
Laravel version: 5.1.46
routes.php
Route::get('/rocha', 'RochaController#index');
Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'Age' => \App\Http\Middleware\AgeMiddleware::class,
'Role' => \App\Http\Middleware\RoleMiddleware::class,
];
RochaController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
class RochaController extends Controller
{
public function __construct() {
$this->middleware('Role');
}
public function index() {
echo '<br>Hi I am index';
}
}
RochaMiddleware.php
namespace App\Http\Middleware;
use Closure;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
echo '<br>Hi I am middleware';
return $next($request);
}
public function terminate($request, $response) {
echo '<br>Shtting down...';
}
}
Result:
Hi I am middleware
Hi I am index
When I use middleware inside the controller via its constructor $this->middleware('Role') the terminate() function does not get called. When I switch the code taking out the constructor in the controller and change the route to the following the terminate() function gets called:
Route::get('/rocha', [
'middleware' => 'Role',
'uses' => 'RochaController#index'
]);
Result:
Hi I am middleware
Hi I am index
Shtting down...
Why does the constructor version ($this->middleware('Role')) prevent the terminate() function from being called?
Why does the route version work and the terminate() function is called as opposed to the above?
If you define a terminate method on your middleware, it will automatically be called after the response is ready to be sent to the browser.
from terminable-middleware
I think you misunderstand the usage of terminate method. Laravel actually call the terminate method, but the browser will not show the output of terminate. Because the response has been sent to browsers.
You can use this terminate method to test whether the call is successful.
public function terminate($request, $response)
{
file_put_contents(__DIR__ . '/1.txt', 'hello terminate');
}
By the way, I'm test your code, it always output:
Hi I am middleware
Hi I am index
I also wonder why you can get Shtting down...
Request class
class LoginRequest extends Request
{
public function authorize() {
return true;
}
public function rules() {
return [
'EmailAddress' => 'required',
'Password' => 'required',
];
}
public function messages() {
return [
"EmailAddress.required" => trans("login.RequiredEmailAddress"),
"Password.required" => trans("login.RequiredPassword")
];
}
}
Route
Route::post('/AuthenticateUser',
array(
'uses' => 'API\Login\apiLoginController#AuthenticateUser',
'as' => 'AuthenticateUser'
)
);
Controller Action Method
I have a controller, I did so far for request class only to validate the input parameters. below is the action method
public function AuthenticateUser(LoginRequest $request) {
dd("Hello");
}
Url
localhost:85/Laravel/public/api/v1/AuthenticateUser
I am using Postman Chrome extension to test the Url. So, as we can see that in the Request class both Email Address and the password are required parameters.
When I pass both parameters value. there is not issue and everything works. When I keep the Email Address value empty...I got 404 error and here is the screenshot.
Am I missing something to get rid of 404 error when Email address is not given? I am expecting an error message to enter Email Address
Below is the working state when I pass both email and password
Solution 1:
I managed to get rid of the 404 and return a 422 by adding the following header in the request:
accept:application/json
This is not really a bug in Laravel as Taylor pointed out but a way to differentiate if it is an AJAX/API request or not.
Solution 2:
Alternatively, if you don't want the client to specify that header, you can create a middleware that will add the header accept:application/json on every API requests. Here's how:
Create a new middleware: app/Http/Middleware/ForceJsonResponse.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
In /app/Http/Kernel.php, inside $middlewareGroups.api, specify the namespace to your newly created middleware:
protected $middlewareGroups = [
'web' => [...],
'api' => [
[...]
\App\Http\Middleware\ForceJsonResponse::class,
],
];
Finally got it working by changing the request class like below.
class LoginRequest extends Request
{
public function wantsJson() {
return true;
}
public function authorize() {
return true;
}
public function rules() {
return [
'EmailAddress' => 'required',
'Password' => 'required',
];
}
public function messages() {
return [
"EmailAddress.required" => trans("login.RequiredEmailAddress"),
"Password.required" => trans("login.RequiredPassword")
];
}
}
just added below code.
public function wantsJson() {
return true;
}
It is because you are validating directly on route handling and not matching throughs NotFoundException. You need to pass the Request to your Controller as is and do:
$this->validate($request, [
'EmailAddress' => 'required|email',
'Password' => 'required',
]);
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();
}
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]);