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

Ensuring uniqueness in Phalcon related n-n models

I have a model contents that has a many to many relationship with tags through the intermediary table contents_tags. When I insert a new row in contents I want to also add multiple tags. While this works fine, I want the tags entries to be unique, so they will be inserted if they are new, or updated (although nothing changes) if they already exist.

This unit test seems to imply that this can be done automatically, but I can't manage to replicate the same behaviour. If I don't have a unique index on my tag table, then I get multiple of the same entries. If I do then the tag model throws an error.

This is my test code:

$content                = new Content();
$content->title         = 'xkcd';
$content->description   = 'description goes here';
$content->url           = 'https://xkcd.com/';
$content->created_on    = new Phalcon\Db\RawValue('NOW()');
$content->tags          = array();

$tagsText = 'xkcd,comics,testing';

$tags = array();
foreach(explode(',', $tagsText) as $tagText) {
    $tag = new Tag();
    $tag->tag = trim($tagText);
    $tags[] = $tag;
}
$content->tags = $tags;

if($content->save()) {
    $app->response->setStatusCode(201, "Created");
    $app->response->setJsonContent($content->overview());
} else {
    $app->response->setStatusCode(400, "Bad Request");
    $app->response->setJsonContent(array('errors'=>$content->getMessagesAsArray()));
}

Contents model:

class Content {
    public function initialize() {
        $this->hasManyToMany(
            'id',
            'ContentsTags',
            'content_id',
            'tag_id',
            'Tag',
            'id',
            array('alias' => 'tags')
        );
    }

    public function getSource() {
        return 'contents';
    }
}

ContentsTag model:

class ContentsTags {

    public function initialize() {
        $this->belongsTo('content_id', 'Content', 'id', array('alias' => 'content'));
        $this->belongsTo('tag_id', 'Tag', 'id', array('alias' => 'tag'));
    }

    public function getSource() {
        return 'contents_tags';
    }
}

Tag model:

class Tag {

    public function getSource() {
        return 'tags';
    }

    public function initialize() {
        $this->hasManyToMany(
            'id',
            'ContentsTags',
            'tag_id',
            'content_id',
            'Content',
            'id',
            array('alias' => 'contents')
        );
    }
}

Example data from the tables:

contents:

+----+-------+-----------------------+------------------+
| id | title | description           | url              |
+----+-------+-----------------------+------------------+
| 11 | xkcd  | description goes here | https://xkcd.com/ |
+----+-------+-----------------------+------------------+

contents_tags:

+----+------------+--------+
| id | content_id | tag_id |
+----+------------+--------+
|  1 |         11 |      1 |
|  2 |         11 |      2 |
+----+------------+--------+

tags:

+----+--------+
| id | tag    |
+----+--------+
|  1 | comics |
|  2 | maths  |
+----+--------+

The models for the unit test mentioned above seem to have no special parameters set, and I can't find the actual table declarations for them so I am at a bit of a loss. The models for the unit test can be seen here:



43.9k
edited Sep '14

Hi

Phalcon\Mvc\Model\Validator\Uniqueness will do the trick for you in Tags class. More doc



1.6k
edited Sep '14

I'm afraid while that does make sure that a value is unique, it will throw a validation error if one of the tags isn't unique. What I want is the behaviour from the unit test; if you look at that example you will see 4 "parts" are created, two of them are then saved individually, then all 4 are saved as part of $robot->save() (lines 354 / 356), and then they test and show that each of the 4 parts has only been saved once.



1.6k

I think it's actually me misunderstanding what the unit test was doing. I thought it was detecting that there was a part called "Part 1" and "Part 2" and so not inserting. What it's actually doing is noticing that those two parts have ID values, and so are not inserts.

My apologies. I'll rewrite my code to test each tag before adding to the tags array.



1.6k

I added this to the Tag class:

/**
 * Look to see if a tag exists, if it does then
 * return it. If it doesn't then create it and
 * return it.
 *
 * @param  string $tagName
 * @return Tag    $tag
 */
public static function getOrCreate($tagName) {
    $tag = static::findFirst(
        array(
            'conditions' => "tag=?0", 
            "bind" => array($tagName)
        )
    );
    if($tag) return $tag;

    try {
        $tag = new Tag();
        $tag->tag = $tagName;
        $tag->save();
        return $tag;
    } catch(Exception $e) {
        $this->appendMessage(new Message($e->getMessage(), 'tags'));
        return false;
    }
}

and altered the test code to this:

$tags = array();
foreach(explode(',', $tagsText) as $tagText) {
    $tags[] = Tag::getOrCreate(trim($tagText));
}
$content->tags = $tags;


43.9k

it will throw a validation error if one of the tags isn't unique

Tag element creation will be impossible for sure, but I do not think that it will stop all the process of related record creation (ContentsTags)



15.1k

You could make this even simpler by overloading the save() method on Tag model and perform your unique test within the save itself. This way you don't need to call any extra methods and just go about your usual business as you would normally do.



43.9k

you're right nazwa !