cerebrate/plugins/Tags/src/Model/Behavior/TagBehavior.php

355 lines
11 KiB
PHP

<?php
namespace Tags\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
class TagBehavior extends Behavior
{
protected $_defaultConfig = [
'finderField' => 'name',
'tagsAssoc' => [
'className' => 'Tags.Tags',
'joinTable' => 'tags_tagged',
'foreignKey' => 'fk_id',
'targetForeignKey' => 'tag_id',
'propertyName' => 'tags',
],
'tagsCounter' => ['counter'],
'taggedAssoc' => [
'className' => 'Tags.Tagged',
'foreignKey' => 'fk_id'
],
'implementedEvents' => [
'Model.beforeMarshal' => 'beforeMarshal',
'Model.beforeFind' => 'beforeFind',
'Model.beforeSave' => 'beforeSave',
],
'implementedMethods' => [
'normalizeTags' => 'normalizeTags',
],
'implementedFinders' => [
'tagged' => 'findByTag',
'untagged' => 'findUntagged',
],
];
public function initialize(array $config): void {
$this->bindAssociations();
$this->attachCounters();
}
public function bindAssociations() {
$config = $this->getConfig();
$tagsAssoc = $config['tagsAssoc'];
$taggedAssoc = $config['taggedAssoc'];
$table = $this->_table;
$tableAlias = $this->_table->getAlias();
$assocConditions = ['Tagged.fk_model' => $tableAlias];
if (!$table->hasAssociation('Tagged')) {
$table->hasMany('Tagged', array_merge(
$taggedAssoc,
[
'conditions' => $assocConditions
]
));
}
if (!$table->hasAssociation('Tags')) {
$table->belongsToMany('Tags', array_merge(
$tagsAssoc,
[
'through' => $table->Tagged->getTarget(),
'conditions' => $assocConditions,
]
));
}
if (!$table->Tags->hasAssociation($tableAlias)) {
$table->Tags->belongsToMany($tableAlias, array_merge(
$tagsAssoc,
[
'className' => get_class($table),
]
));
}
if (!$table->Tagged->hasAssociation($tableAlias)) {
$table->Tagged->belongsTo($tableAlias, [
'className' => get_class($table),
'foreignKey' => $tagsAssoc['foreignKey'],
'conditions' => $assocConditions,
'joinType' => 'INNER',
]);
}
if (!$table->Tagged->hasAssociation($tableAlias . 'Tags')) {
$table->Tagged->belongsTo($tableAlias . 'Tags', [
'className' => $tagsAssoc['className'],
'foreignKey' => $tagsAssoc['targetForeignKey'],
'conditions' => $assocConditions,
'joinType' => 'INNER',
]);
}
}
public function attachCounters() {
$config = $this->getConfig();
$taggedTable = $this->_table->Tagged;
if (!$taggedTable->hasBehavior('CounterCache')) {
$taggedTable->addBehavior('CounterCache', [
'Tags' => $config['tagsCounter']
]);
}
}
public function beforeMarshal($event, $data, $options) {
$property = $this->getConfig('tagsAssoc.propertyName');
$options['accessibleFields'][$property] = true;
$options['associated']['Tags']['accessibleFields']['id'] = true;
if (isset($data['tags'])) {
if (!empty($data['tags'])) {
$data[$property] = $this->normalizeTags($data['tags']);
}
}
}
public function beforeSave($event, $entity, $options)
{
if (empty($entity->tags)) {
return;
}
foreach ($entity->tags as $k => $tag) {
if (!$tag->isNew()) {
continue;
}
$existingTag = $this->getExistingTag($tag->name);
if (!$existingTag) {
continue;
}
$joinData = $tag->_joinData;
$tag = $existingTag;
$tag->_joinData = $joinData;
$entity->tags[$k] = $tag;
}
}
public function normalizeTags($tags) {
$result = [];
$modelAlias = $this->_table->getAlias();
$common = [
'_joinData' => [
'fk_model' => $modelAlias
]
];
$tagsTable = $this->_table->Tags;
$displayField = $tagsTable->getDisplayField();
$tagIdentifiers = [];
foreach ($tags as $tag) {
if (empty($tag)) {
continue;
}
if (is_object($tag)) {
$result[] = $tag->toArray();
}
$tagIdentifier = $this->getTagIdentifier($tag);
if (isset($tagIdentifiers[$tagIdentifier])) {
continue;
}
$tagIdentifiers[$tagIdentifier] = true;
$existingTag = $this->getExistingTag($tagIdentifier);
if ($existingTag) {
$result[] = array_merge($common, ['id' => $existingTag->id]);
continue;
}
$result[] = array_merge(
$common,
[
'name' => $tagIdentifier,
'colour' => '#924da6'
]
);
}
return $result;
}
protected function getTagIdentifier($tag)
{
if (is_object($tag)) {
return $tag->name;
} else if (is_array($tag)) {
return $tag['name'];
} else {
return trim($tag);
}
}
protected function getExistingTag($tagName)
{
$tagsTable = $this->_table->Tags->getTarget();
$query = $tagsTable->find()->where([
'Tags.name' => $tagName
])
->select('Tags.id');
return $query->first();
}
public function findByTag(Query $query, array $options) {
$finderField = $optionsKey = $this->getConfig('finderField');
if (!$finderField) {
$finderField = $optionsKey = 'name';
}
if (!isset($options[$optionsKey])) {
throw new RuntimeException(__('Expected key `{0}` not present in find(\'tagged\') options argument.', $optionsKey));
}
$isAndOperator = isset($options['OperatorAND']) ? $options['OperatorAND'] : true;
$filterValue = $options[$optionsKey];
if (!$filterValue) {
return $query;
}
$filterValue = $this->dissectArgs($filterValue);
if (!empty($filterValue['NOT']) || !empty($filterValue['LIKE'])) {
return $this->findByComplexQueryConditions($query, $filterValue, $finderField, $isAndOperator);
}
$subQuery = $this->buildQuerySnippet($filterValue, $finderField, $isAndOperator);
if (is_string($subQuery)) {
$query->matching('Tags', function ($q) use ($finderField, $subQuery) {
$key = 'Tags.' . $finderField;
return $q->where([
$key => $subQuery,
]);
});
return $query;
}
$modelAlias = $this->_table->getAlias();
return $query->where([$modelAlias . '.id IN' => $subQuery]);
}
public function findUntagged(Query $query, array $options) {
$modelAlias = $this->_table->getAlias();
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
$conditions = ['fk_model' => $modelAlias];
$this->_table->hasOne('NoTags', [
'className' => $this->getConfig('taggedAssoc.className'),
'foreignKey' => $foreignKey,
'conditions' => $conditions
]);
$query = $query->contain(['NoTags'])->where(['NoTags.id IS' => null]);
return $query;
}
protected function dissectArgs($filterValue): array
{
if (!is_array($filterValue)) {
return $filterValue;
}
$dissected = [
'AND' => [],
'NOT' => [],
'LIKE' => [],
];
foreach ($filterValue as $value) {
if (substr($value, 0, 1) == '!') {
$dissected['NOT'][] = substr($value, 1);
}
else if (strpos($value, '%') != false) {
$dissected['LIKE'][] = $value;
} else {
$dissected['AND'][] = $value;
}
}
if (empty($dissected['NOT']) && empty($dissected['LIKE'])) {
return $dissected['AND'];
}
return $dissected;
}
protected function buildQuerySnippet($filterValue, string $finderField, bool $OperatorAND=true)
{
if (!is_array($filterValue)) {
return $filterValue;
}
$key = 'Tags.' . $finderField;
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
$conditions = [
$key . ' IN' => $filterValue,
];
$query = $this->_table->Tagged->find();
if ($OperatorAND) {
$query->contain(['Tags'])
->group('Tagged.' . $foreignKey)
->having('COUNT(*) = ' . count($filterValue))
->select('Tagged.' . $foreignKey)
->where($conditions);
} else {
$query->contain(['Tags'])
->select('Tagged.' . $foreignKey)
->where($conditions);
}
return $query;
}
protected function findByComplexQueryConditions($query, $filterValue, string $finderField, bool $OperatorAND=true)
{
$key = 'Tags.' . $finderField;
$taggedAlias = 'Tagged';
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
if (!empty($filterValue['AND'])) {
$subQuery = $this->buildQuerySnippet($filterValue['AND'], $finderField, $OperatorAND);
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id IN' => $subQuery]);
}
if (!empty($filterValue['NOT'])) {
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, false);
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id NOT IN' => $subQuery]);
}
if (!empty($filterValue['LIKE'])) {
$conditions = ['OR' => []];
foreach($filterValue['LIKE'] as $likeValue) {
$conditions['OR'][] = [
$key . ' LIKE' => $likeValue,
];
}
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, $OperatorAND);
if ($OperatorAND) {
$subQuery = $this->_table->Tagged->find()
->contain(['Tags'])
->group('Tagged.' . $foreignKey)
->having('COUNT(*) >= ' . count($filterValue['LIKE']))
->select('Tagged.' . $foreignKey)
->where($conditions);
} else {
$subQuery = $this->_table->Tagged->find()
->contain(['Tags'])
->select('Tagged.' . $foreignKey)
->where($conditions);
}
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id IN' => $subQuery]);
}
return $query;
}
}