Laravel - Unnecessary columns still being loaded with select statement - php

I am wanting to limit a controller's function's result to only pass certain columns into the view.
It is necessary because it will be used within an API, and so I need the results to be as streamlined as possible.
I have done this successfully with the following function:
public function getIndex()
{
$alerts = Criteria::select('id', 'user_id', 'coordinate_id', 'alert_name')
->with(['coordinate' => function($q){
$q->select('name', 'id');
}])
->get();
}
So it only returns id, user_id and coordinate_id from the criteria table.
However on the function below, I am using a has query (to access a relationship), and thus, using with afterwards to limit the columns, but it's still returning all:
public function getMatches()
{
$matches = Criteria::select('id')
->has('alerts')
->with(['alerts' => function ($q){
$q->select('id', 'headline', 'price_value', 'price_type');
}])
->with('alerts.user.companies')
->get();
}
But, for example, it's still returning the description column, which is in the alert's table. The with query proceeding the has query clearly isn't working (but it's presenting no errors).
Also, the ->with('alerts.user.companies') query, is returning everything within the user's table, which is also unnecessary. How can I return just the companies table data, that's related to the user, who's related to the alert?
Your help would be greatly appreciated.

Depending what you want to achieve, you could use $hidden property to hide columns you don't want to return as json or arrays.
In your Alert model you could do:
protected $hidden = ['description'];
And this way description field won't be returned.
If it's not the way for you (sometimes you want to return description) you could create extra relationships where you limit fields from database.
You could for example create the following relationship:
public function alertsSimple() {
return $this->hasMany('Alert')->select('id', 'headline', 'price_value', 'price_type', 'criteria_id');
}
Also maybe in your select the problem is that you don't use foreign key at all. You could also try with:
$q->select('id', 'headline', 'price_value', 'price_type','criteria_id');
instead of
$q->select('id', 'headline', 'price_value', 'price_type');

Related

How to select only one Column with scope of eloquent model?

I got two tables. Both have a relationship to each other. I´m trying to query both to get the matching results. This results get checked if they also have an column which matches with a parameter value.
I´m trying it with a scope and it work. I only need one column from the second table and I´m trying to use it as column in my first table when I got my result.
So the code works and I got an result but I´m trying to filter to select only one column from the second table.
My code look like that.
My controller:
public function test()
{
$UID='LQuupgYvnuVzbEoguY4TF8bnHUU2';
$event=Events::withState($UID)->get();
echo $event;
}
My model scope function:
public function scopeWithState($query,$UID){
return $query->with(['EventLiked' => function($query) use($UID) {
$query
->where('EventLiked.UID', $UID)
;
}]);
}
My hasMany relationship function:
public function EventLiked()
{
return $this->hasMany(EventLiked::class,'EID','ID')->select('State','UID','EID');
}
I would go for specifying columns inside closure.
New scope:
public function scopeWithState($query,$UID){
return $query->with(['EventLiked' => function($query) use($UID) {
$query
->where('EventLiked.UID', $UID)
->select('State');
}]);
}
Calling scope:
$event=Events::withState($UID)->get();
You're not getting expected results because Laravel splits it into 2 queries:
First, for selecting events.
Then it plucks EID
Second, when it looks for EventLiked where matching ID's is found (from second step) and loads as relationships.
So you want to change select statement only in 2nd query. Not in a first one

Dynamic Property call giving error (Property [game] does not exist on the Eloquent builder instance.)

I have two models: Game and Game_Assignment. Game_Assignment tells whose job it is to play a game.
I am trying to count the number of Game_Assignment's that a user has their id on that also have a specific value on the Game model that it relates to. I'll just get into the Models/the code
Game Model Relationship:
public function assignments() {
return $this->hasMany('App\Models\Game_Assignment', 'game_id');
}
Game_Assignment Relationship:
public function game() {
return $this->belongsTo('App\Models\Game', 'game_id');
}
Where things are going wrong (in a queue job, if that makes a difference)
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game->where('stats_done', '!=', 'yes')->count();
I have also tried the following two things, neither worked:
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game()->where('stats_done', '!=', 'yes')->count();
and...
$gamesDue = Game_Assignment::where('statistician_id', $defaultStatistician->id)->with(['games' => function($query) {
$query->where('stats_done', '!=', 'yes');
}])->count();
None of these work, and the first one I showed threw an error:
Property [game] does not exist on the Eloquent builder instance.
Anyone have an idea of where I am going wrong? I am using this link as my reference https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
When using the query builder of your Game_Assignment model, you cannot simply switch context to the query builder of Game. You can only call ->game() or ->game after you retrieved one or many model instances of Game_Assignment with first() or get().
So, in your particular case, you were looking for whereHas('game', $callback) (where $callback is a function that applies constraints on the foreign table) in order to add a constraint on the foreign table:
use Illuminate\Database\Eloquent\Builder;
$gamesDue = Game_Assignment::query()
->where('statistician_id', $statistician->id)
->whereHas('game', function (Builder $query) {
$query->where('stats_done', '!=', 'yes');
})
->count();
Side note: a column (stats_done) that seems to hold a boolean value (yes/no) should be of boolean type and not string/varchar.

Laravel transform not working as intended on collection

I have a collection which is an eloquent query.
There is one column where I want to replace the value with another value.
I am using the transform function to do this however it is not working as intended.
Here is my query in the controller :
$articles = KnowledgeBaseArticle::getArticlesByDepartment($department)
->get()
->transform(function ($article) {
$article->category_id = KnowledgeBaseCategory::find($article->category_id)->name;
});
And the getArticlesByDepartment query from the model:
public function scopeGetArticlesByDepartment($query, $department){
return $query->where('department', $department)
->select('title', 'updated_at', 'department', 'id', 'category_id')
->orderBy('title', 'asc');
}
I want to return it so that all the rows with column category_id is replaced with the category name. You can see I am trying to do this by using $article->category_id by using find on the KnowledgeBaseCategory model to retrieve this. However this is not working at all and when I die and dump, I get an single column array full of nulls.
When I have died and dumped $article->category_id & find query inside the transform, it is returning the correct category name, it is just not replacing the category_id column with the category name.
I have also tried map instead of transform and got the same result.
If it matters, I am later on converting this data into JSON.
Where am I going wrong?
Transform, not unlike map, needs you to return the modified item of the collection, as this will replace the existing item.
transform(function ($article) {
$article->category_id = KnowledgeBaseCategory::find($article->category_id)->name;
return $article;
});
Since objects are mutable and passed by reference, you can just do this in an each() closure instead to save a line:
each(function ($article) {
$article->category_id = KnowledgeBaseCategory::find($article->category_id)->name;
});
Though, you really should have a relationship set up for category. There's no reason to be performing this find logic in your controller.

Eloquent limit relationship fields

I have the following relationships:
TheEpisodeJob hasOne TheEpisode
TheEpisodeJob hasMany TheJobs
I am successfuly retrieving all TheEpisodesJobs and TheSeriesEpisodes with all the fields in database (including sensitive information) using this command:
$jobs = TheEpisodeJob::with('TheEpisode')->get();
I would like to limit TheEpisode fields shown only for this case (public $hidden will not work)
EDIT
Let's say I need only title and description field from TheEpisode.
How can I achieve that?
As #Buglinjo pointed out you can scope the relationship when eager loading, however, if you're going to be doing this to only select specific columns you must included the related column in the select so that Eloquent knows which Model to attach the related data to.
This should give you what you want:
$jobs = TheEpisodeJob::with(['TheEpisode' => function ($query) {
$query->select('jobID', 'title', 'description');
}])->get();
Furthermore, if you then wanted to to get rid of the jobID as well you could do something like:
$jobs->transform(function ($job) {
$job->TheEpisode->transform(function ($item) {
unset($item->jobID);
return $item;
});
return $job;
});
Hope this helps!
As far as I understood you, you want to limit the results according to some more parameters. If I am right, you should add more queries, like:
->where, ->orwhere, ->select, ->whereNull
Here is the link for more queries. Hope it will help )
I saw an update, so then you need
->pluck('title', 'description');
for more information, go to the link above
You should do like this:
$jobs = TheEpisodeJob::with(['TheEpisode' => function($q){
$q->get(['title', 'description']);
//or
$q->pluck('title', 'description');
}])->get();
Note: pluck is getting as array not as Eloquent Object.

laravel eloquent sort by relationship

I have 3 models
User
Channel
Reply
model relations
user have belongsToMany('App\Channel');
channel have hasMany('App\Reply', 'channel_id', 'id')->oldest();
let's say i have 2 channels
- channel-1
- channel-2
channel-2 has latest replies than channel-1
now, i want to order the user's channel by its channel's current reply.
just like some chat application.
how can i order the user's channel just like this?
channel-2
channel-1
i already tried some codes. but nothing happen
// User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies'])
->orderBy('replies.created_at'); // error
}
// also
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies' => function($qry) {
$qry->latest();
}]);
}
// but i did not get the expected result
EDIT
also, i tried this. yes i did get the expected result but it would not load all channel if there's no reply.
public function channels()
{
return $this->belongsToMany('App\Channel')
->withPivot('is_approved')
->join('replies', 'replies.channel_id', '=', 'channels.id')
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'ASC');
}
EDIT:
According to my knowledge, eager load with method run 2nd query. That's why you can't achieve what you want with eager loading with method.
I think use join method in combination with relationship method is the solution. The following solution is fully tested and work well.
// In User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved');
}
public function sortedChannels($orderBy)
{
return $this->channels()
->join('replies', 'replies.channel_id', '=', 'channel.id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Then you can call $user->sortedChannels('desc') to get the list of channels order by replies created_at attribute.
For condition like channels (which may or may not have replies), just use leftJoin method.
public function sortedChannels($orderBy)
{
return $this->channels()
->leftJoin('replies', 'channel.id', '=', 'replies.channel_id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Edit:
If you want to add groupBy method to the query, you have to pay special attention to your orderBy clause. Because in Sql nature, Group By clause run first before Order By clause. See detail this problem at this stackoverflow question.
So if you add groupBy method, you have to use orderByRaw method and should be implemented like the following.
return $this->channels()
->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
->groupBy(['channels.id'])
->orderByRaw('max(replies.created_at) desc')
->get();
Inside your channel class you need to create this hasOne relation (you channel hasMany replies, but it hasOne latest reply):
public function latestReply()
{
return $this->hasOne(\App\Reply)->latest();
}
You can now get all channels ordered by latest reply like this:
Channel::with('latestReply')->get()->sortByDesc('latestReply.created_at');
To get all channels from the user ordered by latest reply you would need that method:
public function getChannelsOrderdByLatestReply()
{
return $this->channels()->with('latestReply')->get()->sortByDesc('latestReply.created_at');
}
where channels() is given by:
public function channels()
{
return $this->belongsToMany('App\Channel');
}
Firstly, you don't have to specify the name of the pivot table if you follow Laravel's naming convention so your code looks a bit cleaner:
public function channels()
{
return $this->belongsToMany('App\Channel') ...
Secondly, you'd have to call join explicitly to achieve the result in one query:
public function channels()
{
return $this->belongsToMany(Channel::class) // a bit more clean
->withPivot('is_approved')
->leftJoin('replies', 'replies.channel_id', '=', 'channels.id') // channels.id
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'desc');
}
If you have a hasOne() relationship, you can sort all the records by doing:
$results = Channel::with('reply')
->join('replies', 'channels.replay_id', '=', 'replies.id')
->orderBy('replies.created_at', 'desc')
->paginate(10);
This sorts all the channels records by the newest replies (assuming you have only one reply per channel.) This is not your case, but someone may be looking for something like this (as I was.)

Categories