Skip to content

Instantly share code, notes, and snippets.

@speedmax
Created February 4, 2009 03:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save speedmax/57906 to your computer and use it in GitHub Desktop.
Save speedmax/57906 to your computer and use it in GitHub Desktop.
<?php
/**
* To implment tagging behavior to be able to plug into any data model in the system
*
* Requirements
* You are required to create "tags" table to hold all tag entries
* You are required to create a "[tableName]_tags" table for each object you
* want to tag. for example, to implement tags for pages u need this table
* pages_tags (page_id : int, tag_id)
*
*
* Example Setup
* class Page extends AppModel {
* var $actsAs = array('Taggable');
* var $hasAndBelongsToMany => array('Tag' => array('with'=>'PageTag','joinTable'=>'pages_tags');
* }
*
* Done
* now all tagging related features can be used in your models.
* $this->Page->findPopularTags();
* $this->Page->findRelatedTags(array('news'));
*
* Once $this->Page->id is set, you can all pages related to current page.
* $this->Page->findRelatedTagged(10);
*
*
*/
class TaggableBehavior extends ModelBehavior {
function tag(&$model, $name = null) {
if (empty($name)) return false;
if ($existing = $model->Tag->find(array('name'=>$name), array('id'))) {
return $existing['Tag']['id'];
} else {
$handle = Inflector::slug($name, '-');
$model->Tag->create(array('name'=>$name,'handle'=>$handle));
$model->Tag->save();
return $model->Tag->getLastInsertId();
}
}
function afterFind(&$model, $results, $primary = false) {
foreach($results as &$result) {
if (isset($result['Tag'])) {
$tags = join(', ', Set::extract($result['Tag'], '{n}.name'));
$result[$model->alias]['tags'] = $tags;
}
}
return $results;
}
function beforeSave(&$model) {
if (isset($model->data[$model->alias]['tags'])) {
$tags = Set::normalize($model->data[$model->alias]['tags'], false);
$ids = array();
foreach ($tags as $tag) {
$ids[] = $this->tag($model, $tag);
}
$model->data['Tag']['Tag'] = $ids;
}
return true;
}
/**
* Finds other records that share the most tags with the record passed as the
* related parameter.
* Useful for constructing �Related� or �See Also� boxes and lists.
*
* Require Model::$data to be set so it points to current entry.
* @param Model $model
* @param integer $limit
* @param integer $page
* @return array - list of related entries
*/
function findRelatedTagged(&$model, $limit = 20, $page = 1) {
if (!isset($model->hasAndBelongsToMany['Tag']) || !$model->id) {
return false;
}
extract($model->hasAndBelongsToMany['Tag'], EXTR_SKIP);
$prefix = $model->tablePrefix;
$tableName = Inflector::tableize($className);
$offset = $limit * ($page-1);
# SQL query to get related items share most of tags.
$sql = "SELECT {$model->alias}.*, COUNT( joinTable2.{$foreignKey} ) AS count
FROM {$prefix}{$model->useTable} {$model->alias},
{$prefix}{$joinTable} joinTable,
{$prefix}{$joinTable} joinTable2,
{$prefix}{$tableName} Tag
WHERE joinTable.{$foreignKey} = {$model->id}
AND Tag.{$model->primaryKey} = joinTable.{$associationForeignKey}
AND joinTable2.{$foreignKey} != joinTable.{$foreignKey}
AND joinTable2.{$associationForeignKey} = joinTable.{$associationForeignKey}
AND {$model->alias}.{$model->primaryKey} = joinTable2.{$foreignKey}
AND Tag.site_id = ".Configure::read('Site.id')."
GROUP BY joinTable2.{$foreignKey} ORDER BY count DESC
LIMIT {$offset}, {$limit};";
$related = $model->query($sql);
# Filter out count field from result
if (!empty($related)) {
foreach ($related as $i => $item) unset($related[$i][0]);
}
return $related;
}
/**
* Finds other tags that are related to the tags passed thru the tags parameter,
* by finding common records that share similar sets of tags.
* Useful for constructing �Related tags� lists.
*
* @param Model $model - instance of target model
* @param mixed $tags - list of tags in string or array format
* @return array - list of related Tags
*/
function findRelatedTags(&$model, $tags = array()) {
if (!isset($model->hasAndBelongsToMany['Tag']) || !$tags ) {
return false;
}
extract($model->hasAndBelongsToMany['Tag'], EXTR_SKIP);
if (is_string($tags)) {
$tags = Set::normalize($tags, false);
}
$prefix = $model->tablePrefix;
$tableName = Inflector::tableize($className);
$tagsString = "'".join("', '", $tags)."'";
$tagsCount = count($tags);
# Sql find related tags
$sql = "SELECT Tag.*, COUNT(joinTable.{$foreignKey}) AS count
FROM {$prefix}{$joinTable} joinTable,
{$prefix}{$tableName} Tag
WHERE joinTable.{$foreignKey} IN (
SELECT joinTable.{$foreignKey}
FROM {$prefix}{$joinTable} joinTable,
{$prefix}{$tableName} Tags
WHERE joinTable.{$associationForeignKey} = Tags.id
AND Tags.name IN ({$tagsString})
GROUP BY joinTable.{$foreignKey}
HAVING COUNT(joinTable.{$foreignKey}) = {$tagsCount}
)
AND Tag.name NOT IN ({$tagsString})
AND Tag.id = joinTable.{$associationForeignKey}
AND Tag.site_id = ".Configure::read('Site.id')."
GROUP BY joinTable.{$associationForeignKey}
ORDER BY count DESC";
$related = $model->query($sql);
# Filter out count field from result
if (!empty($related)) {
foreach ($related as $i => $item) unset($related[$i][0]);
}
return $related;
}
/**
* Find most popular tags in relation to current model
*
* @param unknown_type $model
* @param unknown_type $limit
* @param unknown_type $page
* @return unknown
*/
function findPopularTags(&$model, $limit = 20, $page = 1) {
extract($model->hasAndBelongsToMany['Tag'], EXTR_SKIP);
$prefix = $model->tablePrefix;
$tableName = Inflector::tableize($className);
$sql = "SELECT Tag.*, COUNT( joinTable.{$foreignKey} ) AS count
FROM {$prefix}{$joinTable} joinTable,
{$prefix}{$tableName} Tag
WHERE Tag.{$model->primaryKey} = joinTable.{$associationForeignKey}
AND Tag.site_id = ".Configure::read('Site.id')."
GROUP BY joinTable.{$associationForeignKey}
ORDER BY count DESC";
if ($limit != false) {
$offset = $limit * ($page - 1);
$sql .= " LIMIT {$offset}, {$limit}";
}
$popular = $model->query($sql);
foreach ($popular as &$tag) {
$tag = array('Tag' => array_merge($tag['Tag'], $tag[0]));
}
return $popular;
}
/**
* Find all associated entry with tags
*
* @param Model $model
* @param Array $tags - Find entries with these tags
* @return $results
*/
function findTaggedWith(&$model, $tags = array(), $type = 'name') {
extract($model->hasAndBelongsToMany['Tag'], EXTR_SKIP);
if (is_string($tags)) {
$tags = array($tags);
}
if (!in_array($type, array('name', 'handle'))) {
throw new Exception('Tags can only be search using name or handle');
}
$Tag = ClassRegistry::init('Tag');
$ids = Set::extract($Tag->findAll(array($type=>$tags), array('id')), '{n}.Tag.id');
if (!isset($model->{$with}->belongsTo[$model->name])) {
$model->{$with}->bindModel(array(
'belongsTo'=> array($model->name => compact('foreignKey'), 'conditions'=>$conditions, 'Tag'))
);
}
$results = $model->{$with}->findAll(array("{$with}.tag_id"=>$ids, "{$model->name}.id >" => 0));
return $results;
}
/**
* Count the number of tags associated to current Model entry
* Require Model::$data to be set so it points to current entry.
*
* @param Model $model
* @return integer - Number of tags
*/
function tagsCount(&$model) {
if(!isset($model->id)) {
return 0;
}
extract($model->hasAndBelongsToMany['Tag'], EXTR_SKIP);
$results = $model->{$with}->findCount(array(
"{$with}.page_id" => $model->id
));
return $results;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment