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.
Related
I am making a REST API that will return different JSON responses depending on which type of User is making the call.
There is a single endpoint: example.com/api/v1/collect that uses Laravel's API authentication to get the User model with $user = auth()->guard('api')->user();.
Each User will belong to a Type.
If User 1 (type_id 1) makes the call, the response will look like:
{
"example": 1,
"success": true,
"data" : [
...
]
}
If User 2 (type_id 2) makes the call, the response can be different, depending on the user's type. It could look like:
{
"example": 2,
"response" : [
...
],
"custom": "string",
"success": 200
}
The ... is the data that we are sending back (for example a list of Post titles) and it will always be the same, but the "envelope" (or wrapper) around it would be specific to each user (or type of user).
So far, I've found two solutions to wrap that ... in an abstracted way:
Solution 1: Using Laravel Blade
// Api\V1\ApiController.php
$data = $user->posts->pluck('title');
// Each type of user will have a different blade filename
// There could be around a 100 types which will result in a 100 blade files
// The filename is stored in the database
$filename = $user->type->filename; // returns 'customBladeTemplate'
// Return a JSON response after passing the $data to the view
return response()->json([
view($filename, compact('data'))->render(),
]);
Using a blade file for each type of user allows me to wrap the data like this:
// resources/views/customBladeTemplate.blade.php
// This filename has to match the one in the database column
{
"example": 1,
"success": true,
"data" : [
{!! $data !!}
]
}
That will output a JSON response for the User 1 (example 1)
Solution 2: Using Laravel response macros
// Api\V1\ApiController.php
$data = $user->posts->pluck('title');
// Each type of user will have a different macro name
// There could be around a 100 types which will result in a 100 different macros
// The macro name is stored in the database
$macroName = $user->type->macro_name; // returns 'customMacroName'
return response()->{macroName}($data);
Creating a Macro for each type of user, using the macro name from the DB:
// App\Providers\AppServiceProvider.php
use Illuminate\Http\Response;
public function boot()
{
Response::macro('customMacroName', function ($data) {
return Response::json([
'example' => 2,
'response' => $data,
'custom' => 'string',
'success' => 200,
]);
});
}
That macro will output a JSON response for the User 2 (example 2)
Both options work fine but I am still wondering:
Is there another (possibly better) way to do it?
Are those two solutions valid or can they be enhanced?
Which of those two solutions seem to be better and why?
Edit: The $data is not actually coming from an eloquent model, it is rather from a serialized JSON column (JSON casting) - which means I can't use the Laravel API resources
If you are looking for the response formatting you should go with the Laravel API Resources
Based on your requirement(data formate different for two type of users), you can create two different Api Resource classes.
AdminResource & UserResource.
Here you have more flixibility on controlling fields or orgnizing data.
Here is how you can define the resource class:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class UserResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
And you can use this as:
use App\User;
use App\Http\Resources\UserResource;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
If you would like to include with condition check in with user type, you can create a common function called renderJson($userType, $data) and place this in your parent class or can wrap with traits, all depends on your application architecture.
Here you can find laravel documentation for API Resource: https://laravel.com/docs/5.8/eloquent-resources
Edited:
With Laravel API Resource, you not only parse the modal object, you can parse any arrayble object.
Essentially they are just simple objects with one very important job
to do — transform your objects (interesting I said objects and not
models). To do this out of the box, all you have to do is instantiate
the Resource (collection or individual) with an Arrayable object. If
you did nothing else but generate a standard Resource and pass in an
Arrayable object the Resource would transform that object
automatically, and because Models are Arrayable this is where I got
caught out because if you create a resource collection and instantiate
it with a collection of models then the models get toArray'd and not
their corresponding resource.
Src: https://medium.com/#dinotedesco/laravel-api-resources-what-if-you-want-to-manipulate-your-models-before-transformation-8982846ad22c
So in your case if you can just collect() the json data and pass to api resource.
You could use middlewares to change what the response looks like.
With middleware you could change the response after the execution of your regular code, without having to take this into account in the controller itself. Using the below code you modify the response AFTER it has been executed.
<?php
namespace App\Http\Middleware;
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
// Calls the controller and processes the request.
$response = $next($request);
// Here you can retrieve the user and modify the response however you want.
// Some example code:
$user = Auth::user();
if ($user->type == 1) {
... //Change response for user type 1
}
if ($user->type == 2) {
... //Change response for user type 2
}
// Etc...
return $response;
}
}
Reference: https://laravel.com/docs/5.8/middleware
Depends on how different the responses are from each other. I'm inclined to take inventory of each type's common features and build a response array as appropriate. This could be done in the controller or a helper function and then returned using Laravel's JSON response type.
$response = [];
// results common to all types
$response['example'] = $example;
$response['success'] = $success;
// customized results for specific types
if (in_array($type, [1, 3, 4, 5, ...])) {
$response['data'] = $dotdotdot;
}
if (in_array($type, [2, 6, 7, 8, ...])) {
$response['response'] = $dotdotdot;
$response['custom'] = $custom;
}
return response()->json($response);
I don't know if this is what you are looking for. I had something similiar a few months ago and fixed it with json files. As json is amazingly fast and you can create thousands of types.
Sorry for my bad english i will fix it after the weekend :-)
Let's get started.
First the user logs in using laravel passport or api routes.
Second the api calls a controller.(class). I will create a class based on your info.
let's say the api calls the ApiController and the method handle
use Illuminate\Http\Request;
class ApiController
{
public function __construct()
{
}
/**
* Handle the incoming request
*
* #param Request $request
*/
public function handle(Request $request)
{
//first let's find the correct format
$type = $requets->user()->type; //lets say type_1
$config = $this->getUserType($type);
//i don't know where you data comes from but let's say $data is your data.
$data = json_encode(['some' => "data", 'to' => "return"]);
//lets fill the data
$response = $this->fillDataInConfig($data, $config);
return response()->json($response);
}
/**
* Find the user type config by type name
*
* #param string $type
* #return object
*/
private function getUserType(string $type) : string
{
//let store them in the local storage
$config = \Storage::disk('local')->get("api_types/$type.json");
return json_decode($config);
}
/**
* Fill the data
*
* #param mixed $data
* #param object $config
* #return object
*/
private function fillDataInConfig($data, object $config) : object
{
//as for your example. The reusl//
// {
// "example": 2,
// "response" : *responseData*, <===== where the response should be
// "custom": "string",
// "success": 200
// }
foreach($config as $index => $value){
if($value === '*responseData*'){
$config->{$idnex} = $data;
}
}
//the data is filled in the response index
return $config;
}
}
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.
I am working on my yii2 api and I was looking for a way to get data from my controller actions. This is a sample of what I need on my response in json or xml.
{"success": true,
"message": {data},
"session": "U0phRm51az",
"metadata": "metadata"
}
I am getting message from the controller whereas success checks whether response is OK, session is the session data and metadata is other data.
My actions looks like these.
public function actionIndex(){
$data = array();
}
All these use the same functions so I do not want to repeat in all actions. I would like to know how to get $data from each action using afterAction or beforeSend event of the response component on my module class (not config file). If this is not possible, how can I achieve this?
If your actions return data as an array, you can add more stuff to that array in afterAction method of your controller.
public function actionIndex()
{
//...
//$data contains an array
return [
'data' => $data
];
}
public function afterAction($action, $result)
{
$result = parent::afterAction($action, $result);
$result['session'] = '...';
$result['metadata'] = '...';
return $result;
}
I have the following Json String:
var jsonString = '{"Users":[{"Name":"abc","Value":"test"},{"Name":"def","Value":"test"}]}';
I am trying to use ZF2's JsonModel class (Zend\View\Model\JsonModel) in the controller to render my view with the above JSON string. However, it seems to take only an array instead of a JSON String.
How do I make the controller return a JSON string?
Thanks
You don't need to use a JsonModel since your json is already "rendered", so, you can set it directly in response object and return it:
/**
* #return \Zend\Http\Response
*/
public function indexAction()
{
$json = '{"Users":[{"Name":"abc","Value":"test"},{"Name":"def","Value":"test"}]}';
$this->response->setContent($json);
return $this->response;
}
That will short-circuit the dispatch event, so the application will return your response immediately, without calling the view layer to render it.
See http://framework.zend.com/manual/2.2/en/modules/zend.mvc.examples.html#returning-early
You have to use the acceptableViewModelSelector controller plugin
public function listAction()
{
$acceptCriteria = array(
'Zend\View\Model\ViewModel' => array(
'text/html',
),
'Zend\View\Model\JsonModel' => array(
'application/json',
));
$viewModel = $this->acceptableViewModelSelector($acceptCriteria);
Json::$useBuiltinEncoderDecoder = true;
$itemsList = $this->getMyListOfItems();
return $viewModel->setVariables(array("items" => $itemsList));
}
The official doc : http://framework.zend.com/manual/2.2/en/modules/zend.mvc.plugins.html#zend-mvc-controller-plugins-acceptableviewmodelselector
Another bonus : explanation of why using this plugin
JsonStrategy security fix
Add in module.config.php:
'strategies' => [
'ViewJsonStrategy',
],
Then you can return in controller json response:
return new JsonModel(['some'=>'data']);
I'm just getting started with Symfony2 and I'm trying to figure out what the correct approach is for echoing out JSON from a controller (e.g., People) for use in an ExtJS 4 grid.
When I was doing everything using a vanilla MVC approach, my controller would have method called something like getList that would call the People model's getList method, take those results and do something like this:
<?php
class PeopleController extends controller {
public function getList() {
$model = new People();
$data = $model->getList();
echo json_encode(array(
'success' => true,
'root' => 'people',
'rows' => $data['rows'],
'count' => $data['count']
));
}
}
?>
What does this kind of behavior look like in Symfony2?
Is the controller the right place for this kind of behavior?
What are the best practices (within Symfony) for solving this kind of problem?
Is the controller the right place for this kind of behavior?
Yes.
What does this kind of behavior look like in Symfony2?
What are the best practices (within Symfony) for solving this kind of problem?
In symfony it looks pretty much alike, but there are couple of nuances.
I want to suggest my approach for this stuff. Let's start from routing:
# src/Scope/YourBundle/Resources/config/routing.yml
ScopeYourBundle_people_list:
pattern: /people
defaults: { _controller: ScopeYourBundle:People:list, _format: json }
The _format parameter is not required but you will see later why it's important.
Now let's take a look at controller
<?php
// src/Scope/YourBundle/Controller/PeopleController.php
namespace Overseer\MainBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class PeopleController extends Controller
{
public function listAction()
{
$request = $this->getRequest();
// if ajax only is going to be used uncomment next lines
//if (!$request->isXmlHttpRequest())
//throw $this->createNotFoundException('The page is not found');
$repository = $this->getDoctrine()
->getRepository('ScopeYourBundle:People');
// now you have to retrieve data from people repository.
// If the following code looks unfamiliar read http://symfony.com/doc/current/book/doctrine.html
$items = $repository->findAll();
// or you can use something more sophisticated:
$items = $repository->findPage($request->query->get('page'), $request->query->get('limit'));
// the line above would work provided you have created "findPage" function in your repository
// yes, here we are retrieving "_format" from routing. In our case it's json
$format = $request->getRequestFormat();
return $this->render('::base.'.$format.'.twig', array('data' => array(
'success' => true,
'rows' => $items,
// and so on
)));
}
// ...
}
Controller renders data in the format which is set in the routing config. In our case it's the json format.
Here is example of possible template:
{# app/Resourses/views/base.json.twig #}
{{ data | json_encode | raw }}
The advantage of this approach (I mean using _format) is that it if you decide to switch from json to, for example, xml than no problem - just replace _format in routing config and, of course, create corresponding template.
I would avoid using a template to render the data as the responsibility for escaping data etc is then in the template. Instead I use the inbuilt json_encode function in PHP much as you have suggested.
Set the route to the controller in the routing.yml as suggested in the previous answer:
ScopeYourBundle_people_list:
pattern: /people
defaults: { _controller: ScopeYourBundle:People:list, _format: json }
The only additional step is to force the encoding in the response.
<?php
class PeopleController extends controller {
public function listAction() {
$model = new People();
$data = $model->getList();
$data = array(
'success' => true,
'root' => 'people',
'rows' => $data['rows'],
'count' => $data['count']
);
$response = new \Symfony\Component\HttpFoundation\Response(json_encode($data));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
?>
To use return new JsonResponse(array('a' => 'value', 'b' => 'another-value'); you need to use the right namespace:
use Symfony\Component\HttpFoundation\JsonResponse;
As described here: http://symfony.com/doc/current/components/http_foundation/introduction.html#creating-a-json-response
Instead of building your own response you can also use the built-in JsonResponse.
You define the route like in the other answers suggested:
ScopeYourBundle_people_list:
pattern: /people
defaults: { _controller: ScopeYourBundle:People:list, _format: json }
And use the new response type:
<?php
class PeopleController extends controller {
public function listAction() {
$model = new People();
$data = $model->getList();
$data = array(
'success' => true,
'root' => 'people',
'rows' => $data['rows'],
'count' => $data['count']
);
return new \Symfony\Component\HttpFoundation\JsonResponse($data);
}
}
For more information see the api or the doc (version 2.6).
Simple. Use FOSRestBundle and only return the People object from the controller.
use
return JsonResponse($data, StatusCode, Headers);