My goal:
We ar developing an API and we need to customize error message not only to send custom string, but also to send cusotm code.
E.g.: custom invalid email error message should look like this:
error[
'code' => 102,
'message' => 'invalid email'
]
I could set these custom error messages, to be arrays, but I have a problem with emails.
I get:
"Array to string conversion" at Illuminate\Support\MessageBag at line 248.
The reason of it, is because it is expecting a string and now I have an array.
protected function transform($messages, $format, $messageKey)
{
return collect((array) $messages)
->map(function ($message) use ($format, $messageKey) {
// We will simply spin through the given messages and transform each one
// replacing the :message place holder with the real message allowing
// the messages to be easily formatted to each developer's desires.
return str_replace([':message', ':key'], [$message, $messageKey], $format);
})->all();
}
I would like to override (bind) this method with:
protected function transform($messages, $format, $messageKey)
{
return collect((array) $messages)
->map(function ($message) use ($format, $messageKey) {
if(is_array($message)){
$message = json_encode($message);
}
// We will simply spin through the given messages and transform each one
// replacing the :message place holder with the real message allowing
// the messages to be easily formatted to each developer's desires.
return str_replace([':message', ':key'], [$message, $messageKey], $format);
})->all();
}
I have daone the following steps.
I have created Libraries/Extensions/MessagesBag folder and plced the followint files there.
MessageBagServiceProvider.php
namespace App\Libraries\Extensions\MessageBag;
use Illuminate\Support\ServiceProvider;
class MessageBagServiceProvider extends ServiceProvider{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app->bind('Illuminate\Support\MessageBag', 'App\Libraries\Extensions\MessageBag\YcoMessageBag');
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return array('messagebag');
}
}
MessageBagFacade.php
namespace App\Libraries\Extensions\MessageBag;
use Illuminate\Support\Facades\Facade as IlluminateFacade;
class MessageBagFacade extends IlluminateFacade {
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor() { return 'messagebag'; }
}
YcoMessagebag.php
namespace App\Libraries\Extensions\MessageBag;
use Illuminate\Support\MessageBag as OriginalMessageBag;
class YcoMessageBag extends OriginalMessageBag{
/**
* Format an array of messages.
*
* #param array $messages
* #param string $format
* #param string $messageKey
* #return array
*/
protected function transform($messages, $format, $messageKey)
{
return collect((array) $messages)
->map(function ($message) use ($format, $messageKey) {
if(is_array($message)){
$message = json_encode($message);
}
// We will simply spin through the given messages and transform each one
// replacing the :message place holder with the real message allowing
// the messages to be easily formatted to each developer's desires.
return str_replace([':message', ':key'], [$message, $messageKey], $format);
})->all();
}
}
I have registered my MessageBagServiceprodider.php in config/app.php
App\Libraries\Extensions\MessageBag\MessageBagServiceProvider::class,
When I have died and dumped in MessageBagServiceProvider's register method, it worked, the code died and dumped "hello".
But the MessageBag class is not overriding, still the original class is loaded.
I have tried to play with
$this->app->bind('Illuminate\Support\MessageBag', 'App\Libraries\Extensions\MessageBag\YcoMessageBag');
I have tried with: $this->app->singleton,
I have tried to reach the original class with \Illuminate\Support\MessageBag also tried with '\App\Libraries\Extensions\MessageBag\YcoMessageBag', but no success.
Can I override this class? What can be the solution?
Thank you!
I have figured it out, here is my solution:
I have created Exteptions/Handler.php
class Handler extends ExceptionHandler
{
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
*
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($exception, $request);
} elseif ($exception instanceof ModelNotFoundException) {
$modelName = strtolower(class_basename($exception->getModel()));
return $this->errorResponse(
'Does not exists any ' . $modelName . ' with this id',
Response::HTTP_NOT_FOUND
);
} elseif ($exception instanceof AuthenticationException) {
return $this->unauthenticated($request, $exception);
} elseif ($exception instanceof AuthorizationException) {
return $this->errorResponse($exception->getMessage(), Response::HTTP_FORBIDDEN);
} elseif ($exception instanceof MethodNotAllowedHttpException) {
return $this->errorResponse('The specified request is invalid!', Response::HTTP_METHOD_NOT_ALLOWED);
} elseif ($exception instanceof NotFoundHttpException) {
return $this->errorResponse('The specified url cannot be found!', Response::HTTP_NOT_FOUND);
} elseif ($exception instanceof HttpException) {
$message = $exception->getMessage();
$status = $exception->getStatusCode();
$httpStatusCodes = collect(Response::$statusTexts);
if (!$httpStatusCodes->has($status)) {
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if ($message == "") {
$message = "An error occured when processing request!";
}
return $this->errorResponse($message, $status);
}
return parent::render($request, $exception);
}
/**
* Convert an authentication exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
*
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
return $this->errorResponse('Unauthenticated', Response::HTTP_UNAUTHORIZED);
}
/**
* Create a response object from the given validation exception.
*
* #param \Illuminate\Validation\ValidationException $e
* #param \Illuminate\Http\Request $request
*
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
$errors = $e->validator->errors()->getMessages();
$errors = $this->convertValidationErrors($errors);
return response()->json([
'success' => false,
'error' => $errors,
'status' => 422,
'message' => $e->getMessage()
], 422);
}
protected function convertValidationErrors($errors){
$codes = [
101 => [
'code'=> 101,
'message'=> 'A mező kitöltése kötelező'
],
106=>[
'code'=> 106,
'message'=> 'A mező tartalma nem elég hosszú'
],
102 =>[
'code'=> 102,
'message'=> 'A mező tartalma nem elég hosszú'
],
103 => [
'code'=> 103,
'message'=> 'A mező tartalma túl hosszú'
],
105 => [
'code'=> 105,
'message'=> ' A mező tartalma nem szöveg'
],
107 => [
'code'=> 107,
'message'=> 'Formátum nem megfelelő'
],
104 => [
'code'=> 104,
'message'=> 'Az email formátuma nem megfelelő'
],
];
foreach($errors as $key => $code){
if(isset($codes[$code[0]])) {
unset($errors[$key][0]);
$errors[$key]['code'] = $codes[$code[0]]['code'];
if(env('APP_ENV') != 'local') {
$errors[$key]['message'] = $codes[$code[0]]['message'];
}
}
}
return $errors;
}
}
Maybe not the most beautiful solution, but it works.
Related
I am refactoring my application according to this article:
https://laravel-news.com/controller-refactor
I had all logic in my controllers so it seems like a good idea to do this. But now I have some struggles with the update function.
class CategoryController extends Controller
{
/**
* Display a listing of the resource.
*
* #param Request $request
* #return JsonResponse
*/
public function index(Request $request): JsonResponse
{
$categories = Category::where('created_by', $request->company->id)->orderBy('order')->get();
return response()->json($categories);
}
/**
* Store a newly created category
*
* #param StoreCategoryRequest $request
* #param CategoryService $categoryService
* #return JsonResponse
*/
public function create(StoreCategoryRequest $request, CategoryService $categoryService): JsonResponse
{
$category = $categoryService->createCategory($request);
if ($category) {
return response()->json(['success' => true, 'message' => 'api.category.save.success']);
}
return response()->json(['success' => false, 'message' => 'api.category.save.failed']);
}
/**
* Update the specified resource in storage.
*
* #param StoreCategoryRequest $request
* #param Category $category
* #param CategoryService $categoryService
* #return JsonResponse
*/
public function update(StoreCategoryRequest $request, Category $category, CategoryService $categoryService): JsonResponse
{
try {
$result = $categoryService->updateCategory($request, $category);
if ($result) {
return response()->json(['success' => true, 'message' => 'api.category.update.success']);
}
return response()->json(['success' => false, 'message' => 'api.category.update.failed']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'api.category.update.failed']);
}
}
}
And the route:
Route::put('category/{category}', [CategoryController::class, 'update']);
Laravel is getting the category based on the id, but I don't know how to handle this correctly in my controller. I autoload the CategoryService there, so that I can use the update function. After that I give the actual category as a property to that service, I also don't know if handling the exceptions like this is the 'best way'.
class CategoryService
{
public function createCategory(Request $request): bool {
$category = new Category();
$category->fill($request->all());
$category->created_by = $request->company->id;
return $category->save();
}
/**
* #throws \Exception
*/
public function updateCategory(Request $request, Category $category): bool {
if($this->isOwnerOfCategory($category, $request->company)) {
$category->fill($request->all());
$category->created_by = $request->company->id;
return $category->save();
}
throw new \Exception('Not the owner of the category');
}
private function isOwnerOfCategory(Category $category, Company $company): bool
{
return $category->created_by === $company->id;
}
}
The create function/ flow feels good. But the update function feels like properties are coming from everywhere and the code is a lot less readable. Are there any suggestions to improve this?
Usually Laravel's form request returns a generic 400 code on failing validation like this:
{
"message": "The given data is invalid",
"errors": {
"person_id": [
"A person with ID c6b853ec-b53e-4c35-b633-3b1c2f27869c does not exist"
]
}
}
I'd like to return a 404 if a request does not pass my custom rule.
My custom rule checks if a record exists in the DB:
class ValidatePersonExists implements Rule
{
/**
* Determine if the validation rule passes.
*
* #param string $attribute
* #param mixed $value
* #return bool
public function passes($attribute, $value)
{
return Person::where('id', $value)->exists();
}
/**
* Get the validation error message.
*
* #return string
*/
public function message()
{
return "A person with ID :input does not exist";
}
}
If I throw a ModelNotFoundException on failure of the exists() check, where can I catch it to respond with a friendly 404 response?
Here's my form request where I am using the rule:
public function rules()
{
return [
'person_id' => ['bail', 'required', 'uuid', new ValidatePersonExists],
];
}
you can modify app/Exceptions/Handler.php, here is how i used it
use Illuminate\Database\Eloquent\ModelNotFoundException;
public function render($request, Exception $e)
{
$status = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500;
//here im using translation, you can set your own message
$response = [
'errors' => trans("response.$status.response"),
'message' => trans("response.$status.message")
];
if (config('app.debug')) {
$response['exception'] = get_class($e);
$response['message'] = $e->getMessage();
$response['trace'] = $e->getTrace();
}
//ModelNotFoundException statusCode is 0 so i need to pass it manually
if($e instanceof ModelNotFoundException){
$response['errors'] = new \Illuminate\Support\ViewErrorBag;
$response['response'] = 404; // im using translation here
return response()->view("errors.index", $response, 404);
}
return parent::render($request, $e);
}
in my blade a simple translation message:
<p>{{trans("response.{$response}.response")}}</p>
<p>{{trans("response.{$response}.message")}}</p>
I've found a solution, but I'm not 100% happy with it.
Basically I've created a class (ApiRequest.php) to extend Illuminate's FormRequest class, and in this ApiRequest class I'm intercepting the IlluminateValidationException and doing some logic on the failed validation, checking if the Request failed on my exists() rule. If it does, I'm changing the status code to 404:
Here's my class:
abstract class ApiRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
* #return void
*
* #throws \GetCandy\Api\Exceptions\ValidationException
*/
protected function failedValidation(Validator $validator)
{
$failedRules = $validator->failed();
$statusCode = $this->getStatusCode($failedRules);
$response = new JsonResponse([
'message' => 'The given data is invalid',
'errors' => $validator->errors(),
], $statusCode);
throw new IlluminateValidationException($validator, $response);
}
private function getStatusCode($failedRules)
{
$statusCode = 400;
foreach ($failedRules as $rule) {
if (Arr::has($rule, "App\Http\Requests\Rules\ValidatePersonExists")) {
$statusCode = 404;
}
}
return $statusCode;
}
}
Hope this helps someone, if anyone has a better solution feel free to post an answer.
im trying to return error message according to the exceptions types:
i have this code in my App\Exception\Handler.php:
<?php
namespace App\Exceptions;
use Exception;
use App\Traits\ApiResponser;
use Asm89\Stack\CorsService;
use Illuminate\Database\QueryException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
class Handler extends ExceptionHandler
{
use ApiResponser;
/**
* A list of the exception types that should not be reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* #param \Exception $exception
* #return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
$response = $this->handleException($request, $exception);
app(CorsService::class)->addActualRequestHeaders($response, $request);
return $response;
}
public function handleException($request, Exception $exception)
{
if ($exception instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($exception, $request);
}
if ($exception instanceof ModelNotFoundException) {
$modelo = strtolower(class_basename($exception->getModel()));
return $this->errorResponse("No existe ninguna instancia de {$modelo} con el id especificado", 404);
}
if ($exception instanceof AuthenticationException) {
return $this->unauthenticated($request, $exception);
}
if ($exception instanceof AuthorizationException) {
return $this->errorResponse('No posee permisos para ejecutar esta acción', 403);
}
if ($exception instanceof NotFoundHttpException) {
return $this->errorResponse('No se encontró la URL especificada', 404);
}
if ($exception instanceof MethodNotAllowedHttpException) {
return $this->errorResponse('El método especificado en la petición no es válido', 405);
}
if ($exception instanceof HttpException) {
return $this->errorResponse($exception->getMessage(), $exception->getStatusCode());
}
if ($exception instanceof QueryException) {
$codigo = $exception->errorInfo[1];
if ($codigo == 1451) {
return $this->errorResponse('No se puede eliminar de forma permamente el recurso porque está relacionado con algún otro.', 409);
}
}
if ($exception instanceof TokenMismatchException) {
return redirect()->back()->withInput($request->input());
}
if (config('app.debug')) {
return parent::render($request, $exception);
}
return $this->errorResponse('Falla inesperada. Intente luego', 500);
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($this->isFrontend($request)) {
return redirect()->guest('login');
}
return $this->errorResponse('No autenticado.', 401);
}
/**
* Create a response object from the given validation exception.
*
* #param \Illuminate\Validation\ValidationException $e
* #param \Illuminate\Http\Request $request
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
$errors = $e->validator->errors()->getMessages();
if ($this->isFrontend($request)) {
return $request->ajax() ? response()->json($errors, 422) : redirect()
->back()
->withInput($request->input())
->withErrors($errors);
}
return $this->errorResponse($errors, 422);
}
private function isFrontend($request)
{
return $request->acceptsHtml() && collect($request->route()->middleware())->contains('web');
}
}
i have this code in my file App\Traits\ApiResponser:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
trait ApiResponser
{
private function successResponse($data, $code)
{
return response()->json($data, $code);
}
protected function errorResponse($message, $code)
{
return response()->json(['error' => $message, 'code' => $code], $code);
}
protected function showAll(Collection $collection, $code = 200)
{
return $this->successResponse(['data' => $collection], $code);
}
protected function showOne(Model $instance, $code = 200)
{
return $this->successResponse(['data' => $instance], $code);
}
}
i have this in postman when i try to return a user that doesnt exist:
but i should get this error message in json:
"No existe ninguna instancia de usuario con el id especificado"
How can I add the status flag to Laravel (6.0) validation response?
this is my validation class.
class LoginRequest extends FormRequest{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => 'required|email|exists:users,email',
'password' => 'required|min:4|max:8'
];
}
}
according to the above validation following response is a return
{
"message": "The given data was invalid.",
"errors": {
"email": [
"The selected email is invalid."
]
}
}
but I need reformat above response like this.
{
"status": "fail",
"message": "The given data was invalid.",
"errors": {
"email": [
"The selected email is invalid."
]
}
}
All you need, create your response method and override failedValidation. Clear?
Update
In LoginRequest
protected function failedValidation(Validator $validator)
{
$this->currentValidator = $validator;
throw new ValidationException($validator, $this->errorResponse(
$this->formatErrors($validator)
));
}
protected function errorResponse(array $errors)
{
//something else
return response($errors);
}
Please look up Illuminate\Validation\ValidationException
Create a custom exception.
php artisan make:exception MyCustomException
Then, override the failedValidation method from FormRequest class
use Illuminate\Contracts\Validation\Validator;
use App\Exceptions\MyCustomException;
class LoginRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => 'required|email|exists:users,email',
'password' => 'required|min:4|max:8'
];
}
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
*
* #return void
*
* #throws \App\Exceptions\MyCustomException
*/
protected function failedValidation(Validator $validator)
{
throw (new MyCustomException($validator));
}
}
and handle the response in App\Exceptions\MyCustomException class
<?php
namespace App\Exceptions;
use Exception;
use Symfony\Component\HttpFoundation\Response;
class MyCustomException extends Exception
{
/**
* The validator instance.
*
* #var \Illuminate\Contracts\Validation\Validator
*/
public $validator;
/**
* Create a new exception instance.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
* #return void
*/
public function __construct($validator)
{
parent::__construct('The given data was invalid.');
$this->validator = $validator;
}
/**
* Get all of the validation error messages.
*
* #return array
*/
public function errors()
{
return $this->validator->errors()->messages();
}
/**
* Report the exception.
*
* #return void
*/
public function report()
{
//
}
/**
* Render the exception into an HTTP response.
*
* #param \Illuminate\Http\Request
* #return \Illuminate\Http\Response
*/
public function render($request)
{
if ($request->acceptsJson()) {
$errors = [
'status' => false,
'message' => 'The given data was invalid',
'errors' => $this->errors(),
];
return response()->json($errors, Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
}
I want to respond with a custom message when authorization fails.
I've overwritten the method in the Policy class but it does not return the custom message.
Policy:
class PostPolicy
{
use HandlesAuthorization;
/**
* Determine if user can view post
* #param User $user
* #param Post $post
* #return bool
*/
public function view(User $user, Post $post)
{
return $user
->posts()
->where('post_id', $post->id)
->exists();
}
/**
* [deny description]
* #return [type] [description]
*/
protected function deny()
{
return response()->json([
'message' => 'My custom unauthorized message'
], 401);
}
}
Implementing in PostController:
...
public function show(Post $post)
{
$this->authorize('view', $post);
...
}
The response still returns whats defined in the HandlesAuthorization trait, i.e.:
protected function deny($message = 'This action is unauthorized.')
{
throw new AuthorizationException($message);
}
You can simply add this code inside the AuthorizationException.php
/**
* Render the exception into an HTTP response.
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function render(Request $request)
{
if ($request->is('api/*')) {
$response = [
'message' => $this->message,
'status' => 403,
];
return response()->json($response, 403);
}
}