How to add meta data in laravel api resource - php

I using laravel api resource for my FRONT-END site.
Actually i using also laravel pagination to handle paginate in FRONT-END.
The api resource structure like this:
{
"data": [...],
"links": [...], // handle pagination
"meta": [...] // handle pagination
}
But i want to send count of query results without considering paginating .For example i paginating database result into 30 pre_page and the all of query result is
200 and i want to send this count to view front-end
I decided to get count of query result and give to resource collection and restructure laravel api resource to this.
{
"data": [...],
"metadata": {
"count": 200
},
"links": [...], // handle pagination
"meta": [...] // handle pagination
}
Also you sould know maybe i want to add another data to this collection. for example conversion rate in addition count to metadata and i don't know how do this.

I find the way hou can i do this
Actually i just have to use additional method for laravel resource like:
$data = Project::limit(100)->get();
return ProjectResource::collection($data)->additional(['some_id => 1']);

See top-level-meta-data.
You need to create a ResourceCollection and then define the additional meta variables in the with function.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
/**
* Get additional data that should be returned with the resource array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function with($request)
{
return [
'meta' => [
'key' => 'value',
],
];
}
}

Related

RESTful API: Data from multiple endpoints in one single view

I've started creating a RESTful API (well, I did my best, I'm trying to follow the patterns) and I have stumbled upon a scenario that I'm not really sure how to handle. I will explain the current structure:
My application has 4 controllers:
Customers
Payments
Log
Taking as example the Customers controller, I have defined the following actions:
GET /customers: returns a list of customers
POST /customers: creates a new customer
GET /customers/{id}: returns the customer with the provided id
PUT /customers/{id}: updates the customer with the provided id
DELETE /customers/{id}: destroys the customer
This is the full code of the Customer controller:
namespace App\Http\Controllers;
use App\Customer;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
return Customer::all();
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$customer = Customer::create($request->all());
return response()->json($customer, 201);
}
/**
* Display the specified resource.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function show(Customer $customer)
{
return $customer;
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function update(Request $request, Customer $customer)
{
$customer->update($request->all());
return response()->json($customer, 200);
}
/**
* Remove the specified resource from storage.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function destroy(Customer $customer)
{
$customer->delete();
return response()->json(null, 204);
}
}
The code is very similar in the other controllers. It's also important to note that:
A Customer can have multiple Payments
A Customer can have multiple records in the Log
The problem starts here:
I need to display in the front-end a summary page with all customer data (name, email, registration date, etc) and a box showing the number of payments made and another box showing the number of entries in the Log.
Do I need to make 3 requests? (One to /customers/id, other to customers/id/payments and other to customers/id/logs)
If I return all the customer related data in the customers/id call, am I breaking the RESTful convention?
I am using apigility, but my answer still will be related to your question. According to the REST terminology (which could be find here https://apigility.org/documentation/intro/first-rest-service#terminology ) You are talking about entity and collection.
/customers/id - entity,
/customers/id/payments - collection,
/customers/id/logs - collection.
These are 3 different requests. So, yes, you need make 3 different requests.
But, to be honest, if you don't need pagination over payments and logs you can have only one request to /customers/id and within response you can have fields with array
{
"_links": {
"self": {
"href": "http://localhost:8080/status/3c10c391-f56c-4d04-a889-bd1bd8f746f0"
}
},
"id": "3c10c391-f56c-4d04-a889-bd1bd8f746f0",
...
_payments: [
...
],
_logs: [
...
],
}
Upd (duplicate from comment for future visitors).
Also, you should pay attention to DTO. I suppose this link will be interesting https://stackoverflow.com/a/36175349/1581741 .
Upd2.
At current moment I treat your collection /customers/id/payments like this:
/payments?user_id=123
where user_id is filtering field on payments table.
I think your problem that you confuse your REST API with your database. They don't have to follow the same structure. You can easily return the whole nested JSON for GET /customers/{id} if that's what you need from your REST API.

Laravel API Resource timestamp "Call to a member function format() on null"

I'm using Laravel's API Resource functionality to format my responses nicely for the client, but the trouble I'm having is with the code below;
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection->transform(function ($item)
{
return [
'id' => $item->id,
'title' => Str::limit($item->title, 32),
'body' => Str::limit($item->body, 32),
'created_at' => $item->created_at->format('d M Y, H:i a'),
'user' => $item->user
];
}),
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
When using this code, I get an error; "Call to a member function format() on null" on the created_at attribute.
But I've already used dd($this->collection) to confirm that none of the attributes are in fact null and I'm not really sure what could be causing it. My migration contains $table->timestamps();, and inside my factory, I'm not overriding the timestamps at all, so I'm not really sure what the issue is.
Here is the test I'm running below to get this error as well;
factory(News::class, 10)->create();
$user = factory(User::class)->create();
$this->actingAs($user)
->get('/news')
->assertOk()
->assertPropCount('news.data', 10)
->assertPropValue('news.data', function ($news)
{
$this->assertEquals(
[
'id', 'title', 'body', 'created_at',
'user',
],
array_keys($news[0])
);
});
The extra functions such as assertPropCount and assertPropValue are sourced from the InertiaJS demo app as I'm using InertiaJS in my project.
Hopefully, someone is able to help as I've asked around a few other places and no one seems to know what the reason for this is, and based on my debugging there doesn't really seem to be much of a valid explanation as to WHY created_at is null.
As a note, if I turn $item->user to $item->user->toArray() in the code as well, this then also fails complaining that user is null when it isn't. It seems that trying to chain any method onto any attribute causes this null error and I'm not sure why.
First of all keep in mind that the transform function you are using alter the original $this->collection property, you better use map instead that serves the same purpose as transform without altering the original array.
This might be related to your problem because you are modifying the collection you are iterating on, and that can cause issues.
Furthermore, I would suggest you to keep on reading this answer and try out one of the two refactoring alternatives I explained below. That's because I think you are not using API resources correctly and using them properly could actually solve the issue.
Suggestion about your API resource structure
The correct way would be to have two separate files: a News resource and a NewsCollection resource.
This setup allows to define the rendering structure of a single News as well as a collection of News and reuse the former while rendering the latter.
To implement API resources correctly there are a couple of ways (based on what you are trying to achieve):
Note: in both methods, I take for granted that you already have an additional User resource that defines the structure to render a User model (the $this->user property of a News).
1) Create separate classes for single and collection resources
You have to create two files in your resources folder through these two artisan commands:
// Create the single News resource
php artisan make:resource News
// Create the NewsCollection resource
php artisan make:resource NewsCollection
Now you can customize the collection logic:
NewsCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class NewsCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
// Each $this->collection array item will be rendered automatically
// with the News resource definition, so you can leave data as it is
// and just customize the links section/add more data as you wish.
'data' => $this->collection,
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
}
as well as the single News resource logic:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
To render a news collection, you only have to do:
use App\News;
use App\Http\Resources\NewsCollection;
// ...
return new NewsCollection(News::paginate());
Laravel will automatically reuse the News resource class to render each single element of the NewsCollection's $this->collection array when you are converting the NewsCollection instance for response.
2) Exploit the ::collection method of the single News resource
This method is applicable only if you need metadata about paginated responses (it seems that is what you are trying to achieve with your code).
You just need the single News api resource that you can generate with:
// Create the single News resource
php artisan make:resource News
Then customize the single resource according to your needs:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
Then to render a paginated collection of news, just do:
use App\News;
use App\Http\Resources\News as NewsResource;
// ...
return NewsResource::collection(News::paginate());
The first method allow for a better overall control of the resulting output structure, but I would not structure the $this->collection inside the collection class.
The responsability to define how each collection element should be structured is of the News resource class.
The second method is quicker and works really nice with Laravel pagination allowing you to save quite some time to generate paginated responses with links (that seems what you want to achieve from your code).
Sorry for the long post, if you need further explaination just ask.

Can I use transformers to transform data coming from API rather than from database?

I have been using laravel to build my APIs. I use transformers to tranform data from model object.
Now instead of database, I have a response coming from an API as the data source and I want to transform that data back to a user, but I am unable to do so.
My Controller
public function rocByName(Request $request)
{
try {
$this->roc_by_name_validator->with( $request->all() )->passesOrFail();
$company_name = $request->input('company_name');
$result = $this->my_service->getDetailsByName($company_name); //$result has the response object from the API which I want to transform and give it as a response.
return $this->response->collection($result,new OnboardingTransformer()); //Tried using tranformer like this
}
catch (ValidatorException $e) {
dd($e);
}
}
My Transformer
<?php
namespace Modules\Onboarding\Transformers;
use League\Fractal\TransformerAbstract;
use App\Entities\OnboardingEntity; //I dont have an entity since the response is coming from an API!! What will give here?
/**
* Class OnboardingTransformerTransformer
* #package namespace App\Transformers;
*/
class OnboardingTransformer extends TransformerAbstract
{
/**
* Transform the \OnboardingTransformer entity
* #param \OnboardingTransformer $model
*
* #return array
*/
public function transform(OnboardingEntity $data_source)
{
return [
'company_name' => $data_source->company_name,
];
}
}
Here the OnboardingEntity refers to data coming from database ideally. Here I am not fetching data from database, instead my data is from an API source. How do I go about it. I am little consfused here. Can someone give a solution?
$result has the following response
[
[
{
"companyID": "U72400MHTC293037",
"companyName": "pay pvt LIMITED"
},
{
"companyID": "U74900HR2016PT853",
"companyName": "dddd PRIVATE LIMITED"
}
]
]
$this->response->collection is meant to get a collection of objects, not the array. Then all of this objects is going to transformer that is transform OnboardingEntity objects as you want. So first you should transform your input array into collection of objects. The example how i did it above (you should change it to your own input array)
$data = json_decode('[
[
{
"companyID": "U72400MHTC293037",
"companyName": "pay pvt LIMITED"
},
{
"companyID": "U74900HR2016PT853",
"companyName": "dddd PRIVATE LIMITED"
}
]
]');
$data = collect( array_map( function($ob){
return (new OnboardingEntity($ob));
}, $data[0]));
And then pass this collection of OnboardingEntity objects to $this->response->collection method, like here
$this->response->collection($data,new TestTransformer());
You may want to send common data structure to Fractal as sources of data are different. Array is best possible type for you.
Consider this when you are fetching data from Eloquent(DB):
$result = $yourModel->get(); // This will return you with a collection object.
Before passing this object to fractal convert it to array.
$this->response->collection($result->toArray(),new OnboardingTransformer());
In case of first or single model object. check for null before calling toArray().
$result = $yourModel->first();
if($result){
$result = $result->toArray();
}
// Fractal itself can handle null
Now for second scenario where data is coming from external source like API or file.
$result = $this->my_service->getDetailsByName($company_name);
// Try converting your response object to array from within
You can do this with json_decode(<Body of response>, true). And then pass this array to Fractal.
Why Array?
Because source of data could be anything from Database to File, from Cache to APIs. Format could be JSON or XML. Converting all these to array comes built in PHP.

Laravel 4. Return database table as JSON

Im really new to Laravel. I have manage to set up a database via the migration functionality, and now i want to renturn a table from the database as json. What im working on is kind of a rest-api-thingy. Nothing too fancy.
In my router i have a route going to /api/cases wich inits the controller for the cases. From that controller i basically just want to return a table from my database as JSON.
Router:
Route::resource('/api/cases', 'CasesController');
Controller:
class CasesController extends \BaseController {
public function index()
{
//return db table as json here
}
}
Model:
class Case extends \Eloquent {
protected $fillable = [];
}
And my database looks like this:
I have only one table, named "cases". That one has attributes like "id", "name", "title".
How would i now return that rest-like as json?
You can simply call the toJSON() method:
Case::all()->toJson();
I assume you have your Case model tested and working properly. Once that's done, you can query for all the objects in this table, convert the result to an array, and encode it as JSON.
public function index()
{
return Response::json(Case::all()->toArray());
}
I don't believe it is the job of the ORM to worry about presentation logic, and that is what JSON is. You'll aways need to cast data to various types as well as hide things and sometimes create a buffer zone to rename things safely.
You can do all of that with Fractal which I built for exactly this reason.
<?php namespace App\Transformer;
use Acme\Model\Book;
use League\Fractal\TransformerAbstract;
class BookTransformer extends TransformerAbstract
{
/**
* List of resources possible to include
*
* #var array
*/
protected $availableIncludes = [
'author'
];
/**
* Turn this item object into a generic array
*
* #return array
*/
public function transform(Book $book)
{
return [
'id' => (int) $book->id,
'title' => $book->title,
'year' => (int) $book->yr,
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book->id,
]
],
];
}
/**
* Include Author
*
* #return League\Fractal\ItemResource
*/
public function includeAuthor(Book $book)
{
$author = $book->author;
return $this->item($author, new AuthorTransformer);
}
}
Embedding (including) stuff might be a bit more than you need right now, but it can be very handy too.
I often give talks about APIs and the dangers of trying to expose database schema directly. Unless you app is on an internal network, and only your app looks at this data, and your app will never going to change at all then interacting directly with the table is a very bad idea.
Here is my talk, which uses Laravel as an example a few times.

How to properly wrap a JSON database result within an object

I created an API for my various SPA's using a Laravel resource controller that works like a charm. While BackboneJS has no problem with the default JSON response, EmberJS expects the result wrapped in a singular and plural named JSON object depending on if it's fetching a single model or a collection of models.
What I already know
Convert the JSON result to an array wrap it and convert it back to JSON.
Wrapping every result in an object.
Change the result comming from the database call to an array with setFetchMode(PDO::FETCH_ASSOC);.
Question
How to properly (using the cheapest process) create the wrapped JSON responses, so without converting the results back and forth between JSON, Arrays or Objects?
A snippit from the Laravel resource controller:
<?php
class ResourceController extends Controller {
...
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index() {
return Model::all();
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id) {
return Model::find($id);
}
...
}
This will wrap the original Eloquent database result for a Laravel application with a database configured as "fetch" => PDO::FETCH_CLASS. It creates a new object of Illuminate\Database\Eloquent\Collection and wraps the Eloquent result in a models property. This way it's formatted properly for using it with Ember data without any conversions.
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index() {
$index = new Illuminate\Database\Eloquent\Collection;
$index['models'] = Model::all();
return $index;
}
You can do that with following structure. Always, return your json response in wrapped format like(I assumed you have User model);
{
"user": [
{"name": "Hüseyin"},
{"surname": "BABAL"},
{"title": "Software Developer"}
]
}
This is suitable for ember.js. For backbone.js, you need to do some extra simple job like following;
var User = Backbone.Collection.extend({
model: User,
url: '/api/userInfo',
parse: function(response) {
return response.user;
});
}
});
By doing this, you will only one format rest service, and you just need to do some extra job for only backbone.js
Update:
Think about index action, you can do that like;
public function index() {
return Response::json(array("user" => Model::all()));
}
Result will be wraped with "user", you can use response as json on frontend. The result will be;
{
"user": [
{...},
{...}
]
}

Categories