I am trying to eager-load a collection multiple levels deep and then go through that collection and let it forget parts of it.
My problem is that the foreach loops here just do not trigger at all and dd() on certain levels just gives me a relationship instead of collection elements.
I also know that my approach is probably not the best of doing something like this. I would be happy to learn a better way :)
So what I have is data similar to the following:
MainChannel->GroupChannel->Channel->Activities
OnlineChannels->SocialMedia->Youtube->Activities
OnlineChannels->SocialMedia->Facebook->Activities
OnlineChannels->Webpages->Something.com->Activities
OfflineChannels->Face-To-Face->Congress->Activities
The Channels are nested in a tree-structure and can go 3 levels deep. They have a lft rgt and depth attribute.
id | name | lft | rgt | depth | color | important_links | parent_id
So first I try to eager load the whole thing:
$GroupChannels = Channel::where('depth', 2)->with(
'parent',
'children',
'children.activities'
);
and maybe filter it by
if(isset($input['filtered_channels'])){
$Channels = $Channels->whereIn('id', $input['filtered_channels']);
}
$Channels = $Channels->get();
and then I want to go through it like this:
foreach($GroupChannels as $Channel){
foreach($Channel->children as $Subchannel){
foreach ($Subchannel->activities as $Activity){
$removeActivity = false;
// do some checks on the activity that will maybe turn the $removeActivity to true
if($removeActivity){
$Subchannel->forget($Activity);
}
}
//forget Subchannel if not Activities remain
}
//forget Channel if not Subchannels remain
}
Here is my model:
class Channel extends Model
{
use CrudTrait;
use SoftDeletes;
use \Venturecraft\Revisionable\RevisionableTrait;
/*
|--------------------------------------------------------------------------
| GLOBAL VARIABLES
|--------------------------------------------------------------------------
*/
protected $table = 'channels';
protected $primaryKey = 'id';
protected $fillable = ['name', 'color', 'parent_id', 'lft', 'rgt', 'depth', 'important_links'];
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
/*
|--------------------------------------------------------------------------
| FUNCTIONS
|--------------------------------------------------------------------------
*/
public static function boot()
{
parent::boot();
}
/*
|--------------------------------------------------------------------------
| RELATIONS
|--------------------------------------------------------------------------
*/
public function activities()
{
return $this->belongsToMany(Activity::class);
}
public function children()
{
return $this->hasMany(Channel::class, 'parent_id', 'id');
}
public function parent()
{
return $this->hasOne(Channel::class, 'id', 'parent_id');
}
/*
|--------------------------------------------------------------------------
| ACCESORS
|--------------------------------------------------------------------------
*/
public function getChildrenAttribute(){
return $this->children()->orderBy('lft');
}
}
The problem occurs on the second level foreach($Channel->children as $Subchannel){ because this does not trigger.
When I do a dd($Subchannel) here it does not show up and if I do it above that line for dd($Channel->children) I get a relation instead of a collection.
HasMany {#872 ▼
#foreignKey: "channels.parent_id"
#localKey: "id"
#query: Builder {#831 ▶}
#parent: Channel {#840 ▶}
#related: Channel {#832 ▶}
}
So I guess the eager-load does not return nested collections or I can not traverse through the tree in that collection like that. I have honestly no idea how to do this the correct way.
I cannot review all the logic, but for sure problem might be with because of using getChildrenAttribute
. You should never create accessor/mutator with the same name as relationship because if you write:
$channel->children
it's hard to say what it does:
does it launch relationship (so in fact it's just shortcut for $channel->children()->get()`
or maybe it launches accesssor so it's running $channel->children()->orderBy('lft')
but in fact it doesn't get any data from database.
I believe the problem is this accessor because it doesn't return any data from relationship - it just returns relationship, so to really get data, then you should use something like this:
$channel->children->get()
but again it won't reuse eager loaded children
relationship.
For ordering you should rather use scopes or relationships with different names. For example you could have 2 relationships:
public function children()
{
return $this->hasMany(Channel::class, 'parent_id', 'id');
}
public function orderedChildren()
{
return $this->children()->orderBy('lft')
}
and then in your code you can just eager load like this:
$GroupChannels = Channel::where('depth', 2)->with(
'parent',
'orderedChildren',
'orderedChildren.activities'
);
or even simpler it's enough to eager load like this:
$GroupChannels = Channel::where('depth', 2)->with(
'parent',
'orderedChildren.activities'
);