Using Laravel whenLoaded() deeper than one level - php

How do I use whenLoaded() for deeper than one level relations? It seems as if one can only use whenLoaded with the first relation depth like this:
'season' => $this->whenLoaded('origin', function () {
return new SeasonResource($this->origin->season);
}),
But then Laravel, if origin is loaded, but not season, Laravel will load it, which creates an N+1 problem. Both origin and season relationships are conditional, and not always used. Therefor I want to use something like this:
$this->whenLoaded('origin.season', ...)
or this:
'season' => $this->whenLoaded('origin', function () {
return new SeasonResource($this->origin->whenLoaded('season'));
}),
Neither of these work. I suppose the deeper relations are not stored on the model itself, and in the second case, whenLoaded() does not exists on the query builder.
How do I use whenLoaded() for deeper than one level relations?

I believe the reason this is not implemented is because it only makes sense for hasOne and belongsTo relationships (relationships that return an object, not a collection).
But if that is the case for you, you could do this:
'season' => $this->when(
$this->relationLoaded('origin') &&
$this->origin->relationLoaded('season'),
function () {
return new SeasonResource($this->origin->season);
}
),
Basically, use ->when instead of ->whenLoaded and manually check if the relation is loaded using the ->relationLoaded public method on the model.

For anyone who comes across this and looking for many to many relationships, I've found a solution.
Let's say you've something like
User ---> user_lived_cities ----> city
with table structures as
user_lived_cities
user_id | city_id
------ | ------
cities
id | name
------ | ------
You want to load the cities the user lived in with something like
User::with('livedCities.city')->take(n)->get();
In the resource, you can manipulate the values like this
$livedCityRelation = $this->whenLoaded('livedCities');
$livedCities = $this->when(!empty((array)$livedCityRelation), function () use ($livedCityRelation) {
if ($livedCityRelation instanceof MissingValue) return []; // the relation has no value
$relationLoaded = count($livedCityRelation) /* Remember: It's a collection */ && $livedCityRelation[0]->relationLoaded('city');
return $relationLoaded ? $livedCityRelation->pluck('city') : []; // you can manipulate as per you need. I'm just extracting the city relation from here
});
In your resource in toArray method, you can return
return [
...,
'lived_cities' => City::collection($livedCities),
];
Remember, $livedCities is basically a collection at this point and you can manipulate it as you wish.
If I want to just return an array of city names, I can do that as well with this
$livedCities->pluck('name')
You can apply any collection method that you want to manipulate as per your wish.
Don't forget to import this at the top
use Illuminate\Http\Resources\MissingValue;

You can check for each nested relationship in turn. For example:
$postsLoaded = $user->relationLoaded('posts');
$commentsLoaded = $user->posts->first()->relationLoaded('comments');
$likesLoaded = $user->posts->first()->comments->first()->relationLoaded('likes');
This isn't my solution but it solved my problem - posting so it may help others as this was hard to find. All credit goes to SaeedPrez:
https://laracasts.com/discuss/channels/eloquent/this-relationloaded-for-distant-relationships

Related

Getting the rows created before the selected one

I was wondering about the best way to get the count of all the rows created before the selected one. Right now I have defined an accessor that looks like this:
// In the model
public function getPositionAttribute() {
return self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
// In the code
$model->position
It works correctly, but I'm worried about 2 things:
Is it a bad practice to call self on the model? Looks somehow off to me.
When called in a foreach this obviously generates a query for each element which is far from optimal. Is there any way to refactor this so that it can be eager loaded in a single query?
Bonus: I have totally discarded the idea of keeping a column with some kind of index because that initially sounded impossible to maintain, eg. when a record is deleted all the others should somehow shift position. Should I reconsider it? Is there a better way?
Pretty sure that using self here is the "best practice" because that is how that keyword was designed to be used.
In regards to refactoring, i personally can't think of optimizing the query as is but instead you could create a function that preloads all the position then use it normally. Assuming your model has a unique key 'id' and you are passing in a collection of model then, you can try something like this:
public static function populateOrderPositions($modelCollection){
// Optimize this query to include your "other condition"
$idCollection = Model::orderBy('created_at') // this will make it in the order of creation
->pluck('id'); // this will only retrieve the id field
// This array will contain an array with the model object ids as key and a numeric position (starts at 0)
$positionArr = $idCollection->flip()->all();
// Then just load all the position into the object and return it.
return $modelCollection->map(function($modelObj) use ($positionArr){
return $modelObj->position = $positionArr[$modelObj->id] + 1; // +1 because array key starts at 0
};
}
You would also need to adjust your attribute code to use the loaded attribute instead of ignoring the loaded attribute like so:
public function getPositionAttribute() {
return $this->attributes['position'] ?? self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
With these changes, you can then prepopulate the position then use it afterward without the need to query the database.
These code are untested as i don't know how your model and query will be structured and is more of an example. Also you would need to compare the performance vs your original code.

Laravel 5. findOrNew for relationships

Just wondering if it is possible that some kind of findOrNew for relationships exist in Eloquent (in case if relationship do not exist attach new model instance)?
What that mean:
Lets say that we have devices and specifications tables. Device belongs to specification. Specification_id is an FK (Know that is not best approach, but I have something like this left by previous programmer). Under id 11 we have device that do not have specification but we have to display that for user anyway.
$device = Device::find(11);
echo $device->specification->cpu;
In this case it will throw an error because specification will be null - it do not exist for device with id 11.
Know that I could check first if it exist but there a a lot of similar lines and app is pretty big. I need to move it from Kohana to Laravel. It works in Kohana because empty object is loaded then and 2nd line just return null. For Laravel I can just check if relationship exist and load new model then but I am curios if maybe there is any other and better way?
I would go for creating extra method in Device model this way:
public function getSpecification()
{
if ($device->specification) {
return $device->specification;
}
return Specification::find(20); // some default specification
// or
// return new Specification(['cpu' => 'Not provided']);
}
And now you could use it this way:
$device = Device::find(11);
$device->getSpecification()->cpu;
Of course it depends how would you need to use it. If you have many properties, you should run this method just once for object to not run multiple queries and in case you would use it for big collections you should also rethink improvements to lower database queries.
This doesn't quite create the related object as you requested, but for the purposes of outputting the data or replicating Kohana's null output in the absence of a related model, I tend to use the data_get() or object_get() helpers for this purpose.
$device = Device::find(11);
echo object_get($device->specification, 'cpu');
// You could probably do this too (untested)
echo object_get($device, 'specification.cpu');
Having had a bit of a look, you can override the getRelationshipFromMethod() method in Illuminate\Database\Eloquent\Model
protected function getRelationshipFromMethod($method)
{
// Different relationships return different types of data so
// tweak this as necessary. In theory you only care if the relationship
// type is a single entity rather than a collection.
$results = parent::getRelationshipFromMethod($method);
if ($results instanceOf Illuminate\Database\Eloquent\Collection) {
return $results;
}
// Generate a null value for any missing attributes
// PHP7 anonymous class. Return a real class < 7.0
return $this->relations[$method] = new class {
public function __get($attribute) {
return null;
}
};
// Or perhaps actually create a relationship with a specification
$this->relations[$method] = Specification::where('default', true)->first();
$this->specification()->associate($this->relations[$method]);
return $this->relations[$method];
}

Laravel - Dynamic relationship using hasManyThough() and unique merge

I can think of several ad-hoc ways to do this, but I'm really looking for a 'best practices' type of solution.
I have 3 tables involved
- users (user_id)
- usages ('user_id', 'provider_id', 'nurse_id', 'patient_id')
- usage_alerts ('usage_id')
Im trying to eager load alerts using hasManyThrough() based on a user's role.
The user_id field is agnostic, and can apply to any role, so merging and filtering needs to take place.
Using $this->hasManyThrough('UsageAlert', 'Usage')->get() will return a collection, making the ->merge() method available. However, when eager loading, on return, i get an error since it's a collection object.
Call to undefined method Illuminate\Database\Eloquent\Collection::addEagerConstraints()
For example, this is my current relation (returns the error above)
public function alerts()
{
$alerts = $this->hasManyThrough('UsageAlert', 'Usage')->get();
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('provider')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'provider_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('patient')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'patient_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('nurse')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'nurse_id'));
return $alerts;
}
Any suggestions? Pperhaps too much complexity for a relationship?
Best practice manipulates the relationship, though official documentation on how lacks. For your scenario, you can union the additional queries into the primary "agnostic" relationship:
$relation = $this->hasManyThrough('UsageAlert', 'Usage');
foreach (['provider','patient','nurse'] as $group) {
if (Sentry::getUser()->inGroup(Sentry::findGroupByName($group))) {
$relation->getBaseQuery()->union(
$this->
hasManyThrough('UsageAlert', 'Usage', $group . '_id')->
getBaseQuery()->
select('UsageAlert.*') // limits union to common needed columns
);
}
}
return $relation;
This approach returns a Relation, rather than a Collection, as would be expected by API users.

Laravel belongsToMany to return specific columns only

So I have a many to many relationship between Users and Photos via the pivot table user_photo. I use belongsToMany('Photo') in my User model. However the trouble here is that I have a dozen columns in my Photo table most I don't need (especially during a json response). So an example would be:
//Grab user #987's photos:
User::with('photos')->find(987);
//json output:
{
id: 987,
name: "John Appleseed",
photos: {
id: 5435433,
date: ...,
name: 'feelsgoodman.jpg',
....// other columns that I don't need
}
}
Is it possible to modify this method such that Photos model will only return the accepted columns (say specified by an array ['name', 'date'])?
User.php
public function photos()
{
return $this->belongsToMany('Photo');
}
Note: I only want to select specific columns when doing a User->belongsToMany->Photo only. When doing something like Photo::all(), yes I would want all the columns as normal.
EDIT: I've tried Get specific columns using "with()" function in Laravel Eloquent but the columns are still being selected. Also https://github.com/laravel/laravel/issues/2306
You can use belongsToMany with select operation using laravel relationship.
public function photos()
{
return $this->belongsToMany('Photo')->select(array('name', 'date'));
}
Im assuming you have a column named user_id. Then you should be able to do the following:
public function photos()
{
return $this->belongsToMany('Photo')->select(['id', 'user_id', 'date', 'name']);
}
You have to select, the foreign key, else it will have no way of joining it.
Specifying the exact columns you want for the Photos relationship will likely end up biting you in the butt in the future, should your application's needs ever change. A better solution would be to only specify the data you want to return in that particular instance, i.e. the specific JSON response you're delivering.
Option 1: extend/overwrite the toArray() Eloquent function (which is called by toJson()) and change the information returned by it. This will affect every call to these methods, though, so it may end up giving you the same problems as doing select() in the original query.
Option 2: Create a specific method for your JSON response and call it instead of the general toJson(). That method could then do any data building / array modifications necessary to achieve the specific output you need.
Option 3: If you're working with an API or ajax calls in general that need a specific format, consider using a library such as League/Fractal, which is built for just such an occasion. (Phil is also working on a book on building APIs, and it doesn't suck.)

Laravel 4 - Eloquent way to attach a where clause to a relationship when building a collection

This may be a dupe but I've been trawling for some time looking for a proper answer to this and haven't found one yet.
So essentially all I want to do is join two tables and attach a where condition to the entire collection based on a field from the joined table.
So lets say I have two tables:
users:
-id
-name
-email
-password
-etc
user_addresses:
-address_line1
-address_line2
-town
-city
-etc
For the sake of argument (realising this may not be the best example) - lets assume a user can have multiple address entries. Now, laravel/eloquent gives us a nice way of wrapping up conditions on a collection in the form of scopes, so we'll use one of them to define the filter.
So, if I want to get all the users with an address in smallville, I may create a scope and relationships as follows:
Users.php (model)
class users extends Eloquent{
public function addresses(){
return $this->belongsToMany('Address');
}
public function scopeSmallvilleResidents($query){
return $query->join('user_addresses', function($join) {
$join->on('user.id', '=', 'user_addresses.user_id');
})->where('user_addresses.town', '=', 'Smallville');
}
}
This works but its a bit ugly and it messes up my eloquent objects, since I no longer have a nice dynamic attribute containing users addresses, everything is just crammed into the user object.
I have tried various other things to get this to work, for example using a closure on the relationship looked promising:
//this just filters at the point of attaching the relationship so will display all users but only pull in the address where it matches
User::with(array('Addresses' => function($query){
$query->where('town', '=', 'Smallville');
}));
//This doesnt work at all
User::with('Addresses')->where('user_addresses.town', '=', 'Smallville');
So is there an 'Eloquent' way of applying where clauses to relationships in a way that filters the main collection and keeps my eloquent objects in tact? Or have I like so many others been spoiled by the elegant syntax of Eloquent to the point where I'm asking too much?
Note: I am aware that you can usually get round this by defining relationships in the other direction (e.g. accessing the address table first) but this is not always ideal and not what i am asking.
Thanks in advance for any help.
At this point, there is no means by which you can filter primary model based on a constraint in the related models.
That means, you can't get only Users who have user_address.town = 'Smallwille' in one swipe.
Personally I hope that this will get implemented soon because I can see a lot of people asking for it (including myself here).
The current workaround is messy, but it works:
$products = array();
$categories = Category::where('type', 'fruit')->get();
foreach($categories as $category)
{
$products = array_merge($products, $category->products);
}
return $products;
As stated in the question there is a way to filter the adresses first and then use eager loading to load the related users object. As so:
$addressFilter = Addresses::with('Users')->where('town', $keyword)->first();
$users= $addressFilter->users;
of course bind with belongsTo in the model.
///* And in case anyone reading wants to also use pre-filtered Users data you can pass a closure to the 'with'
$usersFilter = Addresses::with(array('Users' => function($query) use ($keyword){
$query->where('somefield', $keyword);
}))->where('town', $keyword)->first();
$myUsers = $usersFilter->users;

Categories