I'm using dingo/api package.
Controller:
public function register(RegisterUserRequest $request)
{
dd('a');
}
And for example the email field is required:
<?php namespace App\Http\Requests;
class RegisterUserRequest extends Request
{
/**
* 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()
{
return [
'email' => 'required'
];
}
}
So I send a request without the email, and still getting the "a" response.
I also tried to extend Dingo\Api\Http\Request instead of App\Http\Request, but still the same.
For Dingo to work at all with the FormRequest, by experience (and from this Issue), you have to use Dingo's Form request i.e Dingo\Api\Http\FormRequest; , so you'll have something similar to:
<?
namespace App\Http\Requests;
use Dingo\Api\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\HttpException;
class RegisterUserRequest 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()
{
return [
'email' => 'required'
];
}
// In case you need to customize the authorization response
// although it should give a general '403 Forbidden' error message
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof \Dingo\Api\Http\Request) {
throw new HttpException(403, 'You cannot access this resource'); //not a user?
}
}
}
PS: This is tested on Laravel 5.2.*
Hope it helps :)
According to the Wiki
you must overload the failedValidation and failedAuthorization methods.
These methods must throw one of the above mentioned exceptions and not the response HTTP exceptions that Laravel throws.
If you take a look at Dingo\Api\Http\FormRequest.php, you'll see:
class FormRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
*
* #return mixed
*/
protected function failedValidation(Validator $validator)
{
if ($this->container['request'] instanceof Request) {
throw new ValidationHttpException($validator->errors());
}
parent::failedValidation($validator);
}
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof Request) {
throw new HttpException(403);
}
parent::failedAuthorization();
}
}
Hence, you need to change the names of your methods appropriately, and have them throw the appropriate exceptions, instead of returning a boolean.
you need to call the validate function explicitly when you run it under an Dingo API setup, try something like this (for L5.2):
Probably a few extra providers
...
Illuminate\Validation\ValidationServiceProvider::class,
Dingo\Api\Provider\LaravelServiceProvider::class,
...
Aliases
...
'Validator' => Illuminate\Support\Facades\Validator::class,
...
I'm also pretty much sure that you really don't want to use this below as suggested here and there, It will expect form(encoded) input and will also probably fail on CSRF token as it expects it, so it will fail right after validating (form input). But make sure to test behavior with this on/off.
use Dingo\Api\Http\FormRequest;
Make your headers:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Dingo\Api\Exception\ValidationHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/* This can be a tricky one, if you haven't split up your
dingo api from the http endpoint, there are plenty
of validators around in laravel package
*/
use Validator;
Then the actual code (if you adhere to cors standard,
this should be a POST and that commonly translates to a store request)
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function register(RegisterUserRequest $request) {
$validator = Validator::make($request->all(), $this->rules());
if ($validator->fails()) {
$reply = $validator->messages();
return response()->json($reply,428);
};
dd('OK!');
};
...
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
// or/and 'userid' => 'required'
];
}
That will give you back the response you expect from the validator. If you use this with pregenerated forms, it does not need this fix, there the validator will kick in automatically. (not under Dingo Api).
you probably also need these in composer.json
"dingo/api": "1.0.*#dev",
"barryvdh/laravel-cors": "^0.7.1",
This is untested, by heart, it took me 2 days to figure this out but I have a separate namespace for API specific and authenticated with middleware. success
Related
I am still very beginner in testing with the laravel framework. Because of this, I performed tests on the endpoints of my api, but for the post request I have problems with the methods assertOk(), assertStatus() and other it returns an error, but when I leave the test without the asserts it returns a warning.
Here is my test code
<?php
namespace Tests\Feature;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Request;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
use Tests\TestCase;
class LocationTest extends TestCase
{
/**
* A basic feature test example.
*
* #return void
*/
public function test_get_all_report()
{
Http::fake([
'http://127.0.0.1:8000/*' => Http::response([
[
"latitude"=> "1.56786467",
"longitude"=> "-0.056789685",
"message"=> "test test that's a place of risk"
],
[
"latitude"=> "1.56786467",
"longitude"=> "-0.056789685",
"message"=> "test test that's a place of risk"
]
], 200),
]);
$response = $this->getJson('api/get-all-signal-zone')->assertStatus(200);
}
/**
* A basic feature test example.
*
* #return void
*/
public function test_get_all_report_catch_error()
{
Http::fake([
'http://127.0.0.1:8000/*' => Http::response([
[
"message"=> "Server error"
]
], 500),
]);
$response = $this->getJson('api/get-all-signal-zone')->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
}
/**
* A basic feature test example.
*
* #return void
*/
public function test_get_all_report_by_date()
{
Http::fake();
$response = Http::post('http://127.0.0.1:8000/api/get-all-signal-zone-by-date', [
"date"=> "2022-11-10"
]);
}
}
my route file
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\LocationController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::get('/get-all-signal-zone', [LocationController::class, 'all']);
// Route::get('/get-all-signal-zone-by-date/{date}', [LocationController::class, 'getLocationByDate'])->name('reporting_by_date');
Route::post('/get-all-signal-zone-by-date', [LocationController::class, 'getLocationByDate']);
Route::post('/send-signal-zone', [LocationController::class, 'save']);
And here my controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Repositories\Location\LocationRepository;
/**
* Description of LocationController
*
* #author Amoungui
*/
class LocationController extends Controller
{
public function __construct(LocationRepository $locationRepository) {
$this->locationRepository = $locationRepository;
}
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function all(){
return $this->locationRepository->list();
}
/**
* View a list of the resource based on a date.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function getLocationByDate(Request $request){
return $this->locationRepository->getByDate($request->date);
}
/**
* Save a newly created resource in external api.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function save(Request $request){
$response = $this->locationRepository->store($request->all());
if($response->successful()){
return response()->json([
'status' => true,
'message' => "location submited successfully",
'data' => $request->all()
], 201);
}
return response()->json([
'data' => null,
'message' => 'Failed',
], 500);
}
}
I have this error
WARN Tests\Feature\LocationTest
✓ get all report
✓ get all report catch error
! get all report by date → This test did not perform any assertions C:\wamp64\www\backend-11-user-story-reporting-of-a-place-in-danger\tests\Feature\LocationTest.php:61
Tests: 1 risky, 4 passed
Time: 0.64s
In Laravel, there is a toolset for mocking anything you need to mock as you can see here. I think you have misunderstood what HTTP::fake() does. This function mocks outgoing HTTP requests in your code and instead returns an answer without executing the request.
if you're mocking your own endpoint (i.e 127.0.0.1:8000/*) you're practically preventing Laravel from sending a request to its endpoint and process it as you're returning a fake answer without ever triggering your controller. So the tests are practically pointless at this state.
The main point of HTTP::fake() (warning, opinion ahead) is to mock calls to external endpoints, for example, if your LocationRepository sends a request to http://not-a-real-location-api.com, you'd want to mock that and assume its response since you don't want your tests triggering actual APIs.
Consider removing the use of Http::fake() entirely since in your case it defeats the purpose of your tests, but just to make this answer whole, note that when Http::fake() is called without parameters it returns a default empty HTTP 200 response.
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
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
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
I'm using laravel 5.5
I have a Request that I've built but the required rule is not working correctly.
Route
Route::get('v1/learning_centre/user/{userId}/course/list', 'API\LearningCentre#userCourses');
Controller
public function userCourses(GetUserCourses $request)
{
$courses = User::findOrFail($request->userId)
->courses()
->get();
return new CourseResourceCollection($courses);
}
Request
namespace App\Http\Requests\LearningCentre;
use Illuminate\Foundation\Http\FormRequest;
class GetUserCourses 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()
{
return [
'userId' => 'required|integer'
];
}
/**
* Get the error messages for the defined validation rules.
*
* #return array
*/
public function messages()
{
return [
'userId.required' => 'A User is required',
];
} }
If I turn off the required rule I can get to the controller. If I have the required rule in the request I get a 302. I am passing in a valid userId in phpunit. Without the request rules my code works as intended.
Any ideas?
You should be using route model binding to validate a required GET parameter in this situation, not a FormRequest class, which, as the name should indicate, are intended for form requests.
Your route:
Route::get('v1/learning_centre/user/{user}/course/list', 'API\LearningCentre#userCourses');
Your controller:
public function userCourses(User $user) {
If a user ID is missing (or an invalid one used), your controller will automatically throw a ModelNotFoundException, which Laravel by default returns as a 404.
I've been trying to add a FormRequest with rules and message to my delete method, but the request is coming back empty and the rules are failing every time.
Is it possible to get the request data in a delete method?
Here's my request class:
use App\Http\Requests\Request;
class DeleteRequest extends Request
{
/**
* 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()
{
return [
'staff_id' => ['required', 'exists:users,uid'],
'reason' => ['required', 'string'],
];
}
/**
* Get custom messages for validator errors.
*
* #return array
*/
public function messages()
{
return [
'staff_id.required' => staticText('errors.staff_id.required'),
'staff_id.exists' => staticText('errors.staff_id.exists'),
'reason.required' => staticText('errors.reason.required'),
'reason.string' => staticText('errors.reason.string'),
];
}
}
And the controller:
/**
* Handle the 'code' delete request.
*
* #param integer $id The id of the code to fetch.
* #param DeleteRequest $request The request to handle the data.
* #return response
*/
public function deleteCode($id, DeleteRequest $request)
{
dd($request->all());
}
Even though the HTTP/1.1 spec does not explicitly state that DELETE requests should not have an entity body, some implementations completely ignore the body which contains your data, e.g. some versions of Jetty and Tomcat. On the other hand, some clients do not support sending it as well.
Think of it as a GET request. Have you seen any with form data? DELETE requests are almost the same.
You can read a LOT on the subject. Start here:
RESTful Alternatives to DELETE Request Body
It seems like that you want to alter the state of the resource rather than destroying it. Soft-deleting is not deleting and thus requires either a PUT or a PATCH method which both support entity bodies. If soft-deleting is not the case, you're doing two operations through one call.
I'm using FormRequest to validate from which is sent in an API call from my smartphone app. So, I want FormRequest alway return json when validation fail.
I saw the following source code of Laravel framework, the default behaviour of FormRequest is return json if reqeust is Ajax or wantJson.
//Illuminate\Foundation\Http\FormRequest class
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
I knew that I can add Accept= application/json in request header. FormRequest will return json. But I want to provide an easier way to request my API by support json in default without setting any header. So, I tried to find some options to force FormRequest response json in Illuminate\Foundation\Http\FormRequest class. But I didn't find any options which are supported in default.
Solution 1 : Overwrite Request Abstract Class
I tried to overwrite my application request abstract class like followings:
<?php
namespace Laravel5Cg\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\JsonResponse;
abstract class Request extends FormRequest
{
/**
* Force response json type when validation fails
* #var bool
*/
protected $forceJsonResponse = false;
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->forceJsonResponse || $this->ajax() || $this->wantsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
}
I added protected $forceJsonResponse = false; to setting if we need to force response json or not. And, in each FormRequest which is extends from Request abstract class. I set that option.
Eg: I made an StoreBlogPostRequest and set $forceJsoResponse=true for this FormRequest and make it response json.
<?php
namespace Laravel5Cg\Http\Requests;
use Laravel5Cg\Http\Requests\Request;
class StoreBlogPostRequest extends Request
{
/**
* Force response json type when validation fails
* #var bool
*/
protected $forceJsonResponse = true;
/**
* 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()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
}
Solution 2: Add an Middleware and force change request header
I build a middleware like followings:
namespace Laravel5Cg\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\HeaderBag;
class AddJsonAcceptHeader
{
/**
* Add Json HTTP_ACCEPT header for an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$request->server->set('HTTP_ACCEPT', 'application/json');
$request->headers = new HeaderBag($request->server->getHeaders());
return $next($request);
}
}
It 's work. But I wonder is this solutions good? And are there any Laravel Way to help me in this situation ?
It boggles my mind why this is so hard to do in Laravel. In the end, based on your idea to override the Request class, I came up with this.
app/Http/Requests/ApiRequest.php
<?php
namespace App\Http\Requests;
class ApiRequest extends Request
{
public function wantsJson()
{
return true;
}
}
Then, in every controller just pass \App\Http\Requests\ApiRequest
public function index(ApiRequest $request)
I know this post is kind of old but I just made a Middleware that replaces the "Accept" header of the request with "application/json". This makes the wantsJson() function return true when used. (This was tested in Laravel 5.2 but I think it works the same in 5.1)
Here's how you implement that :
Create the file app/Http/Middleware/Jsonify.php
namespace App\Http\Middleware;
use Closure;
class Jsonify
{
/**
* Change the Request headers to accept "application/json" first
* in order to make the wantsJson() function return true
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
Add the middleware to your $routeMiddleware table of your app/Http/Kernel.php file
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'jsonify' => \App\Http\Middleware\Jsonify::class
];
Finally use it in your routes.php as you would with any middleware. In my case it looks like this :
Route::group(['prefix' => 'api/v1', 'middleware' => ['jsonify']], function() {
// Routes
});
Based on ZeroOne's response, if you're using Form Request validation you can override the failedValidation method to always return json in case of validation failure.
The good thing about this solution, is that you're not overriding all the responses to return json, but just the validation failures. So for all the other Php exceptions you'll still see the friendly Laravel error page.
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;
class InventoryRequest extends FormRequest
{
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response($validator->errors(), Response::HTTP_UNPROCESSABLE_ENTITY));
}
}
if your request has either X-Request-With: XMLHttpRequest header or accept content type as application/json FormRequest will automatically return a json response containing the errors with a status of 422.
i just override the failedValidation function
protected function failedValidation(Validator $validator)
{
if ($this->wantsJson()) {
throw new HttpResponseException(
Response::error(__('api.validation_error'),
$validator->errors(),
470,
[],
new ValidationException)
);
}
parent::failedValidation($validator);
}
So my custom output sample like below:
{
"error": true,
"message": "Validation Error",
"reference": [
"The device id field is required.",
"The os version field is required.",
"The apps version field is required."
],
}
BTW Response::error dont exist in laravel. Im using macroable to create new method
Response::macro('error', function ($msg = 'Something went wrong', $reference = null, $code = 400, array $headers = [], $exception = null) {
return response()->json(//custom here);
});
I came to this solution (Laravel 9):
throw new ValidationException(
$validator,
new JsonResponse([
'errors' => $validator->errors()->messages(),
], 422),
);