Using nestedset in Laravel to design a nested treant tree - php

I'm trying to figure out how these nestedsets in Laravel works. I've an many to many relation between organizations and departments. An organization can have many departments. An department can have many departments. For this I'm using Nestedsets.
What I'm trying to do, is retrieving all organizations from a user. On this query I'd like to retrieve all departments attached to these organizations. I'd like the structure, so I've a infinite parent -> child relation on my departments, so I'm able to build a structuretree using treant.js.
I'm pretty sure I've everything build correctly in my database, so my first thought were to use with. However it seems like I'm only getting the first children. Here is an example:
$currentUser->organizations()->with(
'departments.children',
'departments.commodities',
'departments.children.commodities',
)->get()
I've to include children.[model] for every nested department. So if I've two levels, I've to add departments.children.children.commodities, and so on. This seems pretty retarded!
I've been trying pretty many different approches to get a proper solution, but the one below is my best solution for now. I just feel like I'm using the nestedset-library wrong.
public function getUserDepartmentTree() {
foreach ( $this->organizations()->get() as $organization ) {
$dep[] = $organization->departments()->get()->toTree();
}
return $dep;
}
So my question is, how should I get all relational data from my departments tree-structured?

For this you'll want to use descendants instead of children as children will only return the direct/first child models whereas descendants will return everything under a certain node.
Since this will add the relationship as descendants instead of children you'll need to tweak it slightly i.e. change the name of the relationship and then use the toTree() method:
$organizations = $currentUser->organizations()
->with('departments.commodities', 'departments.descendants.commodities')
->get()
->map(function ($organization) {
$organization->departments->map(function ($department) {
return $department->setRelation('children', $department->descendants->toTree())->unsetRelation('descendants');
});
return $organization;
});

Related

get a values using many to many relations on laravel 5 models

i'm really new working with laravel 5.0, so I got this problem when I try to retrieve a result using a model. I have a users table, with a list of users who can be a manager or not, they can have assigned one or more companies, or none, a company table with companies which can have one or many managers, and a pivot table that I called companies_managers. I set up the relations in every model like this:
/***User model***/
public function companies()
{
return $this->belongsToMany('App\Company', 'companies_managers','id', 'manager_id');
}
and the same in Company model
public function managers()
{
return $this->belongsToMany('App\User', 'companies_managers', 'id', 'company_id');
}
I want to get the managers assigned to a company using a company id to get it, but it just gave me an huge object without the values I looking for (the names of the managers assigned to that company). This is the code that I tried:
$managers = Company::find($id)->managers();
I would appreciate any help you can give me
Using ->managers() (with the brackets) doesn't actually return the associated managers, but rather a Builder instance (the "huge object"), which you can then chain with additional parameters before finally retrieving them with ->get() (or another closure, like ->first(), ->paginate(), etc)
Using ->managers (without the brackets), will attempt to access the associated managers, and execute any additional logic to retrieve them.
So, you have 2 options:
$company = Company::with(["managers"])->findOrFail($id);
$managers = $company->managers;
Or
$company = Company::findOrFail($id);
$managers = $company->managers()->get();
Both of those will perform the necessary logic to pull the managers. ->with() and no brackets is slightly more efficient, doing it all in a single query, so bear that in mind.
You just need to split out your code;
// this will find the company based on the id, or if it cannot find
// it will fail so will abort the application
$company = Company::findOrFail($id);
// this uses the active company record and gets the managers based
// on the current company
$managers = $company->managers;
Thank you for your help guys, I solved the issue fixing the relations in the models to this:
return $this->belongsToMany('App\Company', 'companies_managers', 'manager_id', 'company_id');
and this
return $this->belongsToMany('App\User', 'companies_managers', 'company_id', 'manager_id');
The IDs that I had set were not the correct ones for belongsToMany function
And this
$managers = Company::find($id)->managers();
was a problem too, was a dumb mistake of my part. I solved the return of Builder instance using just return instead of dd(), in that way I got the values I looking for. Thanks everyone!

Laravel 5: How to orderBy, depending on whereHas?

I have 2 Laravel models, Classes and Subjects. Each Class belongs to a Subject.
I'd like to do a query, which sorts and returns the classes depending on whether their subject's ID is within a given array of IDs.
E.g. I can use the code below successfully to get the list of classes, which have the relevant subject IDs.
However, I can't figure out how to get ALL classes (i.e. no filter), and just sort so that the classes which have the relevant subjects are first, followed by other classes.
One approach is to create 2 different Collections, and ->merge them, but that then gets tricky when trying to paginate.
Thanks for your help!
Classes::whereHas('subject',function($q) use ($subject_list) {
$q->whereIn('id', $subject_list);
})->get();
You could perform a leftJoin to get all classes (i.e. no filter), and just sort so that the classes which have the relevant subjects are first
Classes::leftJoin('subjects', function ($join) use ($subject_list) {
$join->on('classes.subject_id', '=', 'subjects.id')
->whereIn('subjects.id', $subject_list);
})
->orderBy('classes.subject_id')
->get();
whereHas() will not help you here. You can use sortByDesc():
$classes = Classes::all();
$classes->sortByDesc(function($i) use($subject_list) {
return in_array($i->subject_id, $subject_list);
});
Or you could execute two queries and merge results:
$classes = Classes::whereIn('subject_id', $subject_list)->get();
$classesNotIn = Classes::whereNotIn('subject_id', $subject_list)->get();
$classes->merge($classesNotIn);
I've tested both approaches and they work, but you'll need to create Paginator manually in both cases.

One Nested Set Table for Multiple Users

I apologize if this has been asked before, however I'm wondering if anyone has any insight to the performance of having one nested set table for potentially thousands of users?
I need every registered user to be able to create infinite nested categories. Right now, my structure is a belongsToMany() relation:
users -> user_categories (pivot) -> categories
Users would only have access to their categories, and cannot modify other users categories.
Would there be a massive performance hit for (potentially) tens of thousands of records inside one nested set table? Should each user get their own nested set categories table?
Thanks in advance!
Nested sets allow to fetch all descendants of a node in a single query so for reading categories the performance hit will be similar to the one on a normal (non nested set) table. The drawback of nested sets comes when you are inserting because it requires updating left and right values for all records in the table after the insert.
So the performance hit will depend on how often do you insert and how big your insets are.
You can significantly reduce the overhead if, instead of having a single big tree, you have one root node per user, all stored in the same table, and therefore inserts will involve updating only a small subtree.
If you want to test the performance by yourself you can use the Laravel Baum package and use this seeder to see how inserting 26*10*3 categories perform:
<?php
use App\Category;
use Illuminate\Database\Seeder;
class CategoriesTableSeeder extends Seeder
{
public function run()
{
// Helper function to populate model attributes
$node = function () {
$args = implode(' ', func_get_args());
return ['name' => "Category $args"];
};
// Create first level nodes
foreach(range('A', 'Z') as $letter)
{
$node0 = Category::create($node($letter));
// Create second level nodes
foreach(range(1, 10) as $number)
{
$node1 = $node0->children()->create($node($letter, $number));
// Create third level nodes
foreach(['Δ', 'Σ', 'Ω'] as $greek)
{
$node2 = $node1->children()->create($node($letter, $number, $greek));
}
}
}
}
}

Laravel eloquent query with several layers of model relationships

I'm having some trouble doing eloquent database queries in Laravel. I have searched around a lot, but I don't know exactly what to search for to get my answer.
I'm making a web page in Laravel for kindergardens, and I need to get all children connected to a kindergarden in a single collection/array. The problem is that children are only connected to the kindergarden through their group(s). So the query needs to be able to get all children that are part of any group connected to the kindergarden.
The model relationships are like this:
A kindergarden has many groups, and groups belong to a kindergarden.
A group has many children, and children have many groups.
The best I've managed to do so far is this:
public function getChildren(){
$kid = Session::get('kid');
$k = Kindergarden::find($kid);
$groups = $k->group;
$children;
foreach($groups as $g){
$children = $children->merge($g->children);
}
$children = $children->toArray();
return Response::json($children);
}
But this makes duplicates when children are in several groups in the same kindergarden. It also seems like an unnecessary complicated way to do it.
For a while I also tried to get the hasManyThrough-relationship to work, but it doesn't seem to work when there'a a many-to-many relationship and a pivot table involved.
I tried with this:
class Kindergarden extends Eloquent {
public function children()
{
return $this->hasManyThrough('Children', 'Group', 'kid', 'gid');
}
}
and then tried to call
Kindergarden::find(1)->children;
I'm sure there is a really simple way to do this, but I'm totally new to laravel and not really that great at sql, so I haven't been able to find anything to help me figure this out.
Edit:
Managed to find a way to do it using Fluent:
$children = DB::table('children')
->join('children_group', 'children.chiid', '=', 'children_group.chiid')
->join('group', 'group.gid', '=', 'children_group.gid')
->where('group.kid', '=', $kid)
->groupby('children.chiid')
->get();
Still want to be able to do it using Eloquent, though.
In your Kindergarden class:
public function groups()
{
$this->hasMany('Groups', 'group_id', 'id');
}
In your Group class:
public function children()
{
$this->hasMany('Children', 'child_id', 'id')
}
Then try:
$children = Kindergarden::with('groups.children')->get();
This way you could go through each group in Kindergarden, and display each child that is apart of each group.
Also just a suggestion: In your database tables, I would name your related id's (such as a group ID in your Kindergarden table) group_id, instead of gid. Makes it much more readable and you know exactly what it's related to. Same with your child id, name it 'child_id' instead of 'chiid'.
EDIT: Since I don't know your table structure, take the functions above with a grain of salt. I hope what I suggested gets you on the right path! Let me know if it helps

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