Save tags to articles (belongsToMany) with joinData

Hello there!

I have a TagTable with a “through-table” called TagsRelations. TagsTable holds all tags (title and type), TagsRelations the primary-keys of the subjects (tag_id, subject_id) and the section that describes, what has been tagged (a file, an article etc…). So TagsRelations.section must not be null.

When I add a new article and select two tags, the tags will be recognized but the tagsrelation with “section” (e.g. “articles” in that case) will not be filled. How can I say the view or the controller (or maybe even the table), that the “section”-column should be filled with “articles”?

My form in add.ctp looks like this (regarding the tags)

    echo $this->Form->control('tags._ids', [
        'type' => 'select',
        'multiple' => true,
        'options' => $tags,
    ]);

I also tried to add in the controller a foreach-loop that adds a “_joinData”-array to the post-data.

In short: How to add joinData using a select-control in a belongsToMany-Relationship (or how do it otherwise), when we don’t know before, how many items (= tags) will be selected.

Thanks in advance.

In TagsRelations table, have you provided the default value to the not null type field (section)?
As whenever we make a column as not null, it will always ask for any value so in that case we can set a default value so that we can skip to fill it at the first time when we don’t know exactly what to fill. Please let me know if that make sense.

No problem, thanks. Here we go:

tags
id | title

tags_relations
id | tag_id | foreign_key | section

articles
id | title | content

When tags “joke” and “bestof” have ids 23 and 24 and article “bestjoke” has id 10, then tags_relations should have two rows like that
1 | 23 | 10 | article
2 | 24 | 10 | article

And for the sake of completeness, here are my Table-Classes:

ArticlesTable

    $this->belongsToMany('Tags', [
            'foreignKey' => 'foreign_key',
            'through' => 'tags_relations',
        ]);//->setConditions(['tags_relations.section' => 'articles']);

TagsTable

    $this->hasMany('TagsRelations', [
            'foreignKey' => 'tag_id'
        ]);

In the meantime, I try to modify post-data in the beforeMarshal-event to add _joinData with section = ‘articles’.

Problem is, that I can’t add extra-data to tags_relations (here the section value of “articles”)

Thanks

I think, now I get what is the actual problem. I am explaining my understanding of the problem:

When we create a new article, there can be two cases like below:

  1. A new article may not have a tag, then we need to save it into the articles table only and not in the tags_relations table. This is the simple case where we don’t need to provide any section value.

  2. When a user select tags for a new article then he must provide a value for the sections so that we can save it into the tags_relations table as well.

Am I correct?

You’re almost fully correct. I want to tag several subjects (e.g. articles, pictures, files…). Since tagging is always the same, I have one “base-table” called “tags” and one for the relations between tags and the user-content.

To keep track (or to describe the “foreign_key” value of the tags_relations-Table) I store wherefrom the “foreign_key”-value comes from. When it comes from the Articles-Backend, I store “articles” to the relations and then I know, that this specific forein_key is an article_id. Then, I can work with that. Otherwise, I / cakephp would not know, what subject is behind that specific forein_key. So it is just a mechanism for reusing the tag-system.

And this is why, your cases are correct with the speciality, that not the user should enter the “section” for each tag. But instead the backend should automatically add/detect/whatever which “section” needs to be applied.

So when we stick to the articles-example, the user should get all tags available while creating a new article, select these tags he want’s to add and the backend stores “section = articles” to the tags_relations table.
When loading an article, all TagsRelations.Tags with section = “article” should be retrieved and the result should be the tags of the article.

So, you are almost correct. Thanks

If you are using different forms for each user content then probably a hidden field can be added for each form like a hidden form with name section having value as “article” for Article form and “file” for File form etc.

This is what I also thought of, but I don’t get this hidden value to the entity data. I think, the associations are messed up or I did an other mistake.

Hi @TheMiller,

Can you check in the Entity TagsRelation.php file if the value for this field “section” is set to True in $_accessible properties? If not, then make it True to get it accessible like below:

protected $_accessible = [
‘id’ => true,
‘tag_id’=>true,
‘foreign_key’=>true,
’section’=>true
];

Good point. But all fields were accessible via "*" => true, but I changed it like you suggested. But without any luck.

I think this is the problem (took from here creating-inputs-for-associated-data (last paragraph).

// Multiple select element for belongsToMany
// Does not support _joinData

But how to workaround? Since we don’t know before, how many tags the user will add, we can’t work with

echo $this->Form->control('tags.0._joinData.section');

So the only way seems to modify the requestData at beforeMarshal-Event (or sth. like this). Am I correct?

Yes, you are correct. You need to modify the requestData.

Thanks for your help. I finally came up with this solution (maybe not the only solution and maybe not the best). Maybe you (or sb. else) will have a look on it. In the beforeMarshal of the ArticleTable, I modify the requestData expected (as per the docs/book says). But I have to set the foreign_key to a dummy value, since the validator says it is required. This foreign_key-column will automatically be replaced with the correct id lateron. The user_id as grabbed from the articles entity.

ArticlesTable.php

public function beforeMarshal($event, $data, $options)
    {
        foreach ($data as $key => $value) {
            if($key == "tags" && is_array($value)) {
                foreach ($value as $idx => $tag_id) {
                    $tag = [
                        'id' => (int)$tag_id,
                        '_joinData' => [
                            'foreign_key' => -1,
                            'user_id' => $data['user_id'],
                            'section' => 'articles'
                        ]
                    ];
                    $tags[] = $tag;
                }
                $data['tags'] = $tags;
            }
        }
    }

Is that correct / a good approach / just a workaround? Thanks !

I recommend looking into plugins, e.g. Tags that encapsulate lots of the logic to keep the application code simple and future proof. Ideally they are agnostic and configurable enough to provide support for many use cases, including yours here. They also, based on community contributions, often provide more complete and correct functionality.
Basically always consult the https://github.com/FriendsOfCake/awesome-cakephp list when you are looking for some functionality.