Relationships and where clauses? - laravel

I have the following tables:
**galleries**
id
location
open_to_public
**pictures**
id
title
published
**gallery_picture**
gallery_id
picture_id
Here's my model for galleries:
class Galleries extends Eloquent {
protected $table = 'galleries';
public function pictures(){
return $this->belongsToMany('pictures', 'gallery_picture', 'gallery_id', 'picture_id');
}
I'm trying to select a gallery (id, location) and get it's related pictures (id, title).
To start with I've tried this but it seems to return a huge amount of data, im not sure if I'm doing it correct?
$this->mGalleries = new Galleries;
return $this->mGalleries->pictures();
What I want to also do is add some constraints to the query, I understand I can do it like:
public function scopePublic()
{
return $query->where('open_to_public','=',1);
}
Then:
return $this->mGalleries->pictures()->public();
But I have yet to implement this correctly. Could someone point me in the right direction.
I want to get gallery.id, gallery.location and all of that galleries pictures where gallery.open_to_public = 1 and to only get pictures that published = 1.
Also I would like to get all of the galleries that are relevant to the above conditions and not just a single one.

Without the specific error you're receiving it's hard to say exactly what's going on her,e but one thing that jumped out at me from your code samples is that you don't accept the $query as a parameter to your scope method. Try this code instead:
public function scopePublic($query)
{
return $query->where('open_to_public','=',1);
}
Additionally to that it looks like you're calling your scoped function on the wrong object. From your description you need something like the following set up:
Gallery (model)
scopePublic($query)
Picture (model)
scopePublished($query)
Also, you can only get the pictures of a single gallery, not all galleries in one go. So you can't use Gallery::all()->pictures as you might expect but instead you have to do your own collection building.
Your final code will end up being something like the following:
// empty collection to store all our pictures
$pictures = new \Illuminate\Database\Eloquent\Collection;
// get all galleries open to the public
$galleries = Gallery::public()->get();
// for each gallery, get its pictures and add it to the collection
$galleries->each(function ($gallery) use ($pictures) {
$pictures->merge($gallery->pictures()->published()->get());
});
However, there are a few different ways you could do this. You might find that preloading your relations helps with the database queries too (look up Eloquent's with(), and see if there's a way to pass your scope call in there somehow). Alternatively, if Eloquent's syntax is a little too verbose, you could try using the DB class and joins manually.
Also, sorry if that code is buggy, it's untested but should give you at least a grounding of how to go about solving this.

Related

Prevent duplicate queries and N+1 problem in Laravel collection

I'm currently working on a simple Laravel project where I need to get the posts of the users I'm following. With the code below, I can get the posts but I also add a lot of duplicate queries and an N+1 issue on the Authenticated user. So it's becoming sort of a head scratcher. I've looked though other similar scenarios online but I haven't been able to pinpoint what I'm doing wrong. Perhaps there is a better way. Currently, I have on the User model:
public function usersImFollowing()
{
return $this->belongsToMany(User::class, 'follow_user', 'user_id', 'following_id')
->withPivot('is_following', 'is_blocked')
->wherePivot('is_following', true)
->wherePivot('is_blocked', false)
->paginate(3);
}
public function userPosts()
{
return $this->hasMany(Post::class, 'postable_id', 'id')
->where('postable_type', User::class);
}
As you can see, I am using two booleans to determine if a user is following or is blocked. Also, the Post model is a polymorphic model. There are several things I've tried, among them, I tried a hasManyThrough, without using the hasMany Posts relationship above. It got the posts for each user but since I'm using the booleans above, I couldn't use them in the hasManyThrough, it simply got the posts based on the following_id, whether or not the user was following or was blocked became irrelevant.
Then in a separate service class, I tried the methods below (I'm using a separate class to maintain the code easier). They both get the posts for each user but add an N+1 problem and 12 duplicate queries based on 5 posts from 2 users. I will also need to filter the results based on some conditions, so it will probably add more queries. Additionally, I'm using a Laravel resource collection that would pull other items for each post, such as images, comments, etc., so the amount of queries would increase even more. Not sure, perhaps I'm doing too much and there is an easier way:
Either:
$following = $request->user()->usersImFollowing();
$posts = $following->map(function($user){
return $user->userPosts()->get();
})->flatten(1);
return $posts;
Or
$postsfromfollowing = [];
$following = $request->user()->usersImFollowing()->each(function($user) use (&$postsfromfollowing){
array_push($postsfromfollowing,$user->userPosts);
});
$posts = Arr::flatten($postsfromfollowing);
return $posts;
Maybe you could use scopes to do little celanup of code and generated sql.
In User model something like
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->where('following_id', '=', $followerId);
}
And in Post model
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->whereHas('user', function($q) use ($followerId) {
$q->isFollowedBy($followerId);
});
}
You can use it then in coltroller like any other condition like this:
Post::isFollowedBy($followerId)->...otherConditions...->get();
The SQL generated won't go through foreach but only add one IF EXISTS select (generated by whereHas part of the code)
More on local scopes in Laravel is here https://laravel.com/docs/8.x/eloquent#local-scopes

How can I eager load a group of relations that was previously specified within model in Laravel?

I have multiple cards that belong to the user model. Like vendor, customer and admin and so on. I can eager load them typically like:
User::with(['admin', 'vendor', 'customer'])->get();
but this cards are changeable, there could be added some more. In that case I don't want to make that change everywhere where I used to eager load that group. Instead I want something like
User::withCards()->get();
And in model itself something like:
protected $cards = ['admin','vendor', 'customer'];
public function withCards(){
return $this->with($this->cards);
}
Well I tried that without luck. But I think with the example above you understand what I meaning. How could I accomplish something like that?
You are looking for local query scopes. You can achieve this using:
protected $cards = ['admin','vendor', 'customer'];
public function scopeWithCards($query){
return $query->with($this->cards);
}

Passing computed parameters to middleware

I would like to pass a computed value into my middleware through my web.php routes file. I have this model configuration:
Planet hasMany Countries
Country hasMany Cities
City hasMany Buildings
In all of my routes I need to build a menu that helps navigate between planets. So, no matter what I'm doing I need to be able to access the planet->id. Right now, in my middleware I have a long series of if/elseif checking to see which parameters exist in the URL:
if (isset($parameters['planet']))
$planetId = $parameters['planet']->id;
else if (isset($parameters['country']))
$planetId = $parameters['country']->planet->id;
else if (isset($parameters['building']))
$planetId = $parameters['building']->country->planet->id;
Obviously this gets very unwieldy very quickly and I feel like there should be a better way to pass the planetId into the middleware but I cannot find any way to gracefully do that...
Thank you very much for your help!
You're better off just adding "planet_id" to all of your models that use their planet reference frequently.
Writing logic with a bunch of nested relationship referencing can really take a toll on the speed of your application. Writing the extra data which seems counter intuitive at first, will actually really help. You want to prioritize the speed of your SELECT queries over the size of your database where you can reasonably. Your SELECT queries will be the majority of your applications database usage. In our modern day having a few extra bytes isn't that big of a deal and it would serve a huge purpose here.
Add planet_id to your countries and buildings migration table.
Add relationship method "planet" to your country and building model class.
Then you can easily do something like:
function getPlanetFromRequest($request)
{
$parameters = $request->all();
$planetParameterNames = ['building', 'country', 'planet'];
foreach ($planetParameterNames as $planetParameterName) {
if (isset($parameters[$planetParameterName])) {
$model = $parameters[$planetParameterName];
// if the model is a planet, just return its self.
if ($model instanceof Planet) {
return $model;
}
// $model would be country or building.
return $model->planet;
}
}
return null;
}
If you only need the planets id, save yourself a query and just reference the models planet_id rather than referencing its planet and then grabbing the id.
$country->planet_id; // NOT $country->planet->id;

Laravel retrieving single related attribute

I have a laravel app that allows users to post posts. Each post has a price (stored as an integer), and belongs to a university, which in turn belongs to a country, which has a currency.
Every-time I retrieve the posts, I want to return the currency as well. I could do with('university.country') but that would return all the details for both the university and country.
I could add a getCurrencyAttribute and define the logic there, but that seems not what mutators are for, especially since if I get all the posts, each post will further run two of its own queries just to get the currency. That's 3 queries to get one post, which quickly takes its toll when returning more than 10 posts.
public function getCurrencyAttribute() {
return $this->university->country->currency;
}
public function getPriceAttribute($value) {
return "{$this->currency}{$value}";
}
^ example above: no need for appends because price is automatically overwritten. This is the problem as seen on DebugBar (two new queries are being called on the Post model, which while expected, becomes inefficient when retrieving lots of posts):
What's the best way to get a single related field, every-time?
Full code on GitHub.
You can limit the eager loading columns:
Post::with('webometricUniversity:uni-id,country_id',
'webometricUniversity.country:id,currency')->get();
If you always need it, add this to your Post model:
protected $appends = ['currency'];
protected $with = ['webometricUniversity:uni-id,country_id',
'webometricUniversity.country:id,currency'];
public function getCurrencyAttribute() {
return $this->webometricUniversity->country->currency;
}
Then you can just use Post::get() and $post->currency.

Laravel - Retrieve the inverse of a many-to-many polymorphic relation (with pagination)

after some digging I still could not find any solid way to retrieve the inverse of a many-to-many polymorphic relation that allows mixed models results.
Please consider the following:
I have several models that can be "tagged". While it is trivial to retrieve for example $item->tags, $article->tags and the inverse with $tag->articles and $tag->items I have no easy way to do something like $tag->taggables to return both articles and items in the same collection. Things get even bumpier as I need to use pagination/simple pagination to the query.
I have tried a few workarounds but the best I could put together still looks crappy and limited. Basically:
I queried the DB once per "taggable";
put all in a single big collection;
passed the collection to a phpleague/fractal transformer (my API uses it) that returns different json values depending on the parsed models.
The limits of this approach is that building a pagination is a nightmare and fractal "include" options can't be used out of the box.
Can anyone help me? I'm currently using Laravel 5.1.
There is not much magic in my current code. Faking and simplifying it to make it short:
From the api controller:
$tag = Tag::findOrDie($tid);
$articles = $tag->cms_articles()->get();
$categories = $tag->cms_categories()->get();
$items = $tag->items()->simplePaginate($itemsperpage);
$taggables = Collection::make($articles)->merge($categories);
// Push items one by one as pagination would dirt the collection struct.
foreach ($items as $item) {
$taggables->push($item);
}
return $this->respondWithCollection($taggables, new TaggableTransformer);
Note: using simplePaginate() is there only because I would like all articles and categories to be shown on first page load while the number of items are so many that need pagination.
From the Transformer class:
public function transform($taggable)
{
switch (get_class($taggable)) {
case 'App\Item':
$transformer = new ItemTransformer;
break;
case 'App\CmsArticle':
$transformer = new CmsArticleTransformer;
break;
case 'App\CmsCategory':
$transformer = new CmsCategoryTransformer;
break;
}
return $transformer->transform($taggable);
}
Please consider that the other transformers are simply returning arrays of data about the models they correlate with. If you use Fractal you would easily spot that nested "included" models would not be applied.
Nothing fancy for the Tag model:
class Tag extends Model
{
protected $morphClass = 'Tag';
protected $fillable = array('name', 'language_id');
public function cms_articles() {
return $this->morphedByMany('App\CmsArticle', 'taggable');
}
public function cms_categories() {
return $this->morphedByMany('App\CmsCategory', 'taggable');
}
public function items() {
return $this->morphedByMany('App\Item', 'taggable');
}
// Would love something like this to return inverse relation!! :'(
public function taggables() {
return $this->morphTo();
}
}
I am also considering the option to do 3 separate calls to the API to retrieve articles, categories and items in three steps. While in this particular scenario this might make sense after all, I would still need to deal with this particular inverse relation headache with another part of my project: notifications. In this particular case, notifications would have to relate to many different actions/models and I would have to retrieve them all in batches (paginated) and sorted by model creation date...
Hope this all makes sense. I wonder if a completely different approach to the whole inverse "polymorphic" matter would help.
Kind regards,
Federico
Ah yes. I was down your path not all that long ago. I had the same nightmare of dealing with resolving the inverse of the relationship of polymorphic relationships.
Unfortunately polymorphic relationships haven't been given much attention in the Laravel ecosystem. From afar they look like unicorns and rainbows but soon you're fighting things like this.
Can you post an example of a $thing->taggable for a better picture? Think it may be solvable with a dynamic trait + accessor magic.

Resources