5
votes

I have the following tables: binders, docs, users, docs_users. Doc belongsTo Binder, Doc hasAndBelongsToMany User.

I want to get binders and their associated docs for the user that is currently logged in (the associated user_id in the docs_users table).

I have tried Containable and find('all') with joins, conditions, etc. but I can't figure out how to remove the Docs that are from Users who are not associated in the docs_users table.

This code does NOT work:

$binders = $this->Binder->find( 
        'all',                  
        array( 
            'joins' => array(
                array( 
                    'table' => 'binders_users', 
                    'alias' => 'BindersUser', 
                    'type' => 'inner', 
                    'foreignKey' => false, 
                    'conditions'=> array(
                        'BindersUser.binder_id = Binder.id',
                        'BindersUser.user_id = ' . $this->Auth->user('id')
                    )
                ),
                array( 
                    'table' => 'docs', 
                    'alias' => 'Doc', 
                    'type' => 'left', 
                    'foreignKey' => false, 
                    'conditions'=> array(
                        'Doc.binder_id = Binder.id',
                    )
                ),                          
                array( 
                    'table' => 'docs_users', 
                    'alias' => 'DocsUser', 
                    'type' => 'left', 
                    'foreignKey' => false, 
                    'conditions'=> array(
                        'DocsUser.doc_id = Doc.id',
                        'DocsUser.user_id = ' . $this->Auth->user('id')
                    )
                )           
            ),
            'recursive'=>0
        )
    );
$this->set('binders', $binders);

And neither does this:

$this->Binder->recursive = 2;
$this->Binder->Behaviors->attach('Containable');
$this->Binder->contain(array(
    'Branch',
    'Doc' => array(                     
        'User' => array(
            'DocsUser' => array(
                'conditions' => array('id = "17"')
            )
        )
    )
));
$binders = $this->Binder->find('all');

Any help from you seasoned pros would be great! Thanks!

Alternative/Simplified Solutions?

This works if I just want to get binders to which users have permissions. Short and sweet. However, it will still send ALL associated docs through, which is NOT the behavior I want. It needs to only pass on the docs to which the user has permissions (as described previously).

$binders = $this->Binder->find( 
    'all',                  
    array( 
        'joins' => array(
            array( 
                'table' => 'binders_users', 
                'alias' => 'BindersUser', 
                'type' => 'inner', 
                'foreignKey' => false, 
                'conditions'=> array(
                    'BindersUser.binder_id = Binder.id',
                    'BindersUser.user_id = ' . $this->Auth->user('id')
                )
            )
        )
    )
);
4
It might help if you set debug to 2 and look at what query cake is generating. Then make the changes required to get your desired result.JohnP
Also, could you paste your DB schema on pastebin or something and link it.JohnP
Good idea. Here it is: pastebin.com/0bRn9USD I'm not using the ACL tables right now because I think doing that at a record-level would create a massive database. Kinda defeats the purpose.user603284
If @Leo's approach doesn't work for you, why not just break it into two queries to get the exact output you want? I find that when working with Cake and $this->find(), I tend to forget the fact that it's all SQL in the end and that you can always break down the problem instead of doing it all in one query :)JohnP
JohnP - You're right. I just don't know how to accomplish this within the framework of CakePHP yet. Still a little green.user603284

4 Answers

4
votes

Here's the final solution I came up with based on all of the great feedback I got. I think this is an elegant solution that can be reused in any scenario where deep associations are required.

In the binder_controller I unbound the Doc model, and bound it back using the finderQuery to select only the Docs that a user has permission to see. Then in joined the binders_users table selecting only the binders that users have permissions to.

Thank you everyone for all your help!

$this->Binder->unbindModel(array('hasMany' => array('Doc')));
$this->Binder->bindModel(
    array('hasMany' => array(
            'Doc' => array(
                'className' => 'Doc',
                'foreignKey' => 'binder_id',
                'dependent' => false,
                'finderQuery' => 'SELECT Doc.* FROM docs AS Doc INNER JOIN docs_users AS DocsUser ON DocsUser.doc_id = Doc.id AND DocsUser.user_id = ' . $this->Auth->user('id')
            )
        )
    )
);

$binders = $this->Binder->find( 
    'all',                  
    array( 
        'joins' => array(
            array( 
                'table' => 'binders_users', 
                'alias' => 'BindersUser', 
                'type' => 'inner', 
                'foreignKey' => false, 
                'conditions'=> array(
                    'BindersUser.binder_id = Binder.id',
                    'BindersUser.user_id = ' . $this->Auth->user('id')
                )
            )               
        )
    )
);

More on binding/unbinding models

0
votes

On this line, you need to tell Cake which Model's id you are talking about:

'conditions' => array('id = "17"')

e.g. DocsUser.id

...and you don't use recursive with containable. Get rid of it.

0
votes

Have you tried coming in from a user perspective?

$this->Binder->Doc->User->Behaviors->attach('Containable');
$this->Binder->Doc->User->contain(array('Doc'=>'Binder'));
$user = $this->Binder->Doc->User->find('all',array('conditions'=>'User.id'=>$user_id));

The 'DocsUser' association should be detected anyway.

From a Binders perspective maybe

In your Binders MODEL add ( please check table, key and model names in case I made a typo )

function getBindersByUserSql($user_id)
    {       
        $dbo = $this->getDataSource();
        $subQuery = $dbo->buildStatement(
            array(
                    'fields' => array('DISTINCT(Doc.binder_id)'),
                    'table' => "docs_users",                                       
                    'joins' => array(
                                array('table' => 'users',
                                    'alias' => 'User',
                                    'type' => 'INNER',
                                    'conditions' => array('DocsUser.user_id = User.id')
                                ),
                                array('table' => 'docs',
                                    'alias' => 'Doc',
                                    'type' => 'INNER',
                                    'conditions' => array('Doc.id = DocsUser.doc_id')
                                )
            ),
                    'alias'=>"DocsUser",                                            
                    'conditions' => array("User.id"=>$user_id),
                    'order' => null,
                    'group' => "Doc.binder_id"
                    ),
                    $this
                    );
        return $dbo->expression($subQuery);
    }

Then in your binders CONTROLLER try

$this->Binder->Behaviors->attach('Containable');
$this->Binder->contain(array('Doc'));
$conditions = array();
$conditions = $this->Binder->getBindersByUserSql($this->Auth->user('id'));
$binders = $this->Binder->find('all',array('conditions'=>$conditions)));

Any good?