简体   繁体   中英

“Three way” many-to-many relationship using Eloquent

I have a simple database setup: Users, Groups, Pages - each are many to many. See diagram: http://i.imgur.com/oFVsniH.png

Now I have a variable user id ($id), and with this I want to get back a list of the pages the user has access to, distinctly, since it's many-to-many on all tables.

I've setup my main models like so:

class User extends Eloquent {
    protected $table = 'ssms_users';

    public function groups()
    {
        return $this->belongsToMany('Group', 'ssms_groups_users', 'user_id','group_id');
    }

}
class Group extends Eloquent {
    protected $table = 'ssms_groups';

    public function users()
    {
        return $this->belongsToMany('User', 'ssms_groups_users', 'user_id','group_id');
    }

    public function pages()
    {
        return $this->belongsToMany('Page', 'ssms_groups_pages', 'group_id','page_id');
    }

}
class Page extends Eloquent {
    protected $table = 'ssms_pages';

    public function groups()
    {
        return $this->belongsToMany('Group', 'ssms_groups_pages', 'group_id','page_id');
    }

}

I can get the groups the user belongs to by simply doing:

User::with('groups')->first(); // just the first user for now

However I'm totally lost on how to get the pages the user has access to (distinctly) with one query?

I believe the SQL would be something like:

select DISTINCT GP.page_id
from GroupUser GU
join GroupPage GP on GU.group_id = GP.group_id 
where GU.user_id = $id

Can anyone help?

Thanks

Have you tried something like this before?

$pages = User::with(array('groups', 'groups.pages'))->get();

Eager loading might be the solution to your problem: eager loading

TL;DR: The fetchAll method below, in the MyCollection class, does the work. Simply call fetchAll($user->groups, 'pages');


Ok, assuming you managed to load the data (which should be done by eager-loading it, as mentioned in the other answer), you should loop through the Group s the User has, then loop through its Page s and add it to a new collection. Since I've had this problem already, I figured it would be easier to simply extend Laravel's own Collection class and add a generic method to do that.

To keep it simple, simply create a app/libraries folder and add it to your composer.json , under autoload -> classmap , which will take care of loading the class for us. Then put your extended Collection class in the folder.

app/libraries/MyCollection.php

use Illuminate\Database\Eloquent\Collection as IlluminateCollection;

class MyCollection extends IlluminateCollection {

    public function fetchAll($allProps, &$newCollection = null) {
        $allProps = explode('.', $allProps);
        $curProp = array_shift($allProps);

        // If this is the initial call, $newCollection should most likely be
        // null and we'll have to instantiate it here
        if ($newCollection === null) {
            $newCollection = new self();
        }

        if (count($allProps) === 0) {

            // If this is the last property we want, then do gather it, checking
            // for duplicates using the model's key
            foreach ($this as $item) {
                foreach ($item->$curProp as $prop) {
                    if (! $newCollection->contains($prop->getKey())) {
                        $newCollection->push($prop);
                    }
                }
            }
        } else {

            // If we do have nested properties to gather, then pass we do it
            // recursively, passing the $newCollection object by reference
            foreach ($this as $item) {
                foreach ($item->$curProp as $prop) {
                    static::make($prop)->fetchAll(implode('.', $allProps), $newCollection);
                }
            }
        }

        return $newCollection;
    }
}

But then, to make sure your models will be using this class, and not the original Illuminate\\Database\\Eloquent\\Collection , you'll have to create a base model from which you'll extend all your models, and overwrite the newCollection method.

app/models/BaseModel.php

abstract class BaseModel extends Eloquent {

    public function newCollection(array $models = array()) {
        return new MyCollection($models);
    }

}

Don't forget that your models should now extend BaseModel , instead of Eloquent . After all that is done, to get all your User 's Page s, having only its ID, do:

$user = User::with(array('groups', 'groups.pages'))
            ->find($id);

$pages = $user->groups->fetchAll('pages');

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM