* * @author Antti Holvikari * * @license http://opensource.org/licenses/bsd-license.php BSD * * @version $Id$ * */ /** * * Tagging model * * Based on the 'toxi' tagging table schema described * [here](http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html). * * @category Abovo * * @package Abovo_Model_Tag * */ class Abovo_Model_Tag extends Solar_Base { /** * Area name * * Area to which all the tags belong to * * @var string */ protected $_area; /** * Tags table model * * @var Abovo_Model_Tags */ private $_tags; /** * Tagmap table model * * @var Abovo_Model_TagMap */ private $_tagmap; /** * * Constructor * * @return void * */ public function __construct($config = array()) { parent::__construct($config); if (empty($this->_config['area'])) { throw $this->_exception('ERR_AREA_NOT_SET'); } else { $this->_area = $this->_config['area']; } // Make sure sql is available if (! Solar::isRegistered('sql')) { Solar::register('sql', Solar::factory('Solar_Sql')); } // Instantiate table models, this will create the tables $this->_tags = Solar::factory('Abovo_Model_Tags'); $this->_tagmap = Solar::factory('Abovo_Model_TagMap'); } /** * * Fetches all tags * * @param int $node_id Node id for which to search tags for * * @param string $handle User handle for which to search tags for * * @return array List of tags * */ public function fetchAll($node_id = null, $handle = null) { $select = Solar::factory('Solar_Sql_Select'); // table names $tagmap = $this->_tagmap->name; $tags = $this->_tags->name; $select->from($tags, 'name') ->leftJoin($tagmap, "$tags.id = $tagmap.tags_id") ->where($tagmap . '.area = ?', $this->_area); // fetch for node_id? if (is_numeric($node_id)) { $select->where($tagmap . '.node_id = ?', (int) $node_id); } // fetch for handle? if (! empty($handle)) { $select->where("$tagmap.handle = ?", $handle); } $select->group("$tags.name"); return $select->fetch('col'); } /** * * Fetches all tags and their counts * * Returns an array with 'tag' => count pairs * * @param int $node_id Node id for which to search tags for * * @param string $handle User handle for which to search tags for * * @return array Tag => tag count pairs * */ public function fetchAllCounts($node_id = null, $handle = null) { $select = Solar::factory('Solar_Sql_Select'); // table names $tagmap = $this->_tagmap->name; $tags = $this->_tags->name; $select->from($tags, "name, count($tagmap.id) as count") ->leftJoin($tagmap, "$tags.id = $tagmap.tags_id") ->where($tagmap . '.area = ?', $this->_area); // fetch for node_id? if (! empty($node_id)) { $select->where($tagmap . '.node_id = ?', (int) $node_id); } // fetch for handle? if (! empty($handle)) { $select->where("$tagmap.handle = ?", $handle); } $select->group("$tags.name"); return $select->fetch('pairs'); } /** * * Normalizes tag strings. * * Converts "+" to " ", trims extra spaces, and removes duplicates, * but otherwise keeps them in order and space-separated. * * Also converts arrays to a normalized tag string. * * @param string|array $tags A space-separated string of tags, or a * sequential array of tags. * * @return string A space-separated string of tags. * */ public function asString($tags) { // convert to array from string? if (! is_array($tags)) { // convert all "+" to spaces (this is for URL values) $tags = str_replace('+', ' ', $tags); // trim all surrounding spaces and extra spaces $tags = trim($tags); //$tags = preg_replace('/[ ]{2,}/', ' ', $tags); // if there is more than one tag after exploding // with a comma, we'll use comma as a separator // and trim all whitespace $tags = explode(',', $tags); foreach ($tags as &$tag) { $tag = trim($tag); } } // make sure each tag is unique (no double-entries) $tmp = array_unique($tags); // return as space-separated text return implode(', ', $tmp); } /** * * Normalizes tag arrays. * * Also converts strings to a normalized tag array. * * @param string|array $tags A space-separated string of tags, or a * sequential array of tags. * * @return array An array of tags. * */ public function asArray($tags) { // normalize to string... $tags = $this->asString($tags); // ... and convert to array. for some reason, // if we explode on an empty tag string, we get // one array element of an empty string. that // screws things up, so check if there are actually // tags in the string. if ($tags) { return explode(', ', $tags); } else { return array(); } } /** * * "Refreshes" the tags for a node_id/user by diff. * * @param int $node_id The node_id to work with. * * @param string $handle User handle * * @param string|array $tags A space-separated string of tags, or a * sequential array of tags. These are the replacement tags. * * @return array New set of tags * */ public function refresh($tags, $node_id, $handle = null) { // get the old set of tags $old = $this->fetchAll($node_id, $handle); // normalize the new tags to an array $new = $this->asArray($tags); // diff the tagsets $diff = $this->_diff($old, $new); // delete? if (! empty($diff['del'])) { $ids = array(); foreach ($diff['del'] as $tag) { // fetch tag's id // @todo this sucks, make a better query $ids[] = (int) $this->_tags->fetchId($tag); } $where = array( 'area = ?' => $this->_area, 'handle = ?' => $handle, 'node_id = ?' => $node_id, 'tags_id IN (?)' => $ids, ); $this->_tagmap->delete($where); }; // insert foreach ($diff['ins'] as $tag) { // check if tag exists $id = $this->_tags->fetchId($tag); if ($id === false) { // add tag $data = $this->_tags->insert(array('name' => $tag)); // get the id $id = $data['id']; } // tag data to the tag map $data = array( 'area' => $this->_area, 'handle' => $handle, 'node_id' => $node_id, 'tags_id' => $id, ); // insert tag data $this->_tagmap->insert($data); } // done! return new set of tags return $new; } /** * * Determines the diff (delete/insert matrix) between two tag sets. * * * $old = array('a', 'b', 'c'); * $new = array('c', 'd', 'e'); * * // perform the diff * $diff = $this->_diff($old, $new); * * // the results are: * // $diff['del'] == array('a', 'b'); * // $diff['ins'] == array('d', 'e'); * // 'c' doesn't show up because it's present in both sets * * * @param array $old The old (previous) set of tags. * * @param array $new The new (current) set of tags. * * @return array An associative array of two keys: 'del' (where the * value is a sequential array of tags removed from the old set) * and 'ins' (where the value is a sequential array of tags to be * added from the new set). * */ protected function _diff($old, $new) { // find intersections first $intersect = array_intersect($old, $new); // now flip arrays so we can unset easily by key $old = array_flip($old); $new = array_flip($new); // remove intersections from each array foreach ($intersect as $val) { unset($old[$val]); unset($new[$val]); } // keys remaining in $old are to be deleted, // keys remaining in $new are to be added return array( 'del' => (array) array_keys($old), 'ins' => (array) array_keys($new) ); } }