I am setting up a API and looking for an efficient way to return each of my categories with the count of the records in each of the categories.
Because of this post: Laravel hasMany relation count number of likes and comments on post I know that creating an accessor is one possibility, but I'm not sure how to apply an accessor to each record within an object.
Currently I'm using eloquent ::all() to return categories:
public function getAPICategories($localKey){
$data['categories'] = Skill::all();
return Response::json($data)->setCallback('test');
}
This returns the full list of categories in JSONP
/**/test({
"categories":[
{
"id":"1",
"name":"Accounting",
"created_at":"-0001-11-30 00:00:00",
"updated_at":"-0001-11-30 00:00:00",
"deleted_at":null
},
{
"id":"2",
"name":"Advertising",
"created_at":"-0001-11-30 00:00:00",
"updated_at":"-0001-11-30 00:00:00",
"deleted_at":null
}]
});
Specifically, what I'm looking to return: - note added "count":
{
"id":"2",
"name":"Advertising",
**"count":"13",**
"created_at":"-0001-11-30 00:00:00",
"updated_at":"-0001-11-30 00:00:00",
"deleted_at":null
},
The count needs to come from the total number of members with the specific category tag on the contractor_skill pivot table.
I got the answer I was looking for by building my own array and adding in the data I was looking needed from the eloquent relations:
public function getAPICategories($localKey){
$skills= Skill::all();
$data = [];
foreach ($skills as $skill){
$arr = array(
'id' => $skill->id,
'name' => $skill->name,
'count' => $skill->Contractors->count()
);
$data['categories'][] = $arr;
}
return Response::json($data)->setCallback('test');
}
Related
I have 4 models that are relevant.
Company
Location
Customer
ThirdPartyLinkClick
Company.php
public function locations() {
return $this->hasMany('App\Location');
}
Location.php
public function customers() {
return $this->hasMany('App\Customer');
}
public function linkClicks() {
return $this->hasManyThrough('App\ThirdPartyLinkClick', 'App\Customer');
}
Customer.php
public function linkClicks() {
return $this->hasMany('App\ThirdPartyLinkClick');
}
There is no issue when acquiring the count of link clicks for all customers when on a single Location.
I can simply do: $this->linkClicks->count(); which creates a query where it does a WHERE IN (1,2,3,4, etc) query
However, on a Company page, I want to also get this count, but avoid an n+1
Right now, my model method on Company.php is
public function getTotalClicksToReviewSites() {
$locations = $this->locations;
$clicks = 0;
foreach ($locations as $location) {
$clicks += $location->linkClicks->count();
}
return $clicks;
}
This creates duplicate queries where it checks location_id on each query. There will be a query for every location rather than checking a group of id's in a WHERE IN statement.
How can I do an eloquent query that will use a single query to gather this data? I only need the count.
You need to use eager loading.
$companies = Company::with('locations', 'locations.linkClicks')->get();
This prevents n + 1 query problem, so, to get te total of linkClicks for each company youur function will work or simply you can do.
$companies = Company::with('locations', 'locations.linkClicks')
->get()
->map(function ($company) {
$company->linkClicksCount = $company->locations->sum(function ($location) {
return $location->linkClicks->count();
});
return $company;
});
The output should be something like this (a new property linkClicksCount added on each company).
[
{"id": 1, "name": "Company 1", "linkClicksCount": 9, "locations": [], ...},
{"id": 2, "name": "Company 2", "linkClicksCount": 3, "locations": [], ...},
...
]
I try to show specific columns of my data after call load() , let say 'id','kd_prop_id' only. can somebody help me
public function show(Provinsi $provinsi)
{
abort_if(Gate::denies('provinsi_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');
return new ProvinsiResource($provinsi->load([])); // <-- i want show specific column here..
}
right now its show all fields :
"data": {
"id": 616,
"kd_prop_id": 11,
"kd_kab": 1102,
"kd_dt1": "06",
"kd_dt2": "10",
"nama_kab": "KAB. ACEH",
"lat": 3.3,
"lng": 97.69,
"kd_bast": "061300",
"created_at": null,
"updated_at": null,
"deleted_at": null
}
plase help..thanks
Actually my choice would be to show the fields I want in the ProvinsiResource.
But you can review for only model:
$company = Company::query()
->first();
//$company->load(['customer']); // All company columns and customer columns
$company->load([
'customer' => function($query) {
return $query->select(['id', 'name']);
}
]);
$company->only(['id', 'customer_id', 'name', 'customer']); // Only specific columns in Company and Customer columns
If you have collection, this way could be better:
$companies = Company::query()
->get();
$companies->map(function ($company) {
return collect($company->toArray())
->only(['id', 'name', 'email'])
->all();
});
To keep it simple, let's take an example of stackoverflow's questions page where there is a paginated list of questions and each question has some tags attached to it.
A Tag model is in Many to Many relation with Question model i.e.
A tag can be assigned to many questions.
A question can be assigned to many tags.
For this relation I created an relational model named QuestionTag (and table for it) that has the relation with both Tag and Question. Then I used laravel's hasManyThrough relation to get a list of tags assigned to a question through the QuestionTag model as such:
class QuestionTag extends Model
{
public function question()
{
return $this->belongsTo(Question::class, 'question_id', 'id');
}
public function tag()
{
return $this->belongsTo(Tag::class, 'tag_id', 'id');
}
}
class Question extends Model
{
public function tags()
{
return $this->hasManyThrough(Tag::class, QuestionTag::class, 'question_id', 'id', 'id', 'tag_id');
}
}
And I created QuestionResource for returning the expected paginated results of questions as such:
QuestionResource
class QuestionResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'subject' => $this->subject,
'body' => $this->body,
'tags' => $this->tags // this will call the has many through relations as expected.
];
}
}
Result
{
"current_page": 1,
"data": [
{
"id": 1,
"subject": "Lorem ipsum dolor sir amet!",
"body": "...",
tags: [
{
"id": 1,
"name": "tag1",
},
// ...and so on
]
},
// ...and so on
],
"first_page_url": "http://127.0.0.1:8000/uv1/questions?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "http://127.0.0.1:8000/uv1/questions?page=1",
"next_page_url": null,
"path": "http://127.0.0.1:8000/uv1/questions",
"per_page": "15",
"prev_page_url": null,
"to": 5,
"total": 5
}
At last, on the index function, I returned the paginated list of questions from the QuestionController's index function as such:
public function index(Request $request)
{
$perPage = $request->input('perPage') ?? 15;
// execute the query.
$crawlers = Question::paginate($perPage);
return QuestionResource::collection($crawlers);
}
It returned what I wanted but when I increased the per_page size to 100 or more, it is returning this error:
Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
I found many solutions that suggests to increase the memory in php.ini(memory_limit = 2048M) but it feels like we are bruteforcing to acheive the outcome. There will be some point when again the memory_limit will fail to return the same when I keep on increasing the per_page size.
Is there any optimal way in laravel to get the same expected result(instead of above mentioned error) with the desired output without increasing the memory size?
I used Inner Join to achieve this and used MySQL's JSON_ARRAYAGG(JSON_OBJECT()) along with it in SELECT statement to create a concatenated json string of tags and later convert it to json array using php json_decode(). Little tacky but it returned the result fast and I could load thousands of records within milliseconds.
My QuestionController now looks like this:
public function index(Request $request)
{
$perPage = $request->input('perPage') ?? 15;
// execute the query.
$crawlers = Question::query()
->select(['questions.*', DB::raw('JSON_ARRAYAGG(JSON_OBJECT("id", `tags`.`id`, "name", `tags`.`name`)) AS `tags`')])
->join('question_tags', 'questions.id', 'question_tags.question_id')
->join('tags', 'question_tags.tag_id', 'tags.id')
->groupBy('questions.id')
->paginate($perPage);
return QuestionResource::collection($crawlers);
}
And I removed the join relations from the models and changed my QuestionResource as such:
class QuestionResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'subject' => $this->subject,
'body' => $this->body,
'tags' => json_decode($this->tags ?? '[]', false, 512, JSON_THROW_ON_ERROR), // convert string JSON to array
];
}
}
Currently, I've implemented this approach but I'm still open to better solutions. :)
I want use Laravel Eloquent to do relationship, but I have a problem accessing a specific filtered object in the relationship.
My objects:
courses:
id - integer
name - string
contents:
id - integer
name - string
course_contents:
id - integer
course_id - integer
content_id - integer
I want get the contents by the course. Until now I can only filter the course_contents to filter contents
My controller:
Course::hasContents()->find($id);
Course Model
public function contents()
{
return $this->hasMany('App\CourseContent');
}
public function scopeHasContents($query)
{
$query->with(['contents' => function($contentQuery) {
$contentQuery->has('content')->with('content');
}]);
}
CourseContents Model:
public function content()
{
return $this->hasOne('App\Content', 'id');
}
My json return ( Course Find ) :
{
"id":1,
"name":"Course Example 1",
"contents":[
{
"id":1,
"course_id":1,
"content_id":1,
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05",
"content":{
"id":1,
"name":"Content Example 1",
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05"
}
},
{
"id":2,
"course_id":1,
"content_id":2,
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05",
"content":{
"id":2,
"name":"Content Example 2",
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05"
}
},{ ... }
],
}
What I need:
{
"id":1,
"name":"Course Example 1",
"contents":[
{
"id":1,
"name":"Content Example 1",
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05"
},
{
"id":2,
"name":"Content Example 2",
"deleted_at":null,
"created_at":"2019-07-16 17:31:05",
"updated_at":"2019-07-16 17:31:05"
},{ ... }
],
}
First, you need to adjust the relationships a bit. You've many to many relationships so the models should look like:
Course.php
public function contents()
{
return $this->belongsToMany(Content::class, 'course_contents');
}
Content.php
protected $hidden = ['pivot'];
public function courses()
{
return $this->belongsToMany(Course::class, 'course_contents');
}
You can retrieve contents data as given below:
for instance: you want to get all contents for a course 1
Content::whereHas('courses', function($query) {
$query->where('courses.id', 1);
})->get();
// You need to pass course id dynamically but for demonstration, I hard coded it.
This will give you the following result:
array:1 [
0 => array:2 [
"id" => 1
"name" => "Content 1"
]
]
Use the belongsToMany relationship:
In your Course model:
public function contents()
{
return $this->belongsToMany(Contents::class, 'course_contents');
}
Then, use $course->contents;
This function returns all content models of the course.
Hope it helps.
The many-to-many relationship is defined by returning a belongsToMany relationship in the relationship method contents in the Course model. As stated in the Laravel many-to-many documentation.
To retrieve only the Content items in the many to many relationship and not the pivot columns, you should change the relationship instance from App\CourseContent to App\Content.
in content model
public function course()
{
return $this->hasMany('App\Course');
}
in course model
public function content()
{
return $this->hasMany('App\Content');
}
you can do
Course::with('content')->find($id)
or
Content::whereHas('course',function($q)use($id){
$q->where('id',$id)
})->get();
I'm building an api using eager loading so i can simply return the user model with its deep relations and it automatically be converted as json. Here's the set up.
users
id
..
clients
id
..
user_clients
id
user_id
client_id
..
campaigns
id
..
client_campaigns
id
client_id
campaign_id
..
campaign_activities
id
campaign_id
..
client_campaign_activity_templates
id
campaign_activity_id
client_id *(templates are unique per client)*
..
I've setup the models' relationships.
User
public function clients() {
return $this->belongsToMany('App\Client','user_clients');
}
Client
public function campaigns() {
return $this->belongsToMany('App\Campaign','client_campaigns');
}
Campaign
public function activities() {
return $this->hasMany('App\CampaignActivity');
}
CampaignActivity
public function templates() {
return $this->hasMany('App\ClientCampaignActivityTemplate')
}
I have a simple api endpoint to provide a JSON of a User object including its deep relations using eager loading.
public function getLoggedInUser(Request $request) {
return \App\User::with('clients.campaigns.activities.templates')->find($request->user()->id);
}
Testing this using postman, I can get the user including its deep relations.
{
"user": {
"id": 1,
"name": "user1",
"clients": [
{
"id": 1,
"name": "client1",
"campaigns": [
{
"id": 1,
"name": "campaign1",
"activities": [
{
"id": 1,
"name": "activity1",
"templates": [
{
"id": 1,
"name": "template1 for client1",
"client_id": 1,
"body": "this is a template.",
}, {
"id": 2,
"name": "template1 for client2",
"client_id": 2,
"body": "This is a template for client2"
}
]
}, {
"id": 2,
"name": "activity2",
"templates": []
}, {
"id": 3,
"name": "activity3",
"templates": []
}
]
}
]
}
]
}
}
However, on the user->clients->campaigns->activities->templates level, it will list all the templates for that activity. I know based on the code of the relationships of the models above that it's supposed to behave like that.
So the question is How would you filter the templates to filter for both campaign_activity_id and client_id?
I've been experimenting on how to filter the templates so it will only list templates for that activity AND for that client as well. I have a working solution but it's N+1, I'd prefer eloquent approach if possible. I've been scouring with other questions, answers and comments for a closely similar problem, but I had no luck, hence I'm posting this one and seek for your thoughts. Thank you
I think what you need are eager loading constraints.
public function getLoggedInUser(Request $request) {
return \App\User::with('clients.campaigns.activities.templates',
function($query) use($request) {
$client_ids = Client::whereHas('users', function($q) use($request){
$q->where('id', $request->user()->id);
})->pluck('id');
$query->whereIn('templates.client_id', $client_ids);
})->find($request->user()->id);
}
Not tested but it should only require one additional query.
What I am doing is: define a constraint for your eager loading, namely only show those templates that have a client_id that is in the list (pluck) of Client IDs with a relation to the User.
Try using closures to filter through related models:
$users = App\User::with([
'clients' => function ($query) {
$query->where('id', $id);
},
'clients.campaigns' => function ($query) {
$query->where('id', $id);
}
])->get();
Here's my working solution, but I'm still interested if you guys have a better approach of doing this.
On the CampaignActivity model, I added a public property client_id and modified the relationship code to
CampaignActivity
public $client_id = 0
public function templates() {
return $this->hasMany('App\ClientCampaignActivityTemplate')->where('client_id', $this->client_id);
}
and on my controller, limit the eager loading to activities only (actually, there are more sqls executed using eager loading[9] in this case vs just iterating[7], and also eager loading doesn't make sense anymore because we're iterating lol)
public function getLoggedInUser(Request $request) {
foreach ($user->clients as $client)
foreach( $client->campaigns as $campaign)
foreach ($campaign->activities as $activity) {
$activity->client_id = $client->id;
$activity->templates; //to load the values
}
return $user;
}