355 lines
11 KiB
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;
|
|
}
|
|
}
|