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": [
{...},
{...}
]
}
Related
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',
],
];
}
}
When I create a CRUD controller, this is the show route created by default:
/**
* Display the specified resource.
*
* #param \App\Team $team
* #return \Illuminate\Http\Response
*/
public function show(Team $team)
{
//
}
$team is an object here, an instance of Team. If I do this I have the correct object passed to blade:
public function show(Team $team)
{
return view('admin.teams.show', ['team' => $team]);
}
But, Team has a many-to-many relationship with another model called Player, and this relationship is defined as such from the Team side:
public function players() {
return $this->belongsToMany(Player::class);
}
In my show method, I'd like to return the $team with its related players. But since $team is already an object and not a query builder, it's too late to do something like
$team->with('players')
So how do I get the related players here? I know I can do something like:
public function show(Team $team)
{
$team_extended = Team::where('id', $team['id'])->with('players')->first();
return view('admin.teams.show', ['team' => $team_extended]);
}
But it feels like hacking a functionality that should be there by default. Is there a built-in Laravel way to do this or am I just inventing hot water and should take the approach I used in my solution above?
If you've already got your Team model loaded, you can load a relationship without having to completely re-create it using the ->load() method:
public function show(Team $team){
$team->load("players");
return view("admin.teams.show", ["team" => $team]);
}
Note however, this isn't required unless you need to modify the default content of $team->players. When you trying to access $team->players say in your admin.teams.show view, if that property doesn't already exist (as it would using ->with(["players"]) or ->load("players"), Laravel will load it automatically.
I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).
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.
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.