I'm currently playing around with Exception Handler, and creating my own custom exceptions.
I've been using PHPUnit to run tests on my Controller Resource, but when I throw my custom exceptions, Laravel thinks it's coming from a regular HTTP request rathen than AJAX.
Exceptions return different response based on wether it's an AJAX request or not, like the following:
<?php namespace Actuame\Exceptions\Castings;
use Illuminate\Http\Request;
use Exception;
use Actuame\Exceptions\ExceptionTrait;
class Already_Applied extends Exception
{
use ExceptionTrait;
var $redirect = '/castings';
var $message = 'castings.errors.already_applied';
}
And the ExceptionTrait goes as follows:
<?php
namespace Actuame\Exceptions;
trait ExceptionTrait
{
public function response(Request $request)
{
$type = $request->ajax() ? 'ajax' : 'redirect';
return $this->$type($request);
}
private function ajax(Request $request)
{
return response()->json(array('message' => $this->message), 404);
}
private function redirect(Request $request)
{
return redirect($this->redirect)->with('error', $this->message);
}
}
Finally, my test goes like this (excerpt of the test that's failing)
public function testApplyToCasting()
{
$faker = Factory::create();
$user = factory(User::class)->create();
$this->be($user);
$casting = factory(Casting::class)->create();
$this->json('post', '/castings/apply/' . $casting->id, array('message' => $faker->text(200)))
->seeJsonStructure(array('message'));
}
My logic is like this although I don't think the error is coming from here
public function apply(Request $request, User $user)
{
if($this->hasApplicant($user))
throw new Already_Applied;
$this->get()->applicants()->attach($user, array('message' => $request->message));
event(new User_Applied_To_Casting($this->get(), $user));
return $this;
}
When running PHPUnit, the error I get returned is
1) CastingsTest::testApplyToCasting PHPUnit_Framework_Exception:
Argument #2 (No Value) of PHPUnit_Framework_Assert:
:assertArrayHasKey() must be a array or ArrayAccess
/home/vagrant/Code/actuame2/vendor/laravel/framework/src/Illuminate/Foundation/T
esting/Concerns/MakesHttpRequests.php:304
/home/vagrant/Code/actuame2/tests/CastingsTest.php:105
And my laravel.log is over here http://pastebin.com/ZuaRaxkL (Too large to paste)
I have actually discovered that PHPUnit is not actually sending an AJAX response, because my ExceptionTrait actually changes the response on this. When running the test it takes the request as a regular POST request, and runs the redirect() response rather than ajax(), hence it's not returning the correspond.
Thanks a bunch!
I have finally found the solution!
As I said, response wasn't the right one as it was trying to redirect rathen than return a valid JSON response.
And after going through the Request code, I found out that I need to use also wantsJson(), as ajax() may not be the case always, so I have modified my trait to this:
<?php
namespace Actuame\Exceptions;
trait ExceptionTrait
{
public function response(Request $request)
{
// Below here, I added $request->wantsJson()
$type = $request->ajax() || $request->wantsJson() ? 'ajax' : 'redirect';
return $this->$type($request);
}
private function ajax(Request $request)
{
return response()->json(array('message' => $this->message), 404);
}
private function redirect(Request $request)
{
return redirect($this->redirect)->with('error', $this->message);
}
}
Related
Something like Request::only() that throws an error if there is any extra/unexpected field in the request?
E.g.:
$request->strictly(['username', 'password']);
that would throw an error if POST request contains fields 'username', 'password', and 'foo'?
In case there isn't, what would be the best way to perform such a verification manually?
You can create your own strictly method in the request class using Macro
AppServiceProvider.php and inside boot method add this method
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
public function boot()
{
Request::macro('strictly', function(array $paramaters) {
$array = array_diff(request()->keys(), $paramaters);
if (count($array) > 0) {
throw ValidationException::withMessages(
collect($array)->map(function ($row) {
return [$row => 'not needed'];
})->collapse()->all()
);
}
});
}
Now You can use this method inside the controller or request class
request()->strictly(['username', 'password']);
To transform a database entity to an API response Laravel support resources, eg. UserResource extends JsonResource. The resource allows me to cleanly define which fields from the entity should be included in the response, how to transform them etc.
Is there a similar functionality for requests? My requests typically look like this:
public function create(JsonRequest $request): UserResource
{
$data = $request->json()->all();
/* Remove, transform, add request fields etc. */
$user = User::create($data);
$user->save();
return new UserResource($user);
}
In our case we have a legacy database behind a modern API so there are a number of fields that need to transformed, renamed etc. before pushing them into the entity class. The fields differ from request to request but the steps are very similar. Is there a less boilerplate-y way to do this, something similar to how resources transform entities to responses?
Something like:
class UserRequest extends JsonRequest {
public function fromArray(JsonRequest $request) {
…
}
}
Then the request could look like this:
public function create(UserRequest $request): UserResource
{
$user = User::create($request);
$user->save();
return new UserResource($user);
}
I suppose, that most of your problems can solve form request. See example below
Form request class:
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class TestRequest 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 [
'date' => 'required|date_format:Y-m-d H:i:s',
'name' => 'required|string',
];
}
// here you can specify custom error messages
public function messages()
{
return [
'date.required' => 'No date specified',
'date.date_format' => 'Invalid date format',
'name.required' => 'No name specified',
'name.string' => 'Invalid name format',
];
}
// here you can implement some data mapping before validation
protected function validationData()
{
return $this->transform($this->all());
}
// some data transformation logic
// You can place it anywhere in your applciation services
protected function transform($input)
{
$transformed = [];
foreach ($input as $field => $value) {
if ($field == 'name') {
$value = strtoupper($value);
} elseif ($field == 'date') {
$value = Carbon::parse($value)->toDateTimeString();
}
$transformed[$field] = $value;
}
return $transformed;
}
public function failedValidation(Validator $validator)
{
// here you can implement custom validation failure
parent::failedValidation($validator);
}
}
Here is my test route: Route::get('/test', 'TestController#index');
And controller:
use App\Http\Requests\TestRequest;
class TestController extends Controller
{
public function index(TestRequest $request)
{
return response()->json($request->validated());
}
}
So, then requesting route: curl -H 'Accept: application/json' 'http://localhost:8000/test?date=01.01.2019&name=petya'
And getting response: {"date":"2019-01-01 00:00:00","name":"PETYA"}
And dont be shy to see source code of request and form request, cause of not all methods you wish are described in docs. Hope this helps
Good morning, i have a trouble.
Let me detail and i will be grateful with yours
I Have a Controller that store the resources - a trait that catch the http_error_codes (404,500)
The trouble is when i try to save a duplicated register on the database the validator didnt work. I am validating with a Request and i want that if is a Query Exception appear a message saying "This field cant not be duplicated (for example)"
Mi Controller
public function store(StoreCategory $request)
{
$data = request()->all();
$newCategory = Category::create($data);
return $this->respondCreated(new CategoryResource($newCategory));
}
My Trait
public function respondCreated($data)
{
$response = $data->response();
return $this->setStatusCode(IlluminateResponse::HTTP_CREATED)->respond(
$response
);
}
public function respond($data, $headers = [])
{
$response = $data;
return $response->setStatusCode($this->statusCode);
}
On my trait i am validating the Status Codes, but all the duplicated registers on my DB appear like 404 because i can not validate
if ($this->isModelNotFoundException($e)) {
return $this->modelNotFound();
} elseif ($this->isHttpException($e) && $e->getStatusCode() == 404 ){
return $this->modelNotFound();
} elseif ($this->isHttpException($e) && $e->getStatusCode() >= 500){
return $this->internalError();
} else{
return $this->badRequest();
}
My Form Request
public function rules()
{
return [
'name' => 'required|max:50|unique:categories',
'parent_id' => 'required'
];
}
If you can help me with anything, I would be very grateful. Thank you in advance, and excuse my English, it's not very good
The problem is that FormRequest causes ValidationException before your controller method runs. If you wish to customize response, this is possible at app/Exceptions/Handler. Do something like:
public function render($request, Exception $exception)
{
if ($exception instanceof ValidationException) {
$errors = $exception->validator->getMessageBag();
//some handling based on errors
}
}
Another way to solve this issue is define at your FormRequest method failedValidation:
protected function failedValidation(Validator $validator)
{
throw new CustomValidationException($validator, $this->response(
$this->formatErrors($validator)
));
}
Then define your own custom exception class with render method(for Laravel >= 5.5) or catch it with instanceof in render method of exception handler
If you want to solve problem without exceptions, you can move validation to controller and create custom response based on your validation logic
I am building a REST API with Laravel 5.
In Laravel 5, you can subclass App\Http\Requests\Request to define the validation rules that must be satisfied before a particular route will be processed. For example:
<?php
namespace App\Http\Requests;
use App\Http\Requests\Request;
class BookStoreRequest extends Request {
public function authorize() {
return true;
}
public function rules() {
return [
'title' => 'required',
'author_id' => 'required'
];
}
}
If a client loads the corresponding route via an AJAX request, and BookStoreRequest finds that the request doesn't satisfy the rules, it will automagically return the error(s) as a JSON object. For example:
{
"title": [
"The title field is required."
]
}
However, the Request::rules() method can only validate input—and even if the input is valid, other kinds of errors could arise after the request has already been accepted and handed off to the controller. For example, let's say that the controller needs to write the new book information to a file for some reason—but the file can't be opened:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Http\Requests\BookCreateRequest;
class BookController extends Controller {
public function store( BookStoreRequest $request ) {
$file = fopen( '/path/to/some/file.txt', 'a' );
// test to make sure we got a good file handle
if ( false === $file ) {
// HOW CAN I RETURN AN ERROR FROM HERE?
}
fwrite( $file, 'book info goes here' );
fclose( $file );
// inform the browser of success
return response()->json( true );
}
}
Obviously, I could just die(), but that's super ugly. I would prefer to return my error message in the same format as the validation errors. Like this:
{
"myErrorKey": [
"A filesystem error occurred on the server. Please contact your administrator."
]
}
I could construct my own JSON object and return that—but surely Laravel supports this natively.
What's the best / cleanest way to do this? Or is there a better way to return runtime (as opposed to validate-time) errors from a Laravel REST API?
You can set the status code in your json response as below:
return Response::json(['error' => 'Error msg'], 404); // Status code here
Or just by using the helper function:
return response()->json(['error' => 'Error msg'], 404); // Status code here
You can do it in many ways.
First, you can use the simple response()->json() by providing a status code:
return response()->json( /** response **/, 401 );
Or, in a more complexe way to ensure that every error is a json response, you can set up an exception handler to catch a special exception and return json.
Open App\Exceptions\Handler and do the following:
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* #var array
*/
protected $dontReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
NotFoundHttpException::class,
// Don't report MyCustomException, it's only for returning son errors.
MyCustomException::class
];
public function render($request, Exception $e)
{
// This is a generic response. You can the check the logs for the exceptions
$code = 500;
$data = [
"error" => "We couldn't hadle this request. Please contact support."
];
if($e instanceof MyCustomException) {
$code = $e->getStatusCode();
$data = $e->getData();
}
return response()->json($data, $code);
}
}
This will return a json for any exception thrown in the application.
Now, we create MyCustomException, for example in app/Exceptions:
class MyCustomException extends Exception {
protected $data;
protected $code;
public static function error($data, $code = 500)
{
$e = new self;
$e->setData($data);
$e->setStatusCode($code);
throw $e;
}
public function setStatusCode($code)
{
$this->code = $code;
}
public function setData($data)
{
$this->data = $data;
}
public function getStatusCode()
{
return $this->code;
}
public function getData()
{
return $this->data;
}
}
We can now just use MyCustomException or any exception extending MyCustomException to return a json error.
public function store( BookStoreRequest $request ) {
$file = fopen( '/path/to/some/file.txt', 'a' );
// test to make sure we got a good file handle
if ( false === $file ) {
MyCustomException::error(['error' => 'could not open the file, check permissions.'], 403);
}
fwrite( $file, 'book info goes here' );
fclose( $file );
// inform the browser of success
return response()->json( true );
}
Now, not only exceptions thrown via MyCustomException will return a json error, but any other exception thrown in general.
A simple approach is to use the abort() method in the controller. This will return an error that will be picked up by ajax error:function(){}
Controller Example
public function boost_reputation(Request $request){
$page_owner = User::where('id', $request->page_owner_id)->first();
// user needs to login to boost reputation
if(!Auth::user()){
toast('Sorry, you need to login first.','info');
abort();
}
// page owner cannot boost his own reputation
if(Auth::user() == $page_owner){
toast("Sorry, you can't boost your own reputation.",'info');
abort();
}
}
Ajax Example
$('.reputation-btn').on('click',function(event){
var btn = this;
var route = "{{ route('boost_reputation') }}";
var csrf_token = '{{ csrf_token() }}';
var id = '{{ $page_owner->id }}';
$.ajax({
method: 'POST',
url: route,
data: {page_owner_id:id, _token:csrf_token},
success:function(data) {
...your success code
},
error: function () {
...your error code
}
});
});
More info: https://laravel.com/docs/7.x/errors
In laravel 5, we can now use the Request classes for input validation like so :
public function store(StoreItemRequest $request)
{
$item = Item::create($request->all());
return 'success';
}
When the validation fails, I can get the errors thanks to the response function in the Request class :
public function response(array $errors) {
return response()->json(['errors' => $errors]);
}
But what if the validation succeeds? Is there a function that would get automatically triggered like so :
public function ????(){
if($request->ajax())
return response()->json(['success' => true]);
}
Note: it is required that the content of the function store does NOT get executed if the request is ajax (just as it would not get executed if the validation fails).
Yeah, I found validate method in ValidateWhenResolvedTrait which you could override in your form request class;
public function validate(){
$instance = $this->getValidatorInstance();
if($this->passesAuthorization()){
$this->failedAuthorization();
}elseif(!$instance->passes()){
$this->failedValidation($instance);
}elseif( $instance->passes()){
if($this->ajax())
throw new HttpResponseException(response()->json(['success' => true]));
}
}
Currently there is no such method. You can do this in your request with some "hacks" (like override validate method) but I do not recommend it.
Idea of Request was to move validation/authorization out of controller. If you move successful response to Request than we have the same situation as before (well, name changed).
I would suggest to let controller handle ajax too:
public function store(StoreItemRequest $request)
{
$item = Item::create($request->all());
if($request->ajax())
return response()->json(['success' => true]);
return 'success';
}
Is there a reason why you are trying to achieve this in a request?