Laravel Eloquent Relationships for Siblings - laravel

Relationship for Siblings are many to many self relationship. So, for many to many self relationship, we can define two functions in the model:
public function siblings()
{
return $this->belongsToMany('Student', 'student_sibling', 'student_id', 'sibling_id');
}
public function siblingOf()
{
return $this->belongsToMany('Student', 'student_sibling', 'sibling_id', 'student_id');
}
The first one returns the students who are siblings of the student. The reverse is also true for siblings. So, the second one returns the students of whom the student is a sibling.
So, we can merge both the collections to get a list of students who are siblings of the student. Here is my code in the controller method:
$siblingOf = $student->siblingOf;
$siblings = $student->siblings;
$siblings = $siblings->merge($siblingOf);
But there is more. Siblings relationship is a chain relationship unlike friends relationship. This mean, if X is a sibling of Y and Y is a sibling of Z, then Z is a sibling of X.
So, how to get the collection of all students who are sibling of a student?

I'm assuming that you have a Student model, with student_id as your primary key, and a pivot table that looks something like this:
+---------+------------+------------+
| id (PK) | student_id | sibling_id |
+---------+------------+------------+
| 1 | 1 | 2 |
| 2 | 2 | 1 |
| 3 | 3 | 4 |
| 4 | 4 | 2 |
| 5 | 6 | 5 |
+---------+------------+------------+
In this example, we want to be able to discern that 1, 2, 3 and 4 are all siblings of each other. 5 and 6 are siblings as well.
One way to solve this is by gathering the result of the two belongsToMany() relationships in your model—as you did in your question—and then recursively iterating over the result, continuing to check the belongsToMany() functions until we run out of siblings.
In the Student model, define two functions:
public function getAllSiblings(){
// create a collection containing just the first student
$this->siblings = $this->newCollection()->make($this);
// add siblings (recursively) to the collection
$this->gatherSiblings($this->student_id);
// OPTIONAL: remove the first student, if desired
$this->siblings->shift();
return($this->siblings);
}
private function gatherSiblings($student_id){
$checkStudent = Student::find($student_id);
if ($checkStudent) {
// get related siblings from model, combine them into one collection
$siblingOf = $checkStudent->siblingOf()->get();
$siblings = $checkStudent->siblings()->get();
$siblings = $siblings->merge($siblingOf);
// iterate over the related siblings
$siblings->each(function($sibling)
{
// if we've found a new sibling who's
// not already in the collection, add it
if(!$this->siblings->contains($sibling->student_id)) {
$this->siblings->push($sibling);
// look for more siblings (recurse)
$this->gatherSiblings($sibling->student_id);
};
});
return;
}
}
In your controller, find the initial student, and then call getAllSiblings() from Student:
$student = Student::find($id);
$siblings = $student->getAllSiblings();
The result is a collection with all the siblings of the original student. So, if you run this for student 1, you will get a collection containing students 2, 3 and 4. (If you'd prefer to keep the original student as part of the siblings collection, so that running this for 1 returns 1, 2, 3 and 4, simply remove the optional step in getAllSiblings().)
From there, you can cast the collection to an array, or sort, etc. as needed.

Recursive relation could do this, BUT it will probably cause infinite loop (function nesting limit error).
Anyway this is how to setup such relation:
public function siblingsRecursive()
{
return $this->siblings()->with('siblingsRecursive');
}
public function siblings()
{
return $this->belongsToMany('Student', 'siblings');
}
Then you call, as simple as this:
$students = Student::with('siblingsRecursive');

I think what you might want is this:
public function siblings() {
return $this->hasMany(self::class, 'parent_id', 'parent_id');
}

Related

In Laravel I'm attempting to get the info from a table with a pivot

I currently have 3 tables:
medias, colors, media_colors
media_colors is the pivot table I'm trying to use it contains
media_id | color_id
------------------
1 | 1
in colors table I have
id | front
----------
1 | color
My model for medias contains:
public function mediaColors()
{
return $this->belongsToMany(Color::class, 'media_colors');
}
In a controller I attempt to query front like so:
$m = Media::find($estimate->media_id);
dd($m->mediaColors[0]->pivot->front);
This returns null. How would I be able to access this?
You can access the colors directly from the relation collection as marked in the comment.
$m = Media::find($estimate->media_id);
foreach($m->mediaColors as $color) {
//you can access all the colors here
$fronts[] = $color->front;
}
//or if you only want the first one (if it exists)
$m->mediaColors[0]->front
one easier way to get just the first value if you only need that
$m = Media::find($estimate->media_id);
$front = $m->mediaColors()->value('front');
// this will return null if no relation found without triggering an exception

laravel sum of property in parent *and* children models?

I have Places that get appear in Timelines. These places can be grouped: a parent Place can have children Places (only one level deep).
Would like to lazy load the Places including visit counts of that Place and its children, and order by total count.
It's too many places to get 'em all up front and do the sum and sort afterwards.
Simplified example of the result I am trying to get:
- place B - visits 5, visits children 6, total visits 11
- place B.1 - visits 2
- place B.2 - visits 4
- place A - visits 5, visits children 0, total visits 5
- place C - visits 1, visits children 2, total visits 3
- place C.1 - visits 2
With the code below I get all of that but the "total visits".
I am using straightforward relationships for the Place model:
public function children(){
return $this->hasMany(Place::class, 'parent_id');
}
public function visits(){
return $this->hasMany(Timeline::class);
}
public function visitsChildren(){
return $this->hasManyThrough(
Timeline::class, // the end result
Place::class, // going through this one
'parent_id', // foreign key on Place (child)
'place_id', // foreign key on Timeline
'id', // local key on Place (parent)
'id' // local key on Place (child)
);
}
I can get the visit count for that Place and the children and their visit counts.
\App\Models\Place::where(...)
->withCount('visits', 'visitsChildren')
->with('children', fn($q) => $q->withCount('visits'))
->orderByDesc('visits_count')
->get()
But how to add them up and sort by the total (without eager loading)?
I.e. can I "merge" visits and visitsChildren?
Tried this but that does not work because column visits_count and visits_children_count are (understandably) not available at that level:
...
->select(DB::raw('visits_count + visits_children_count as visits_including_children_count'))
->get()
Update: added the relationship for visitsChildren and the DB::raw attempt.
You can use map() and sum() methods of laravel collection and solve this
$result = \App\Models\Place::where(...)
->withCount('visits')
->with('children', fn($q) => $q->withCount('visits'))
->orderByDesc('visits_count')
->get();
$count = $result->map(function ($item) {
$children_count = $item->children->sum(function ($child) {
return $child->visits_count ?? 0;
});
return $children_count + $item->visits_count;
});
dd($count);

Eloquent - Detect multiple column duplicate data

I have a table that has 3 column id, sub_id, name. That is a pretty big table and there are some duplicates.
What is the best way to detect the duplicates so that I can remove them?
I tried this but it returns everything (I guess thinking ids are making them non-unique)
$collection = \App\MyModel::all();
$colUnique = $collection->unique(['name', 'sub_id']);
$dupes = $collection->diff($colUnique);
I want to get the models that has same name and sub_id.
id sub_id name
1 2 John
2 2 John <- duplicate
3 2 Robin <- unique
My best bet would be DB::Query.
Step 1: Fetch data by group by
$uniqueData = DB::table('TABLE_NAME')
->groupBy(['sub_id', 'name'])
->select('id')
->toArray();
Step 2: Delete duplicate record.
$noOfDeletedRecords = DB::table('TABLE_NAME')
->whereNotIn($uniqueData)
->delete();
Benefits:
1. Only 2 Queries
2. Better performance over collection.
You can utilize Collection.groupBy method.
$collection = \App\MyModel::all();
$collection
// Group models by sub_id and name
->groupBy(function ($item) { return $item->sub_id.'_'.$item->name; })
// Filter to remove non-duplicates
->filter(function ($arr) { return $arr->count()>1; })
// Process duplicates groups
->each(function ($arr) {
$arr
// Sort by id (so first item will be original)
->sortBy('id')
// Remove first (original) item from dupes collection
->splice(1)
// Remove duplicated models from DB
->each(function ($model) {
$model->delete();
});
})

How to format array with relations parent-child?

I have the typical structure of table in MySQL.
id | parent_id | name | object_id
1 0 G 1
2 1 T 1
3 1 R 1
How to build result array with values of parent/child when I select data by object_id?
If its laravel and the parent and the object are both Eloquent models you should be able to do something like this:
class Parent{
public function children(){
return $this->belongsToMany('App\Object', 'your_table', 'parent_id, 'object_id');
}
}
And then the child object class:
class object{
public function parent(){
return $this->belongsToMany('App\Parent', 'your_table', 'object_id, 'parent_id');
}
}
Look into eloquent relationships here: Eloquent relations
If its any other way you are looking for the comments above surely might help you in the correct direction.

Self referencing "ownerless" many_many relationships?

I have a many many relationship where I don't care who created the relationship, if the querying models ID is in either side of the relationship I need the other side.
--
Let's say I have this model
class Ingredient extends Model
{
public function complements()
{
return $this->belongsToMany(
self::class, 'ingredient_complements',
'ingredient_id', 'complement_id'
);
}
}
And I've created a few different instances as below
$ing1 = new Ingredient();
$ing2 = new Ingredient();
$ing3 = new Ingredient();
$ing4 = new Ingredient();
$ing5 = new Ingredient();
$ing6 = new Ingredient();
$ing1->save(); //2,3,4,5,6...
Next I go through and relate a few of them
$ing1->complements()->attach($ing2);
$ing2->complements()->attach($ing3);
$ing3->complements()->attach($ing4);
$ing4->complements()->attach($ing6);
$ing5->complements()->attach($ing6);
$ing6->complements()->attach($ing1);
So now we have a pivot table that looks like
| ing_id | cmp_id |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 5 |
| 5 | 6 |
| 6 | 1 |
So..
if I call $ing1->complements I'll get back ing2
if I call $ing2->complements I'll get back ing3
But..
if I call $ing2->complements I won't get back ing1
if I call $ing3->complements I won't get back ing2
I need to fix that.
I've got a second method that I'm using at the moment that I can call using $ing3->complementaryIngredients; which ends up executing something like
select * from `ingredients`
inner join `ingredient_complements` on `ingredient_id` = '3'
or `complement_id` = '3' and `ingredient_id` = `id`
or `complement_id` = `id` where `id` <> '3'
Which works, but I don't feel this is something that I should have to break out of the ORM for?
public function getComplementaryIngredientsAttribute()
{
return self::join('ingredient_complements', function(JoinClause $join){
$join->where('ingredient_id', $this->id);
$join->orWhere('complement_id', $this->id);
$join->on('ingredient_id', '=', 'id');
$join->orOn('complement_id', '=', 'id');
})->where('id', '<>', $this->id)->get();
}
You actually need to define bi-directional relationship, so your model should have 1 more method which will define the opposite direction.
public function ingredients()
{
return $this->belongsToMany(
self::class, 'ingredient_complements',
'complement_id', 'ingredient_id'
);
}
Then you will get the other side of your relation in this way $ing2->ingredients() and it will return what you are looking for.
Another approach is to create model for your pivot table so you can query that model to get both sides of your relationship

Resources