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.
Related
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
I am currently working on a desktop management application with laravel 5.6. According to the management rule a patient can have one or more consultations according to given dates. When I display the list of consultations, I have the same name that repeats, the name that repeats corresponds to the patient who had several consultations, my question of how to avoid this. What I want is the name, and all the dates for these consultations.
class Consultation extends Model
{
public function patient()
{
return $this->belongsTo('App\Models\Patient');
}
}
class Patient extends Model
{
public function consultations()
{
return $this->hasMany('App\Models\Consultation');
}
}
Here is the query :
$consultations = Consultation::all();
The simplest (but not the prettiest) way to do this is to simply find all patients with consultations. Put those patients in an array, and then in your blade you would loop through these patients and show the consultations individually.
Controller Code:
$active_patients = [];
foreach(Patient::all() as $patient) {
if($patient->consultations->count()>0)
array_push($active_patients,$patient);
}
Pass $active_patients to your view, then loop over it as shown below. Obviously, I don't know all of the attribute names for your Patient or Consultation models and you will need to fix html markup as required, but you can get the picture:
#foreach($active_patients as $patient)
<p>{{$patient->name}}:</p>
#foreach($patient->consultations as $consultation)
<p>{{$consultation->date}}</p>
#endforeach
#endforeach
Disclaimer: This is not the most robust way to do this. It's simply the most straightforward approach. The best way to do this is to use scoped queries combined with appended attributes. For instance, you would make a scope on the Patients model for all patients that have a consultation by using the 'whereHas' eloquent query method to find patients that have consultations scheduled. Then you could just reference them directly as ActivePatient rather than having to build an array each time you reference them. You could also append an attribute to the Consultations model that does the same thing and grabs each consultation for the specific users and makes a nested model collection, but that's much more involved. I'd be happy to share that method with you if you want, but the above code would at least provide you with a working method to achieve what you requested.
I have two models, Expense and Tag, which have a Many to Many relation.
For each Expense, I can add multiple tags, which are stored in a pivot table using sync. The table is called expense_tag.
Now on my expenses.show page, I want to display details about one expense, obviously. But, I want to show ALL related expenses, using the tags relationship.
The problem:
I only have the information for one expense. Which means, I need to collect all tags that are assigned to that expense, and then using those tags, grab all expenses that were assigned one or more of those tags as well.
I want to refrain from having to use foreach loops to accomplish this. I've been trying with filter but I am unsure how to go about it. I just prefer keeping it simple.
Any suggestions for this?
My relations in my model:
Expense:
public function tags()
{
return $this->belongsToMany(Tag::class);
}
Tag:
public function expenses()
{
return $this->belongsToMany(Expense::class);
}
The solution is to use a where in clause
$tagIds = $expense->tags()->pluck('id')->toArray();
$expenseIds = DB::table('expense_tag')->
whereIn('tag_id',$tagIds)->pluck('expense_id')->toArray();
$relatedexpenses = Expense::whereIn('id', $expenseIds)->get();
note: this uses 3 queries, so it might be slightly slower than a full sql solution, but it should be ok.
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.
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.