Laravel one to many results empty after inserts - laravel

I have this relationship:
class UserWeek extends BaseModel
{
public function userDays()
{
return $this->hasMany(UserDay::class, 'week_id');
}
}
I have this method inside my model:
public function days($userWeek)
{
if ($userWeek->userDays->count() == 0) {
$date = $this->start;
$carbon = Carbon::createFromFormat('Y-m-d', $date);
while ($date <= $this->end) {
UserDay::insert([
'week_id' => $userWeek->id,
'date' => $date,
]);
$date = $carbon->addDay()->format('Y-m-d');
}
}
return $userWeek->userDays;
}
This method is supposed to return the days which belongs to a week.
BUT at the first time, there will be no days, so it will create them.
I check the database after execution of this method, days are really created.
STRANGELY this method returns an empty collection, which is the initial content of $userWeek->userDays
I think I understand the cause here, $userWeek->userDays seems to be kind of cached when I call it first. YET I don't have the solution to this problem.
I would appreciate a clean explanation and solution.

I think you're correct
if ($userWeek->userDays->count() == 0)
In there you load the relation and then count(), so this $userWeek->userDays will be a collection. That way of querying is call lazy/dynamic loading in laravel's doc.
You can get the count from a query instead of the collection.
if ($userWeek->userDays()->count() == 0)
The idea on this one is that all types of eloquent relationships serve as queries (Querying Relations).
Reload the collection
$userWeek->load('userDays');
return $userWeek->userDays;
Return a fresh call of the relationship
return $userWeek->userDays()->get();

Related

Laravel hasOne()->orderBy() vs hasOne()->ofMany()

One bit of "magic" in the Laravel codebase is here in HasOneOrMany.php where if the underlying query behind a hasOne relationship technically fetches multiple rows, it will just return the first row instead.
protected function getRelationValue(array $dictionary, $key, $type)
{
$value = $dictionary[$key];
return $type === 'one' ? reset($value) : $this->related->newCollection($value);
}
This got me thinking that, given a related model that is normally hasMany, an oldestOfMany() relation can be simulated with just an orderBy, e.g.
public function models()
{
return $this->hasMany(Model::class);
}
public function firstModel()
{
return $this->hasOne(Model::class)->orderBy('created_at');
}
Are there any downsides to this shortcut approach?
As pointed out in the comments, calling ->first() on the many relationship works just fine for actually retrieving a model. In our app it's common to build relations from other relations, and I want to do that here. e.g.
public function firstActiveModel()
{
return $this->firstModel()->active();
}

Get max count from laravel eloquent relationship

I have a model Topic which has hasMany relationship with Game model.
//Topic.php
public function Games() {
return $this->hasMany(Game::class);
}
//Game.php
public function Topic() {
return $this->belongsTo(Topic::class);
}
What I need is getting the Topic which has the max count (also the count value) of Games based on 'created_at' today,this week,this month etc. I tried the following code, but the nested whereBetween query does not work, instead it is showing all the related topic->games created ever
$top_topic_month = Topic::with('games')->get()->sortBy(function($q)
{
return $q->games->whereBetween('created_at',[today()->startOfMonth, today()])->count();
})->first();
$top_topic_month_count = ?
try this
use withCount to generate games_count
$top_topic_month = Topic::withCount('games')->whereHas('games',function($q) use($startMonth){
$q->whereBetween('created_at', [$startMonth, today()]);
})->orderByDesc('games_count')->first();
$top_topic_month_count = $top_topic_month->count()

Eager load for collections on the fly

I have this, user has many to many property via pivot property_users.
I am making somehow reusable classes in my webapp.
These are the models with their eager loading functions:
//User model
public function properties()
{
return $this->belongsToMany(Property::class, 'property_users', 'user_id', 'property_id');
}
//Property model
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'property_id', 'id');
}
//PropertyUser model
public function user()
{
return $this->belongsTo(User::class);
}
//GetProperties class
public function handle()
{
return auth()->user()->properties()->get();
}
//somewhere in a feature
$properties = $this->run(GetProperties::class);
//this returns valid properties under the logged in user
I now need to get the chat_username in property_users that belongs to this user
I manage to make it work if I loop through the properties and then doing it on the fly.
$properties = $properties->map(function($property) {
$propertyUsers = $property->property_users()->get();
$chatUsername = null;
foreach($propertyUsers as $propertyUser) {
if($propertyUser->property_id == $property->id) {
$chatUsername = $propertyUser->chat_username;
}
}
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $chatUsername
];
});
But I am trying to reduce query on loop to reduce hits especially when they are on multiple properties in the database.
The other way is that I can add the property_users in the eager loading under GetProperties class by updating it to:
$query = Property::query();
$query->with(['property_users']);
$query->whereHas('property_users', function($qry) {
$qry->where('user_id', Auth::user()->id);
});
$properties = $query->get();
return $properties;
But I do not want to rely on adding more eager loading to the original GetProperties class as the GetProperties will get fat and I do not really need those data (let's say adding property_invoices, property_schedules, etc but not really needing it in some area).
Rather, I want to do the eager loading on the fly but with a twist! This is how I would imagine it:
Collect all the ids from the properties, do the fetch using wherein and apply all the users to the properties in a single query. This way it will be even more beautiful.
Maybe something like this: (using the original GetProperties class)
$properties = $this->run(GetProperties::class);
//this does not work. The error is: Method Illuminate\Database\Eloquent\Collection::property_users does not exist.
$property->property_users = $properties->property_users()->get();
Would be great if someone can show me how to do it.
What about eager loading only the fields you actually need?
$query->with('property_users:id,user_id');
The model will not get fat, and you will not need to do separate queries in the loop.
It is documented in the official documentation: https://laravel.com/docs/5.8/eloquent-relationships#eager-loading , see Eager Loading Specific Columns
Edit: if you want to perform the query after the GetProperties class, you will need to collect all the ids and perform a second query. I honestly don't like this second approach, because it is far slower, less performant and I consider it less elegant then adding a single line in the GetProperties class, but it is gonna work:
$properties = $this->run(GetProperties::class);
$ids = $properties->pluck('id'); // Get all the ids as an array
$propertyUsers = propertyUsers::whereIn('property_id', $ids)->get(); // Get the propertyUsers model
foreach($properties as $property) {
$property->property_users = $propertyUsers->where('property_id', $property->id); // Not even sure you can do that, proerty_users may not been writable
}
Alright, after reading here and there.
I found this article: https://laravel-news.com/eloquent-eager-loading
The solution is really nice. Simply:
$properties = $this->run(GetProperties::class);
//lazy load
$properties->load('property_users');
$properties = $properties->map(function($property) {
$user = $property->property_users->first(function($user) {
if($user->user_id == Auth::user()->id) {
return $user;
}
})->only('chat_username');
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $user['chat_username']
];
});
After checking query logs:
//auth query done by middleware
[2019-05-21 07:59:11] local.INFO: select * from `users` where `auth_token` = ? or `temporary_auth_token` = ? limit 1 ["token_removed_for_security_purpose","token_removed_for_security_purpose"]
//These are the queries made:
[2019-05-21 07:59:11] local.INFO: select `properties`.*, `property_users`.`user_id` as `pivot_user_id`, `property_users`.`property_id` as `pivot_property_id` from `properties` inner join `property_users` on `properties`.`id` = `property_users`.`property_id` where `property_users`.`user_id` = ? [8]
[2019-05-21 07:59:11] local.INFO: select * from `property_users` where `property_users`.`property_id` in (2, 4)
This way, I can keep my GetProperties as small as possible, then just lazy load it whereever I need it.

Laravel oneToMany accessor usage in eloquent and datatables

On my User model I have the following:
public function isOnline()
{
return $this->hasMany('App\Accounting', 'userid')->select('rtype')->latest('ts');
}
The accounting table has activity records and I'd like this to return the latest value for field 'rtype' for a userid when used.
In my controller I am doing the following:
$builder = App\User::query()
->select(...fields I want...)
->with('isOnline')
->ofType($realm);
return $datatables->eloquent($builder)
->addColumn('info', function ($user) {
return $user->isOnline;
}
})
However I don't get the value of 'rtype' for the users in the table and no errors.
It looks like you're not defining your relationship correctly. Your isOnline method creates a HasMany relation but runs the select method and then the latest method on it, which will end up returning a Builder object.
The correct approach is to only return the HasMany object from your method and it will be treated as a relation.
public function accounts()
{
return $this->hasMany('App\Accounting', 'userid');
}
Then if you want an isOnline helper method in your App\User class you can add one like this:
public function isOnline()
{
// This gives you a collection of \App\Accounting objects
$usersAccounts = $this->accounts;
// Do something with the user's accounts, e.g. grab the last "account"
$lastAccount = $usersAccounts->last();
if ($lastAccount) {
// If we found an account, return the rtype column
return $lastAccount->rtype;
}
// Return something else
return false;
}
Then in your controller you can eager load the relationship:
$users = User::with('accounts')->get(['field_one', 'field_two]);
Then you can do whatever you want with each App\User object, such as calling the isOnline method.
Edit
After some further digging, it seems to be the select on your relationship that is causing the problem. I did a similar thing in one of my own projects and found that no results were returned for my relation. Adding latest seemed to work alright though.
So you should remove the select part at very least in your relation definition. When you only want to retrieve certain fields when eager loading your relation you should be able to specify them when using with like this:
// Should bring back Accounting instances ONLY with rtype field present
User::with('accounts:rtype');
This is the case for Laravel 5.5 at least, I am not sure about previous versions. See here for more information, under the heading labelled Eager Loading Specific Columns
Thanks Jonathon
USER MODEL
public function accounting()
{
return $this->hasMany('App\Accounting', 'userid', 'userid');
}
public function isOnline()
{
$rtype = $this->accounting()
->latest('ts')
->limit(1)
->pluck('rtype')
->first();
if ($rtype == 'Alive') {
return true;
}
return false;
}
CONTROLLER
$builder = App\User::with('accounting:rtype')->ofType($filterRealm);
return $datatables->eloquent($builder)
->addColumn('info', function (App\User $user) {
/*
THIS HAS BEEN SUCCINCTLY TRIMMED TO BE AS RELEVANT AS POSSIBLE.
ARRAY IS USED AS OTHER VALUES ARE ADDED, JUST NOT SHOWN HERE
*/
$info[];
if ($user->isOnline()) {
$info[] = 'Online';
} else {
$info[] = 'Offline';
}
return implode(' ', $info);
})->make();

Multiple relationships on same table

In my Property Model I have these two relationships defined
public function images() {
return $this->hasMany('Image')
->where('approved', '=', 1);
}
public function pending_images() {
return $this->hasMany('Image')
->where('approved', '=', 0);
}
In my Controller, I create a Property object and try to fetch both the approved and pending images.
$images = $property->images;
$pending = $property->pending_images;
var_dump($images);
var_dump($pending);
exit;
The $images variable is a Illuminate\Database\Eloquent\Collection as expected.
However, $pending is just NULL!
I try fetching the last DB query, using this answer, and it seems that the pending query is not even being executed. (The last query run has approved = 1, which is for the images relationship.)
My suspicion is that it might be a problem that the relationship is on the same table but I'm stumped.
You need to rename that relation to camelCase:
public function pendingImages() {
return $this->hasMany('Image')
->where('approved', '=', 0);
}
Then it will work as expected, moreover you will be able to access it either way:
$property->pending_images == $property->pendingImages;
Dynamic properties work only with camelCased methods, that's all.

Resources