0
votes

I want to write a easy Tagging-Plugin for cakephp3 applications. So lets say we have a model books and a model reviews. For each of this models it should be possible to attach Tags - just by adding a behavior (in a plugin): $this->addBehavior('Tag.Taggable').

I created two Tables in the Database: tags, tagged_tags.

Table tagged_tags:

id  |  tag_id  |  tagged_id                   |
1   |  1       |  1                           |
2   |  2       |  1                           |

tagged_id is the id of the tagged entity. The information which model it belongs to is in the other table. Table Tags:

id  |  tag     |  model                       |
1   |  book    |  App\Model\Table\BooksTable  |
2   |  nobook  |  App\Model\Table\ReviewsTable|

Obviously, only the first Tag belongs to a book.

class TaggableBehavior extends Behavior
{
    // Some more code here

    public function __construct(Table $table, array $config = [])
    {
        parent::__construct($table, $config);
        $this->_table = $table;

        $this->_table->belongsToMany('Tag.Tags', [ 
            'joinTable' => 'tagged_tags',
            'foreignKey' => 'tagged_id',
            'targetForeignKey' => 'tag_id',
            'conditions' => [
                'Tags.model' => get_class($this->_table);
            ]
        ]);

    }
}

Retrieving the data works perfectly. But saving is an issue:

Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Tags.model' in 'where clause'

SQL Query:

SELECT TaggedTags.id AS TaggedTags__id, TaggedTags.tagged_id AS TaggedTags__tagged_id, TaggedTags.tag_id AS TaggedTags__tag_id FROM tagged_tags TaggedTags WHERE (tagged_id = :c0 AND Tags.model = :c1)

I'm not so sure why cakephp performs a SELECT-query here, and I don't really care. Why this query causes an error is clear. But where is my mistake here? It has to do with the 'conditions' => ['Tags.model' => get_class($this->_table);. Without this, I can save data (but cant say which Tag belongs to a book or not)

EDIT: Some Additional Info

Here is the complete sql statement, displayed in the debug kit http://freetexthost.com/tc3s46nugi

controller code:

public function add()
{
    $book = $this->Books->newEntity();
    if ($this->request->is('post')) {
        $book = $this->Books->patchEntity($book, $this->request->data);
        if ($this->Books->save($book)) {
            $this->Flash->success(__('The book has been saved.'));
            return $this->redirect(['action' => 'index']);
        } else {
            $this->Flash->error(__('The book could not be saved. Please, try again.'));
        }
    }

In the Behavior I have some logic (copy/pasted) form the Bookmarks-tutorial

public function beforeSave($event, $entity, $options)
{
    if ($entity->tag_string) {
        $entity->tags = $this->_buildTags($entity->tag_string);
    }
}

protected function _buildTags($tagString)
{
    $new = array_unique(array_map('trim', explode(',', $tagString)));
    $out = [];
    $query = $this->_table->Tags->find()
        ->where([
            'Tags.tag IN' => $new,
            'Tags.model' => $this->name()
            ]);
    // Remove existing tags from the list of new tags.
    foreach ($query->extract('tag') as $existing) {
        $index = array_search($existing, $new);
        if ($index !== false) {
            unset($new[$index]);
        }
    }
    // Add existing tags.
    foreach ($query as $tag) {
        $tag['count'] = $tag['count']+1;
        $out[] = $tag;
    }
    // Add new tags.
    foreach ($new as $tag) {
        $out[] = $this->_table->Tags->newEntity(['tag' => $tag, 'model' => $this->name(), 'count' => 0]);
    }
    return $out;
}
1
Can you provide your code that is in controller? - Ray
I've added the code to the posting above. - Matthias Moritz

1 Answers

0
votes

Easy tagging plugin you will make if you create Tags model, and create a HABTM relationship with other models.

Here are simple guidelines.

add.ctp

Omit multiple select tags field, instead put tagging text field (eg. tagsinput). Add some jquery tagging plugin. When adding a new tag (keyword), it is immediately stored in the tags table via jquery post methods. If the keyword exists in the tags table, then do not store it again.

Behavior

In the beforeSave method processing value from tagging field. If the value separated by a comma, you can use something like this (CakePHP 2):

public function beforeSave($options = array())
    {
        if(!empty($this->data['Article']['tagsinput'])) {
            $tags = explode(',', $this->data['Article']['tagsinput']);
            foreach ($tags as $tag) {
                $tagKey = $this->Tag->find('first',array(
                    'recursive' => -1,
                    'fields' => array('id'),
                    'conditions' => array('Tag.name' => $tag)
                    ));
                $this->data['Tag']['Tag'][] = $tagKey['Tag']['id'];
            }
        }
        return true;
    }

This creates HABTM relationship and stores such values in the table articles_tags.

edit.ctp

Create a comma separated values:

<?php
            $extract_tags = Hash::extract($this->data['Tag'],'{n}.name');
            $tags = implode(',', $extract_tags);
            echo $this->Form->input('tagsinput',
                array(
                    'id' => 'tags',
                    'div'=>true,
                    'label'=>__('Tags'),
                    'class'=>'form-control input-lg',
                    'placeholder'=>__('Add keywords here'),
                    'value' => $tags
                    )
                );
            ?>