Limit attributes from joined tables in complex query build - laravel

So my question is a follow in from the one I posted here:
Laravel 8 - Limit joined tables
And using the suggestion in the accepted answer, this is what I ended up with
User::with(['profile'])->with('collections.game')
->whereHas('collections', function (Builder $query) {
return $query->whereHas('game', function (Builder $query) {
return $query->where('status', '<>', 'denied');
});
})
->paginate(10)
It works great. I get the information about the user from both the users and profiles table as well as all the collections the user owns of games that do not have a denied status. Sweet! Thanks #matiaslauriti!
The problem I'm seeing is that all of the columns are getting pulled in. This includes information about the user such as their email address (from the user's table) and their physical mailing address (from the profile table) among other things. Information that I would prefer not to disclose.
Under normal circumstances, this wouldn't be a problem because once the blade template was parsed and the markup for the page is generated, all of that data would get trashed and not get returned to the browser. But my circumstance is different. I'm using InertiaJS with React so all of that data is getting returned from the server via an XHR request and passed in as props to my component. This makes it so that all the data is completely visible and I definitely don't want that.
I've played around with ::without() and ::withOnly() but I couldn't get them to work. Not just not work for User but for the nested Profile data as well. So is there a way to do what I'm doing to get all the data I need but without all the sensitive data I'd rather not return to the client?
thnx,
Christoph

It can be done one by passing a closure function in with() as second index of array.
Example:
Post::query()
->with(['user' => function ($query) {
$query->select('id', 'username');
}])
->get()
It will only select id and username from other table.
Remember that the primary key (id in this case) needs to be the first param in the $query->select() to actually retrieve the necessary results.*

IF you are using a JSON response (e.g. you have an API) and IF you want to have a standard way to create "views" in your data for the purposes of e.g. displaying them in specific responses you can do that using JSON resources(if these two don't apply to you then you can ignore this answer)
In your case you could create two resources e.g. UserResource and ProfileResource and implement them as:
UserResource:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'profile' => new ProfileResource($this->profile),
];
}
ProfileResource:
public function toArray($request)
{
return [
'public_info' => $this->public_info,
];
}
As the docs mention you can send this response as:
$users = User::with(['profile'])->with('collections.game')
->whereHas('collections', function (Builder $query) {
return $query->whereHas('game', function (Builder $query) {
return $query->where('status', '<>', 'denied');
});
})
->paginate(10);
return UserResource::collection($users);

Calling the whereHas method will never select the related records. So if you want to run only the query on relations remove with in query
User::query()
->whereHas('collections', function (Builder $query) {
return $query->whereHas('game', function (Builder $query) {
return $query->where('status', '<>', 'denied');
});
})
->paginate(10);
You can also use dotted relation
User::query()
->whereHas('collections.game', function (Builder $query) {
return $query->where('status', '<>', 'denied');
})
->paginate(10);

Related

Laravel, eloquent, query: problem with retriving right data from DB

I'm trying to get the right data from the database, I'm retriving a model with a media relation via eloquent, but I want to return a photo that contains the 'main' tag stored in JSON, if this tag is missing, then I would like to return the first photo assigned to this model.
how i assign tags to media
I had 3 ideas:
Use orWhere() method, but i want more likely 'xor' than 'or'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main')->orWhere();
}]);
return $models->paginate(self::PER_PAGE);
Raw SQL, but i don't really know how to do this i tried something with JSON_EXTRACT and IF/ELSE statement, but it was to hard for me and it was a disaster
Last idea was to make 2 queries and just add media from second query if there is no tag 'main'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}]);
$models_all_media = Model:: with(['media']);
return $models->paginate(self::PER_PAGE);
but i tried something like
for($i=0; $i<count($models); $i++) {
$models->media = $models_all_media
}
but i can't do this without get() method, beacuse i don't know how to change this to LengthAwarePaginator class after using get()
try using whereHas https://laravel.com/docs/9.x/eloquent-relationships
Model::with('media')
->whereHas('media',fn($media)=>$media->whereJsonContains('custom_properties->tags', 'main'))
->paginate(self::PER_PAGE);
as per your comment you can use
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}])
->leftJoin('media', function ($join) {
$join->on('models.id', '=', 'media.model_id')
->whereNull('media.custom_properties->tags->main');
})
->groupBy('models.id')
->paginate(self::PER_PAGE);
return $models;

Have select() and/or pluck() been broken in Laravel 6?

The following code does not pluck the name column of the selected user record. Rather, returns the entire row. Before I make a re-creatable example: Is this the expected behaviour here?
I want to explicitly select columns across joins to reduce my JSON payload size, and to return a nested model hierarchy to my clients.
I should add that I'm experiencing the same behaviour when using the pluck() function as well, on the same line. Perhaps I've done something wrong.
There's tons of examples showing this approach with earlier versions of Laravel. Version 6 may have broken this.
$query = Post::whereHas('user.address', function ($query) use ($lat, $lon, $distance) {
$query->distance($lat, $lon, $distance);
})->with([
'user' => function ($query) {
$query->select('name'); // TODO: Report this bug. I've also tried pluck()
},
'user.address' => function ($query) use ($lat, $lon, $distance) {
$query->distance($lat, $lon, $distance);
},
'user.address.city',
'bids' => function ($query) {
$query->orderBy('amount', 'DESC');
},
'bids.user',
'images',
]);
pluck() is a collection method, it executes the query and returns a simple Collection object of the field you specify.
Using pluck() inside your subquery builder executes it (returning nothing, because you are assigning it to nothing) while the $query variable is unmodified and behaves as normal returning all columns.
If you were to dump the value of the pluck() inside this query, you would see it is an array of just names, and because of that, it has no affect on the query itself.
'user' => function ($query) {
dd($query->pluck('name'));
}
select() should work fine in this case. You just need to also provide the relationship key or else it will just return a null object.
'user' => function ($query) {
$query->select(['id', 'name']);
},

How to remove fields from Laravel JSON Api response

I have an API returning an object with one-to-one relation to another object. As the models behind the objects, do have timestamps, these are also delivered when asking API.
// Get all transactions
Route::get('transaction', function() {
return Transaction::with('Personone','Persontwo')->get();
});
How do I prevent Laravel from returning the timestamps of the objects in the API?
I googled, but only found some hints to middleware or response macros but found no example pointing me into the right direction. Maybe you can help me.
You can make attributes "hidden" so that they do not show up in json.
docs
class Transaction extends Model
{
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['timestamp'];
}
I'm not sure if I get the question correctly but if you want to select from eager loads there are two ways
first one is inline selecting
Route::get('transaction', function () {
return Transaction::with('Personone:id,foo,bar', 'Persontwo:id,foo,bar,foobar')->get();
});
second one is to pass a closure
Route::get('transaction', function () {
return Transaction::with([
'Personone' => function ($query) {
$query->select('id', 'foo', 'bar');
},
'Persontwo' => function ($query) {
$query->select('id', 'foo', 'bar', 'foobar');
}])->get();
});
Eager Loading Specific Columns You may not always need every column
from the relationships you are retrieving. For this reason, Eloquent
allows you to specify which columns of the relationship you would like
to retrieve:
$users = App\Book::with('author:id,name')->get();
Constraining Eager Loads Sometimes you may wish to eager load a
relationship, but also specify additional query constraints for the
eager loading query. Here's an example:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%'); }])->get(); In this example, Eloquent will only eager load posts where the post's title
column contains the word first. Of course, you may call other query
builder methods to further customize the eager loading operation:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc'); }])->get();

How to query a table using result of many-to-many relation with Eloquent ORM?

here is link to my database schema:
How can I get all topics from the blogs to which the user is subscribed using his id? I use Eloquent ORM.
I have added the following lines to my User model:
public function blogs()
{
return $this->belongsToMany('Blog', 'blog_subscriptions');
}
And then requested in the controller
User::find(1)->blogs; // User id is hardcoded
With that I got all blogs to which the user is subscribed.
And now I am stuck on how to get the topics of those blogs.
Assuming relations:
// User
belongsToMany('Blog', 'user_subscriptions')
// Blog
belongsToMany('User', 'user_subscriptions')
hasMany('Topic')
// Topic
belongsTo('Blog')
1 My way, that will work with any relation, no matter how deeply nested:
User::with(['blogs.topics' => function ($q) use (&$topics) {
$topics = $q->get()->unique();
}])->find($userId);
2 Eloquent standard methods:
$topics = Topic::whereHas('blog', function ($q) use ($userId) {
$q->whereHas('users', function ($q) use ($userId) {
$q->where('users.id', $userId);
});
})->get();
// or
$user = User::find($userId);
foreach ($user->blogs as $blog)
{
$blog->topics; // collection of topics for every single blog
}
3 Joins:
$topics = Topic::join('blogs', 'topics.blog_id', '=', 'blogs.id')
->join('user_subscriptions as us', function ($j) use ($userId) {
$j->on('us.blog_id', '=', 'blogs.id')
->where('us.user_id', '=', $userId);
})->get(['topics.*']);
Mind that last solution will rely on your pivot data consistency. That said, if you can't be sure, that there are no redundant entries in user_subscriptions (eg. blog or user has been deleted but the pivot entry remains), then you need to further join users as well.
I am new to laravel and I don't have the possibility of test this, but I had a similar need and solved it with something like this:
DB::Query('topics')->join('blogs', 'blogs.id', '=', 'topics.blog_id')
->join('blog_subscriptions', 'blogs.id', '=', 'blog_subscriptions.blog_id')
->select('name.id','table.email')
->where('blog_subscriptions.user_id', '=', $user_id);

Laravel eloquent: get data with model wherePivot equal to custom field

I have an eloquent object Performer that has Albums and Albums have Images
Here is setup:
Model Performer->albums():
public function albums()
{
return $this->belongsToMany('Album','performer_albums','performer_id','album_id');
}
Model Album->images()
public function images()
{
return $this->belongsToMany('Image','album_images','album_id','image_id')->withPivot(['type','size']);
}
I have performer object stored as such:
$performer = Performer::where...->first();
Now I need to get Performer's Albums with images where size is 'large'
So to avoid nesting queries, can I use with()?
I tried
$performer->albums()
->with('images')
->wherePivot('size','large')
->get();
But laravel tells me it's trying to use wherePivot for Performer-Album relationship (M-2-M)
PS. I am also aware that I can do this,
$performer = Performer::with('albums')
->with('albums.images')
->.....-conditions for additional fields in album_images....
->get();
but question remains the same.
You need eager load constraints:
$performer->albums()
->with(['images' => function ($q) {
$q->wherePivot('size','large');
}])
->get();
And btw, no, you can't do this:
Performer::with('albums')
->with('albums.images')
->.....-conditions for additional fields in album_images....
->get();
instead you could do:
Performer::with(['albums.images' => function ($q) {
$q-> .....-conditions for additional fields in album_images....
}])->get();

Resources