Update related bindings in many-to-many models

I have two models with many-to-many relationships. I want to assign groups to a user. In my form I want to assign multiple already existing groups to a user. In the documentation I can only find info on how to create a User with many Groups. How do I accomplish this?

Phalcon version: 2.1.0

In the User model I have tried with a function like this that do not update the UserHasGroup table:

public function setMultipleGroupsById($array) {
        $groups = array();
        foreach($array as $id) {
            $group = Model\Group::findFirstById($id);
            $groups[] = $group;
        }
        $this->groups = $groups;
        return $this;
    }

User class:

class User extends \Phalcon\Mvc\Model {
    public $id;
    public $name;

    public function initialize() {
        $this->hasManyToMany("id", "Model\UserHasGroup", "user_id", "group_id", "Model\Group", "id", array('alias' => 'groups'));
    }
}

Group class:

class Group extends \Phalcon\Mvc\Model {
    public $id;
    public $title;
    public function initialize() {
        $this->hasManyToMany("id", "Model\UserHasGroup", "group_id", "user_id", "Model\User", "id", array('alias' => 'users'));
    }
}

UserHasGroup class:

class UserHasGroup extends \Phalcon\Mvc\Model
{
    public $user_id;
    public $group_id;
}

What will happen if you change:

$this->groups = $groups;

to:

$this->__set('groups', $groups);
[01-Apr-2016 12:00:14 UTC] PHP Warning:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction' in C:\opt\kitcloud\dev\Module\Administration\Form\UserUpdateForm.php:149
Stack trace:
#0 [internal function]: PDOStatement->execute()
#1 [internal function]: Phalcon\Db\Adapter\Pdo->executePrepared(Object(PDOStatement), Array, Array)
#2 [internal function]: Phalcon\Db\Adapter\Pdo->execute('INSERT INTO `us...', Array, Array)
#3 [internal function]: Phalcon\Db\Adapter->insert('user_has_group', Array, Array, Array)
#4 [internal function]: Phalcon\Mvc\Model->_doLowInsert(Object(Phalcon\Mvc\Model\MetaData\Memory), Object(Phalcon\Db\Adapter\Pdo\Mysql), 'user_has_group', false)
#5 [internal function]: Phalcon\Mvc\Model->save()
#6 [internal function]: Phalcon\Mvc\Model->_postSaveRelatedRecords(Object(Phalcon\Db\Adapter\Pdo\Mysql), Array)
#7 C:\opt\kitcloud\dev\Module\Administration\Form\UserUpdateForm.php(149): Phalcon\Mvc\Model->save()
#8 C:\opt\kitcloud\dev\Module\Core\Form\Ba in C:\opt\kitcloud\dev\Module\Administration\Form\UserUpdateForm.php on line 149^M
[01-Apr-2016 12:00:14 UTC] PHP Fatal error:  Maximum execution time of 30 seconds exceeded in C:\opt\kitcloud\dev\Module\Administration\Form\UserUpdateForm.php on line 14

What will happen if you change:

$this->groups = $groups;

to:

$this->__set('groups', $groups);
edited Apr '16

I have started on a function to the job, but isn't this build into Phalcon? Is this the right way to go?

    public function updateRelatedByIds(array $identifiers, $related_alias) {
        $related_ids = array();
        $existing_ids = array();
        $new_ids = array();

        $related = $this->getRelated($related_alias);
        foreach($related as $r) {
            array_push($related_ids, $r->id);
        }

        foreach($identifiers as $i) {
            $group = \KitCloud\Module\Core\Model\Group::loadVirtual($i);
            if($group) {
                if(!in_array($group->id, $related_ids)) {
                    array_push($new_ids, $group->id);
                } else {
                    array_push($existing_ids, $group->id);
                }
            }
        }

        $delete_ids = array_diff($related_ids, array_merge($existing_ids, $new_ids));
        // Function to add $new_ids
        // Function to delete $delete_ids
        print_r($new_ids);
        print_r($existing_ids);
        return $this;
    }

    $this->updateRelatedByIds($post, 'groups');

It's built in, but phalcon is using magic setter for setting related objects though.

Could you provide a working example?

edited Apr '16

Just this should work:

public function setMultipleGroupsById($array) {
        $groups = array();
        foreach($array as $id) {
            $group = Model\Group::findFirstById($id);
            $groups[] = $group;
        }
        $this->__set('groups',$groups);
        return $this;
    }

If it's not and you have maybe groups property in your class, try to remove it and check it again, if still not working then i dont know, try to check it out of class, and just access property in private way which will use magic setter. Also instead of:

        $groups = array();
        foreach($array as $id) {
            $group = Model\Group::findFirstById($id);
            $groups[] = $group;
        }

do:

    $groups = Model\Group::find([
        'conditions'=>'id IN ({ids:array})',
        'bind'=>['ids'=>$array]
    ]);

If it's still not working try maybe to add on UserHasGroup this:

public function initialize()
{
    $this->belongsTo('user_id','Model\User','id');
    $this->belongsTo('group_id','Model\Group','id');
}

But it shouldn't be necessary. I will check it later in home and test it why it's not working.

I could not get it to work as intended. Maybe this has something to do with it: https://github.com/phalcon/cphalcon/issues/2871#issuecomment-65855976.

I created a function that I put in my base model:

/**
     * Sync a many to many record to an entity
     * https://laravel.com/docs/4.2/eloquent#inserting-related-models
     * 
     * Usage:
     * $entity->sync('alias_name', array(1, 2, 3), $transaction);
     * @param $related_alias   string   Many to Many alias name
     * @param $identifiers     array    array of ID's of the related records
     * @param $transaction     object   Phalcon Trancation object if the entity is part of a transaction
     * @return true | Exception
     */
    public function sync($related_alias, array $identifiers, \Phalcon\Mvc\Model\Transaction $transaction = NULL) {
        $modelsmanager = $this->getDI()->getModelsManager();
        $relation = $modelsmanager->getRelationByAlias(get_class($this), $related_alias);
        if(!$relation) {
            $msg = sprintf('Relation alias "%s" does not exists', $related_alias);
            if($transaction) {
                $transaction->rollback($msg);
            } else {
                throw new \Exception($msg);
            }
        }

        $related_ids = array();
        $existing_ids = array();
        $new_ids = array();

        $related = $this->getRelated($related_alias);
        foreach($related as $r) {
            array_push($related_ids, $r->id);
        }

        foreach($identifiers as $i) {
            if(!in_array($i, $related_ids)) {
            array_push($new_ids, $i);
            } else {
            array_push($existing_ids, $i);
            }
        }

        $delete_ids = array_diff($related_ids, array_merge($existing_ids, $new_ids));

        $intermediate_model = $relation->getIntermediateModel();
        foreach($new_ids as $related_id) {
            $intermediate = new $intermediate_model;
            if($transaction) {
                $intermediate->setTransaction($transaction);
            }
            $intermediate->{$relation->getIntermediateFields()} =  $this->id;
            $intermediate->{$relation->getIntermediateReferencedFields()} = $related_id;
            if($intermediate->create() == false) {
                $msg = 'Could not create intermediate record: ';
                foreach($intermediate->getMessages() as $m) {
                    $msg .= $m;
                }
                if($transaction) {
                    $transaction->rollback($msg);
                } else {
                    throw new \Exception($msg);
                }
            }
        }

        foreach($delete_ids as $related_id) {
            $intermediate = $intermediate_model::findFirst(array(
                $relation->getIntermediateFields().' = ?0 AND '.$relation->getIntermediateReferencedFields().' = ?1',
                'bind' => array(
                    0 => $this->id,
                    1 => $related_id
                )
            ));
            if($intermediate) {
                if($transaction) {
                    $intermediate->setTransaction($transaction);
                }
                if($intermediate->delete() == false) {
                    $msg = 'Could not delete intermediate record: ';
                    foreach($intermediate->getMessages() as $m) {
                        $msg .= $m;
                    }
                    if($transaction) {
                        $transaction->rollback($msg);
                    } else {
                        throw new \Exception($msg);
                    }
                }
            }
        }
        return true;
    }

I am not surprised, that you could not get this to work. I have the same problem. I've just stopped reading the source code of the Mvc\Model class and there is no single line of code responsible for storing n:n relation..