We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Many to Many expected behaviour

I have seen a few posts similar to this over the months, and just wanted to clarify the current state of things.

So say I have a blog with Post and Tag in a many to many, with a joining table, and I run this code

$tag1 = \Tag::findFirst('id=1');
$tag2 = \Tag::findFirst('id=2');
$tag3 = \Tag::findFirst('id=3');

// create a post
$post = new \Post();
$post->tags = array($tag1, $tag2);
$post->save();

// edit a post
$post = \Post::findFirst('id=1'); // the same post that was created earlier
$post->tags = array($tag1, $tag3);
$post->save();

What is the expected behaviour assuming I have everything set up correctly? I see there being 3 possiblities

  1. The post has tags 1, 2 & 3
  2. the post has tags 1 & 3
  3. the post has 2 of tag1 and 1 each of 2 & 3

Is one of these "right" or is it a config issue? If what is the syntax for choosing which one happens?

Note that a ResultSet\Simple cannot be assigned directly to $post->tags

$post->tags = Tag::find("id IN (1,2)"); // Wrong

Neither can a full array() export

$post->tags = Tag::find("id IN (1,2)")->toArray(); // Wrong

Only an array of Models can be assigned :

$post->tags = Tag::find("id IN (1,2)")->filter(function($t) {
    return $t;
}); // Right

Workaround #1 : overloading the default ResultSet\Simple class

class MyResultSet extends \Phalcon\Mvc\Model\ResultSet\Simple
{
    public function getAll()
    {
        return $this->filter(function($t) {
            return $t;
        });
    }
}

// ...

$post->tags = Tag::find("id IN (1,2)")->getAll();

Workaround #2 : overloading the default \Phalcon\Mvc\Model::__set() method

abstract class AbstractModel extends \Phalcon\Mvc\Model
{
    public function __set($property, $value)
    {
        if ($value instanceof \Phalcon\Mvc\Model\ResultSetInterface)
        {
            $value = $value->filter(function($r) {
                return $r;
            });
        }
        parent::__set($property, $value);
    }
}

// ...

$post->tags = Tag::find("id IN (1,2)");

Thanks for your reply, but you have lost me a bit. you say

Only an array of Models can be assigned :

but findFirst returns an instance of the model, not a result set so

$tag1 = \Tag::findFirst('id=1');
$tag2 = \Tag::findFirst('id=2');

$post->tags = array($tag1, $tag2);

is an array of models, I don't see what the difference in end result is between my code and yours.

But even that is a bit OT, using your code copy and pasted what should happen?

$post->tags = Tag::find("id IN (1,2)")->getAll();
$post->save();

$post->tags = Tag::find("id IN (1,3)")->getAll();
$post->save();
  1. The post has tags 1, 2 & 3
  2. the post has tags 1 & 3
  3. the post has 2 of tag1 and 1 each of 2 & 3

thanks :)



7.5k

Did you solve it? I have the same problem.

Not yet... I am still not sure if there even is a problem... I don't know if the way it is behaving is the way the programmers intended it to behave!



2.3k
edited Jun '14

I have worked a lot with other PHP ORMs like Doctrine 2 and so on. The buggy behaviour described here is obviously just a bug/issue in Phalcon ORM, and it needs to be resolved. It hurts a lot, because this problem means that even fundamentals of the ORM (update records correctly) is broken nowadays in Phalcon.

is there an official bug report registered?

i finally came up with the following crutch

    public function setTags($tags) {

        // crutch for Phalcon ORM bug https://forum.phalcon.io/discussion/2190/many-to-many-expected-behaviour#C8434
        \Phalcon\DI::getDefault()->get('modelsManager')->executeQuery('delete from PostsTags where postref = ' . $this->getId());

        $this->Tags = $tags;
    }
edited Sep '14

PLEASE make someone from Phalcon dev-team make clear about this.

Saving many to many is really painful right now. There is no official statement, we are lost..

Please answer the very first question in this thread..



2.6k

Bumping this.. As above, could a dev please answer the first question for us?



6.9k

I think I have the fix ready to be added to the Phalcon codebase but I want to be absolutely sure I get this right:

1) There's a table "tag" and a table "post" and they have a many to many relation 2) When adding relation elements (tags) to the entity (post), the old relations (tags) get overwritten



2.6k

There is also no way to 'remove' all of the 'tags'. If you set the 'tag' array in your 'post' to an empty array, it will just leave the relation as it is in the database, as if you didn't do anything. (instead of removing the relations).

$post->tag = $tags; $post->save(); //ok $post->tag = array(); $post->save(); //retains tags

On Thu, Nov 6, 2014 at 1:32 AM, Rian Orie [email protected] wrote:

I think I have the fix ready to be added to the Phalcon codebase but I want to be absolutely sure I get this right:

1) There's a table "tag" and a table "post" and they have a many to many relation 2) When adding relation elements (tags) to the entity (post), the old relations (tags) get overwritten

— Reply to this email directly or view the complete thread on Phosphorum https://forum.phalcon.io/discussion/2190/many-to-many-expected-behaviour#C12817. Change your e-mail preferences here https://forum.phalcon.io/settings

--

  • Tim


6.9k

that shouldn't prove too troublesome to take along. Can you confirm my use case though?



2.6k
edited Nov '14

No, I can't. Maybe first firstactivemedia can. I am following this thread for a different reason. I think things got a little confusing along the way. :( I am here because this just straight up doesn't work:

$tag1 = \Tag::findFirst('id=1');
$tag2 = \Tag::findFirst('id=2');

$post->tags = array($tag1, $tag2);

but this does, because it returns a 'result set'.

$post->tags = Tag::find("id IN (1,2)")->filter(function($t) {
    return $t;
});

This just seems buggy and broken. This is what I thought we were discussing. More on the topic of what you were asking, I would expect the old relations to get overriden if I do this:

$post->tags = Tag::find("id IN (1,2)")->filter(function($t) {
    return $t;
});

$post->save(); //post has tags 1 and 2

$post->tags = Tag::find("id IN (3)")->filter(function($t) {
    return $t;
});

$post->save(); //post only has tag 3. Makes sense that 1 and 2 were 'removed'.

I would expect to have to do an array push of some kind to retain 1 and 2 while adding 3. That is, load up the post, array_push tag with id 3 into the posts tag array and save the post.

The code would be so much easier to work with if the first snippit worked properly. To add tag with 'id' 3 to a post, i would just do this:

$tag1 = \Tag::findFirst('id=1');
$tag2 = \Tag::findFirst('id=2');

$post->tags = array($tag1, $tag2);
$post->save(); //post has tags 1 and 2

...

array_push($post->tags, \Tag::findFirst('id=3'));
$post->save(); //post has tags 1 2 and 3
//or if you wanted to remove all othe relations:
$post->tags = array(\Tag::findFirst('id=3'));
$post->save(); //post has only tag 3


2.6k

Rian, incase you are replying via email, I have edited my post with some extra stuff. I'll just leave this comment to notify you.



615

@fverry

Workaround #1 : overloading the default ResultSet\Simple class

How would you do that ? Resultset is returned by db adapter.