Laravel Custom Wrapper for API Responses - php

I am trying to structure my project in Laravel just created to use it as a back-end API. I want all my responses from Laravel to be returned in the JSON:API format defined on the official site: https://jsonapi.org/format/
For example:
I have created the following 2 resource files:
1- php artisan make:resource User
2- php artisan make:resource UserCollection --collection
Two simple resource files, one to return a resource and one to return a collection.
Now, I would like to return in all my responses the following format:
In case of success
1- The status return code can be 200, 201 or 202.
2- The returned response should be similar to the following:
{
"data": [
{
"id": 1,
"email": "collins.elody#example.org"
}
],
"message": null,
"success": true
}
You may be wondering what is the point of passing the message key, in this case it is null because it would be returning a collection of records, that is, reading, but in case you needed to add a new record, update or delete one, you would need to pass a message to my front-end, in this case I would use that key for that.
Example, adding record, response status code 201:
{
"data": [],
"message": "Record created succesfully",
"success": true
}
In case of failure
As said here: https://jsonapi.org/format/#document-top-level : The members data and errors MUST NOT coexist in the same document.
So, in case of error, I need change data key by errors key, for example, suppose I am trying to authenticate myself, and the validation fails, in this case, it should turn out like this:
{
"errors": {
"email": "E-Mail is required",
"password": "Password is required"
},
"message": null,
"success": false
}
or I just want to return an error message, expected output should by:
{
"errors": [],
"message": "Something is Wrong!",
"success": false
}
So in essence what I need is a global wrapper for all the responses I make from Laravel. I would like to call return in an elegant way as follows:
return $this->success($collection);
or
return $this->success('Done!', 201);
So the first thing that came to mind was creating a trait and defining the methods you need to then call them from anywhere in Laravel
My Trait
<?php
namespace App\Traits;
trait APIResponse
{
public function success($data, $status = 200) {
return [
'data' => $data,
'success' => in_array($status, [200, 201, 202]) ? true : false,
'message' => null
];
}
public function failure($data, $status = 500) {
// TODO
}
}
My Controller
class ExampleController extends Controller
{
public function index() {
$collection = new UserCollection(User::all());
return $this->success($collection);
}
}
But I am not sure it is the right way to do it, please, someone skilled in the field who can help me. Thank you very much in advance.

You are on the right path, there is two main solutions i would consider best approaches, to handling your exact problem. Fractal and Eloquent Resources, i prefer Fractal due to some design decisions and experience.
I will show an example in fractal, using the wrapper by Spatie. Firstly create an serializer that will wrap the data as expected.
class YourCustomSerializer extends SerializerAbstract
{
public function collection($resourceKey, array $data)
{
return [
$resourceKey ?: 'data' => $data,
'message': null,
'success': true,
];
}
public function item($resourceKey, array $data)
{
return [
$resourceKey ?: 'data' => $data,
'message': null,
'success': true,
];
}
This should be added to your fractal.php config, there is published through the spatie wrapper.
Transforming your data you need a transformer.
class UserTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email,
];
}
}
Now you can transform your data into the expected format.
public function response($data, int $statusCode = Response::HTTP_OK)
{
return fractal($data, $this->transformer)->respond($statusCode);
}
For error codes, you should go to the Handler.php and add something similar to this. This is very naive way of doing it, but for know should get you going on error handling, you need to do something with validation exception, status code etc.
if ($request->wantsJson()) {
return response()->json([
'success' => false,
'message' => $exception->getMessage(),
], Response::HTTP_BAD_REQUEST);
}

Related

Laravel: make validation rule fail when using closure

Say I have a custom logic for validating the uniqueness of a FormRequest field, something requiring to find another resource in the database like below:
class CreateMyResourceRequest extends FormRequest {
public function rules() {
return [
'my_field' => [
Rule::unique('some_other_resource', 'some_column')
->where(function ($query) {
$otherResource = SomeOtherResource::where(...)->firstOrFail();
// Process the retrieved resource
}),
]
The firstOrFail() call obviously makes the request fail with a 404 - Not found while I would like to return a 422 with a validation error on the field.
Is there a way to achieve this while still using the Rule::unique() provided by the framework?
Thanks in advance!
I'd suggest the following
public function rules()
{
return [
"your_field" => ["you_can_have_more_validations_here", function($key, $value, $cb) {
$queryResult = SomeModel::find(1);
if (someCondition) {
$cb("your fail message");
}
}]
];
}
when the $cb run the validation will fail with 422
Don't use firstOrFail, instead just use first and check if the output is null. If it is, return false.
Alternatively, the more Laravel way of doing it is:
$query->leftJoin('otherresource', 'primarykey', 'foreignkey')->where(...)

Can I find unepexcted issues by creating dynamic properties on an object (via $this) on a callback function?

I have a question regarding the use of $this inside a Laravel routing.
Recently I'm trying to find a way to pass PHP variables between Route::group() and Route::get/put/post/anything-that-fits-inside-the-group(). So I tried to Google my way out and found most of the results always related to passing variable between routes and view (which is not the case). This result were closest to -- if not exactly -- what I'm trying to achieve, stating that variable passing can't be done in such way.
During my curiosity over this topic, I fiddled around with the routes, trying to pass the PHP variables between routes in a few ways until I found a solution to this case: by using $this
Here's a bit of my code just to give you some context:
...
Route::prefix("api")->middleware("verify_api_call")->group( function(){
[$this->success, $this->message, $this->data] = [false, "", null];
$this->ctx = [
"success" => &$this->success,
"message" => &$this->message,
"data" => &$this->data
];
Route::get("test", function(){
$this->success = true;
$this->message = "ok";
$this->data = [
"somekey" => "somevalue"
];
return response()->json(["Contextual" => $this->ctx, "All of this" => $this]);
});
...
Calling mysite.com/api/test gives the expected result as below:
{
"Contextual": {
"success": true,
"message": "ok",
"data": {
"somekey": "somevalue"
}
},
"All of this": {
"success": true,
"message": "ok",
"data": {
"somekey": "somevalue"
},
"ctx": {
"success": true,
"message": "ok",
"data": {
"somekey": "somevalue"
}
}
}
}
Now the question might be no longer about how it works, but rather: will there be any unexpected problems that I'm unaware of if I keep using $this this way?
TL;DR; Yes, you could cause undefined behaviour in future.
You're basically storing your custom variables on an instance of the class:
Illuminate\Routing\RouteFileRegistrar
And currently, said Class has only $router field, and each Group creates a new instance of said Class for itself.
But in future Laravel versions, said Class may have additional fields, which you may override by accident, or it could entirely change to another Class.
So, for now you are good to go, but consider using function () use($ctx) {} syntax instead, like:
Route::prefix("api")->middleware("verify_api_call")->group( function(){
[$success, $message, $data] = [false, "", null];
$ctx = [
"success" => & $success,
"message" => & $message,
"data" => & $data
];
Route::get("test", function() use($ctx) {
$ctx->success = true;
$ctx->message = "ok";
$ctx->data = [
"somekey" => "somevalue"
];
return response()->json(["Contextual" => $ctx]);
});
});
You are creating public dynamic properties on an object of class you don't control.
Two immediate problems:
The API of the class could change in the future, and your chosen properties may clash with whatever the class creator chooses.
Dynamic properties will be deprecated by PHP 8.2, and removed by PHP 9.0. So unless the class maintainer adds code to allow for developers to add random dynamic properties, your code will fail by then.
It's a bad idea and poor design.

response()->json() returns with "resource" wrap - Laravel 5.6.*

I have a problem regarding the response JSON of my API. I used a resource, since I wanted to limit what data to send back to the client. It was properly giving my intended response before, but when I opened my project again, the response changed.
Here's are parts of my code:
api.php
Route::get('admin/adminuserdetails/{adminUser}', 'AdminsController#AdminUserDetails');
Sample URL:
http://localhost:8000/api/admin/adminuserdetails/1
Controller
public function AdminUserDetails(AdminUsers $adminUser){
return response()->json(new AdminUserAccessDetails($adminUser), 200);
}
AdminUsers Model
class AdminUsers extends Model
{
//
protected $table = 'AdminUsers';
protected $primaryKey = 'AdminUserId';
protected $guarded = [];
}
AdminUserAccessDetails Resource
class AdminUserAccessDetails extends JsonResource
{
public function toArray($request)
{
//return parent::toArray($request);
return [
'adminUserId' => $this->AdminUserId,
'adminFirstName' => $this->AdminFirstName,
'adminLastName' => $this->AdminLastName,
'modulesAllowed' => $this->ModulesAllowed,
'actionsAllowed' => $this->ActionsAllowed
];
}
}
Sample response (before, my intended response)
{
"adminUserId": 1,
"adminFirstName": "asdfasdf",
"adminLastName": "asdfsadf",
"modulesAllowed": "",
"actionsAllowed": ""
}
Sample response (now)
{
{
"resource": {
"adminUserId": 1,
"adminFirstName": "asdfasdf",
"adminLastName": "asdfsadf",
"adminEmail": "asdfsadf#fsafsa.com",
"adminPassword": "l6wfDtAaYAp6aM04TU++9A==",
"authToken": "68bbc9fc7eb08c9f6d96f6b63d30f056",
"fCMToken": null,
"profileImage": "https://www.gravatar.com/avatar/5d0d65256e8c2b15a8d00e8b208565f1?d=identicon&s=512",
"userTypeId": "0",
"status": "A",
"createDate": "2018-06-26 16:01:43.947",
"updateDate": "2018-06-26 16:01:44.143",
"modulesAllowed": "",
"actionsAllowed": ""
},
"with": [],
"additional": []
}
I didn't change anything, but when I tested it again (not only in this particular route), everything that uses any resource is now enclosed within that resource wrap, and I can't seem to find the reason why.
I tried implementing the same logic with another clean project, and it's working perfectly.
What's causing this and how do I get my intended response back?
Edit 1:
I tried to change my return, I removed the "response()->json()" code so my controller would look like:
public function AdminUserDetails(AdminUsers $adminUser){
//return response()->json(new AdminUserAccessDetails($adminUser), 200);
return new AdminUserAccessDetails($adminUser);
}
The response of this edit is now a bit closer to my intended output:
{
"data": {
"adminUserId": 1,
"adminFirstName": "asdfasdf",
"adminLastName": "asdfsadf",
"modulesAllowed": "",
"actionsAllowed": ""
}
}
However I still prefer using the response()->json() so that I can return a proper HTTP response code..
I think that the problem is you are sending a Laravel Object as API response. Remember, Laravel Core instance object add some properties/methods to manage/use easily inside your code.
I may recommend you create a new Class or Associative array to encapsulate specifics properties and later send it.
For Example:
public function AdminUserDetails(AdminUsers $adminUser){
// Example with Object
$Object = new YourClass(); // you can define with yours wished properties
$Object->PropertyA = $adminUser->Property1;
$Object->PropertyB = $adminUser->Property2;
// ...
return response()->json($Object, 200);
// Example with Associative Array
$AssociateArray = array(
"PropertyA" => $adminUser->Property1,
"PropertyB" => $adminUser->Property2,
// ...
);
return response()->json($AssociateArray, 200);
}
I hope be useful.

REST API in Laravel when validating the request

I'm currently trying out on how to build a RESTful API with Laravel and I'm currently in the process of creating a new user. This is just a test and I'm getting some result when trying to validate the request using validation in Laravel; here is the result:
I've been trying to create a new one by this code:
public function store()
{
$validation = Validator::make(Request::all(),[
'username' => 'required|unique:users, username',
'password' => 'required',
]);
if($validation->fails()){
} else{
$createUser = User::create([
'username' => Request::get('username'),
'password' => Hash::make(Request::get('password'))
]);
}
}
but then I don't know how to return the error in validation. But it keeps on giving me that HTML as showed in the image when I was trying to do the if with validation->fails(). Is there a way to get the validation in JSON format?
these code will help you, working for me.
$response = array('response' => '', 'success'=>false);
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
$response['response'] = $validator->messages();
} else {
//process the request
}
return $response;
You should probably return errors (which is an instance of Illuminate\Support\MessageBag) and encode that. A MessageBag instance allows you to convert it directly to its JSON representation.
$errors = $validation->errors();
return $errors->toJson();
Now not to toot my own horn but I've recently developed a RESTful API package for Laravel which does all of this for you and all you need to do is throw a simple exception. See my dingo/api package and the Wiki on returning errors. Basically, instead of returning the errors you would throw an exception.
throw new Dingo\Api\Exception\StoreResourceFailedException('Could not create a new user.', $validation->errors());
It would be represented by the following JSON.
{
"message": "Could not create a new user.",
"errors": {
"username": ["The username is already in use."]
}
}
Laravel provides out of the box a validation method that you can call from your Controller.
if you check the Laravel Controller abstract class you will find it uses a trait called ValidatesRequests
abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
So you can use a method $this->validate(Request $request, array $rules); as you long as your controller class extends the Controller
the full method declaration is
public function validate(Request $request, array $rules, array $messages = [], array $customAttributes = [])
{
$validator = $this->getValidationFactory()->make($request->all(), $rules, $messages, $customAttributes);
if ($validator->fails()) {
$this->formatValidationErrors($validator);
}
}
If The $validator fails, the method will throw an error depending on the request type, if it is ajax (in this case you should include in the request headers (Accept application/json) it will return a JSON response containing the validation errors.
For laravel 5.5 and up, see docs: AJAX Requests & Validation
TL;DR: On failed validation a json response with a 422 is returned along with the validation error messages. It took me a bit of time to find those validation errors in the response object, so to see the error messages if you're using axios, try this in your browser console:
axios.post('/api/your-route-here')
.then(response => {
console.log(response.data);
}).catch(error => {
console.log(error.response.data.errors)
});
There are many ways to get a validator response first is to get an all validation error at the same time i.e you will get a response like below
$validator = \Validator::make($request->all(), [
'username' => 'required|unique:users, username',
'password' => 'required',
]);
if ($validator->fails()) {
$responseArr = CustomHelper::returnRespArr("");
$responseArr['message'] = $validator->errors();;
$responseArr['token'] = '';
return response()->json($responseArr, Response::HTTP_BAD_REQUEST);
}
Response you will get is:
{
"status": false,
"data": [],
"message": {
"username": [
"The username field is required."
],
"password": [
"The password field is required."
]
},
"is_valid": 0,
"token": ""
}
The second way to get a validation response. In this, you will get a one validator error a time.
if ($validator->fails()) {
$responseArr = CustomHelper::returnRespArr("");
$responseArr['message'] = $validator->messages()->first();;
$responseArr['token'] = '';
return response()->json($responseArr,Response::HTTP_BAD_REQUEST);
}
The response you will get
{
"status": false,
"data": [],
"message": "The username field is required.",
"is_valid": 0,
"token": ""
}
I am using Laravel 9.x and found a quite simple way to validate errors with REST APIs:
public function store(Request $request)
{
$input = $request->all();
$validator = Validator::make($input, [
'title' => 'required|string|max:50'
]);
// Will return an error, if validation fails.
// https://laravel.com/api/9.x/Illuminate/Foundation/Validation/ValidatesRequests.html#method_validateWith
$this->validateWith($validator, $request);
// Only use the properties that were validated.
$input = $validator->validated();
// Create a new event model, with the data provided.
$event = Event::create($input);
return new EventResource($event);
}
In order to return a json error message, make sure to set the Accept header of the client to application/json. I make the mistake to not set this in my debug client, so I only saw html/xml messages.
You can also force the output to json.

Laravel nesting all json responses

I'm using Laravel to create a JSON REST API, and it has been quite present so far. However I'm needing to wrap my JSON outputs with a bit of meta status JSON created by say a metaDataController (or probably model) and I am curious what a good approach to this might be.
For instance, all responses would take on the following format:
{
"meta": {
"status": 200,
"notifications": 2
},
"response": {
//JSON from the route's Controller/Model/etc
}
}
From what I can tell I either need to modify the Laravel Response defaults and delegate to a metaDataController, or create some sort of Route::any that merges the two sections of JSON as mentioned in Returning Multiple Laravel Eloquent Models as JSON. Although I always know metaDataController, the other controller is in flux depending on the route.
I'm thinking there must be a way to declare this structure as a default for all routes or a Route::group.
Thanks!
I don't think doing json_decode->json_encode cycle is an acceptable solution (as in Chris answer).
Here is another solution
use Illuminate\Http\Response;
use Illuminate\Http\Request;
Route::filter('apisuccess', function($route, Request $request, Response $response = null) {
$response->setContent(json_encode([
'data' => $response->original,
'meta' => ['somedata': 'value']
]));
});
Then I would attach this filter to my REST API routes.
Edit: another solution (more complex).
Create a custom Response class:
use Illuminate\Http\Response;
class ApiResponse extends Response
{
protected $meta;
protected $data;
public function __construct($content = '', $status = 200, $headers = array())
{
parent::__construct([], $status, $headers);
$this->meta = [];
}
public function withMeta($property, $value = null)
{
if (is_array($property))
$this->meta = $property;
else
array_set($this->meta, $property, $value);
return $this;
}
public function withData($data)
{
$this->data = $data;
return $this;
}
public function sendContent()
{
echo json_encode(['success' => true, 'data' => $this->data, 'meta' => $this->meta, 'echo' => ob_get_contents()]);
}
}
Put it as a singleton in IOC container:
$this->app->bindShared('ApiResponse', function() {
return new \Truinject\Http\ApiResponse();
});
Finally, create filter and use it as "before" on your routes:
Route::filter('apiprepare', function(Illuminate\Routing\Route $route, Illuminate\Http\Request $request) {
$data = $route->run();
return App::make('ApiResponse')->withData($data);
});
So we are basically overriding default response class with our own, but still calling the appropriate controller with $route->run() to get the data.
To set meta data, in your controller do something like this:
\App::make('ApiResponse')->withMeta($property, $value);
I've added method "meta" in my base API controller class, which encapsulates this.
You could use the global after filter in app.php to catch all responses, then reconfigure it however you please:
App::after(function($request, $response)
{
if(is_a($response, 'Illuminate\Http\JsonResponse')) {
$response->setContent(json_encode(array(
'data' => json_decode($response->getContent()),
'foo' => 'bar',
'cat' => 'dog'
)));
}
});
In the above example, you're taking all the existing json data and putting it in a child data element (this would be "response" in your example) then adding foo and bar. So foo, bar and data would be top level json objects.
If you don't like the global positioning, after is an event sent, so you could also listen to it inside a controller/elsewhere.

Categories