category manager

mailling@bigfoot.com mailling@bigfoot.com
Wed, 18 Jul 2001 15:17:10 -0500


I finished it and tested it, it should work even if we could optimize it a 
lot. Maybe, we could add later on a cache, ...

----
- lib/Category.php
<?php
// $Horde: horde/lib/Category.php,v 1.13 2001/07/15 22:48:24 shuther Exp $

/** Required values for $params:
* groupid define each group of categories we want to build
*/

// Return codes
/** @constant CATEGORY_OK Operation succeeded. */
define('CATEGORY_OK', 0);

/** @constant CATEGORY_ERROR Operation failed. */
define('CATEGORY_ERROR', -1);

/** @constant CATEGORY_ERROR_PARAMS Bad or missing parameters. */
define('CATEGORY_ERROR_PARAMS', -2);

/** @constant CATEGORY_ERROR_CONNECT Connection failure. */
define('CATEGORY_ERROR_CONNECT', -3);

/** @constant CATEGORY_ERROR_AUTH Authentication failure. */
define('CATEGORY_ERROR_AUTH', -4);

/** @constant CATEGORY_ERROR_EMPTY Empty retrieval result. */
define('CATEGORY_ERROR_EMPTY', -5);

/** @constant CATEGORY_ERROR_UNSUPPORTED Method not supported by driver. */
define('CATEGORY_ERROR_UNSUPPORTED', -6);

/** @constant CATEGORY_ERROR_ALREADYEXISTS The category already exists. */
define('CATEGORY_ERROR_ALREADYEXISTS', -7);

/** @constant CATEGORY_ERROR_NOTEXIST The category/parent doesn't exist. */
define('CATEGORY_ERROR_NOTEXIST', -8);

/** @constant CATEGORY_ERROR_REMOVE You must remove before every childs!. */
define('CATEGORY_ERROR_REMOVE', -9);

//Kind of category

/** @constant CATEGORY_GROUPID_GROUP Group of groups (for users) or users*/
define('CATEGORY_GROUPID_GROUP', 1);

/** @constant CATEGORY_GROUPID_MENU Define a menu, like PEAR/HTML/menu.php */
define('CATEGORY_GROUPID_MENU', 2);

/** @constant CATEGORY_GROUPID_BOOKMARK Define categories for boommarks */
define('CATEGORY_GROUPID_BOOKMARK', 3);


//Kind of format
/** @constant CATEGORY_FORMAT_LIST List every category in an array */
define('CATEGORY_FORMAT_LIST', 1);

/** @constant CATEGORY_FORMAT_TREE List every category in an array,
	similar to PEAR/html/menu.php*/

define('CATEGORY_FORMAT_TREE', 2);

/**
  * The Category:: class provides a common abstracted interface into the
  * various backends for the Horde authentication system.
  *
  * @author  Stephane Huther <shuther@bigfoot.com>
  * @version $Revision: 1. $
  * @since   Horde 1.3
  * @package horde.category
  */
class Category {

	/**
	* Array of all categories: indexed by name=parent
	*/
     var $categories = Array();

     /**
      * Hash containing connection parameters.
      * @var array $params
      */
     var $params = Array();

	/**
	* Constructor
       * @param array  $params A hash containing any additional
       *                       configuration or connection parameters a 
subclass
       *                       might need.
       *                       here, we need  'groupid' = a constant that 
defines
       *                       in each group we will work
	*/
	function Category($params)
	{
		$this->params = $params;
	}

     /**
      * Attempts to return a concrete Category instance based on $driver.
      *
      * @param string $driver The type of concrete Category subclass to return.
      *                       This is based on the storage driver 
($driver). The
      *                       code is dynamically included.
      * @param array  $params A hash containing any additional
      *                       configuration or connection parameters a subclass
      *                       might need.
      *                       here, we need  'groupid' = a constant that 
defines
      *                       in each group we will work
      *
      * @return object Category   The newly created concrete Category instance,
      *                       or false on an error.
      * @see constants CATEGORY_GROUPID_*
      */
     function factory($driver, $params)
     {
         $driver = strtolower($driver);

         if (empty($driver) || (strcmp($driver, 'none') == 0)) {
             return new Category($params);
         }

         if (include_once dirname(__FILE__) . '/Category/' . $driver . 
'.php') {
             $class = 'Category_' . $driver;
             return new $class($params);
         } else {
             return false;
         }
     }

     /**
      * Attempts to return a reference to a concrete Category instance based on
      * $driver. It will only create a new instance if no Category instance
      * with the same parameters currently exists.
      *
      * This should be used if multiple permissions sources (and, thus,
      * multiple Category instances) are required.
      *
      * This method must be invoked as: $var = &Category::singleton()
      *
      * @param string $driver The type of concrete Category subclass to return.
      *                       This is based on the storage driver 
($driver). The
      *                       code is dynamically included.
      * @param array  $params (optional) A hash containing any additional
      *                       configuration or connection parameters a subclass
      *                       might need.
      *
      * @return object Category  The concrete Auth reference, or false on an
      *                        error.
      */
     function &singleton($driver, $params = array())
     {
         static $instances;

         if (!isset($instances)) $instances = array();

         $signature = md5(strtolower($driver) . '][' . implode('][', $params));
         if (!isset($instances[$signature])) {
             $instances[$signature] = Category::factory($driver, $params);
         }

         return $instances[$signature];
     }

     /**
      * Add a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param optional string $parent	the name of the parent category
      */
      function add ($name, $parent=-1)
	{
	     return $this->protected_add($name, $parent);
	}

     /**
      * Add a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param optional string $parent	the name of the parent category
      *
      * @access protected
      */
      function protected_add ($name, $parent=-1)
	{
		if (isset($this->categories[$name]))
			return CATEGORY_ERROR_ALREADYEXISTS;

		if ($parent!=-1 && !isset($this->categories[$parent]))
			return CATEGORY_ERROR_NOTEXIST;

		$this->categories[$name]=$parent;
		return CATEGORY_OK;
	}

     /**
      * Remove a group
      *
      * @param string $name       The name of the category.
      */
      function remove ($name)
	{
	     return $this->protected_remove($name);
	}

     /**
      * Remove a group
      *
      * @param string $name       The name of the category.
      * @access protected
      */
      function protected_remove ($name)
	{
		if (!isset($this->categories[$name]))
		     return CATEGORY_ERROR_NOTEXIST;

		if (in_array($name, array_values($this->categories)))
		     return CATEGORY_ERROR_REMOVE;

		$this->categories[$name]=null;
		unset($this->categories[$name]);
		return CATEGORY_OK;
	}

     /**
      * Move a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param string $new_parent The name of the new parent.
      */
      function move ($name,$new_parent)
	{
	     return protected_move($name,$new_parent);
	}

     /**
      * Move a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param string $new_parent The name of the new parent.
      *
      * @access protected
      */
      function protected_move ($name,$new_parent)
	{
		if (!isset($this->categories[$name]))
			return CATEGORY_ERROR_NOTEXIST;
		if ($new_parent!=-1 && !isset($this->categories[$new_parent]))
			return CATEGORY_ERROR_NOTEXIST;

		$this->categories[$name]=$new_parent;
		return CATEGORY_OK;
	}

     /**
      * Export a list of categories
      *
      * @param integer $format       Format of the export
      * @param string optional $parent The name of the parent from
      *                                where we export.
      *
      * @return mixed
      */
	function &export ($format,$parent=-1)
	{
		$out=array();

		switch ($format)
		{
		case CATEGORY_FORMAT_LIST:
			$this->extractAllLevelList($out, $parent);
			break;
		case CATEGORY_FORMAT_TREE:
			$this->extractAllLevelTree($out, $parent);
			break;
		default:
			return CATEGORY_ERROR_PARAMS;
		}

		return $out;

	}

     /**
      * Extract one level of categories, based on a parent, get the childs
      *
      * @param string optional $parent The name of the parent from
      *                                where we begin.
      *
      * @return array
      */
	function extract1Level($parent=-1)
	{
		$out=array();
		foreach ($this->categories as $name=>$vparent)
		{
			if ($vparent==$parent)
			{
				if (!isset($out[$parent])) $out[$parent]=array();
				$out[$parent][$name]=true;
			}
		}
		return $out;
	}


     /**
      * Extract all level of categories, based on a parent
      * Tree format
      *
      * @param array $out	Contain the result
      * @param string optional $parent The name of the parent from
      *                                where we begin.
      *
      */
	function extractAllLevelTree(&$out, $parent=-1)
	{

		$k=$this->extract1Level($parent);
		if (!isset($k[$parent]))
			return;

		$k=$k[$parent];
		foreach ($k as $name=>$v)
		{
			if (!isset($out[$parent]) || !is_array($out[$parent])) 
$out[$parent]=array();
				$out[$parent][$name]=true;
				$this->extractAllLevelTree($out[$parent], $name);
		}
	}

     /**
      * Extract all level of categories, based on a parent
      * List format
      *
      * @param string optional $parent The name of the parent from
      *                                where we begin.
      * @param array $out	Contain the result
      *
      */
	function extractAllLevelList(&$out, $parent=-1)
	{

		$k=$this->extract1Level($parent);
		if (!isset($k[$parent]))
			return;

		$k=$k[$parent];

		foreach ($k as $name=>$v)
		{
			printf ('%s - %s<br>',$parent, $name);

			if (!isset($out[$parent])) $out[$parent]=array();
			if (!isset($out[$parent][$name]))
			{
				$out[$parent][$name]=true;
				$this->extractAllLevelList($out, $name);
			}
		}
	}

     /**
      * Get a list of parents, based on a child
      *
      * @param string $child The name of the child
      *
      * @return array
      */
	function getParents($child)
	{
		if (!isset($this->categories[$child]))
		return CATEGORY_ERROR_NOTEXIST;

		return $this->categories[$child];
	}
}

?>


----
- lib/Category/sql.php
<?php
// $Horde: horde/lib/Category/sql.php,v 1.7 2001/07/16 21:28:48 shuther Exp $

/**
  * The Categoryh_sql class provides a sql implementation of the Horde
  * caqtegory system.
  *
  * Required values for $params:
  *      'phptype'       The database type (ie. 'pgsql', 'mysql, etc.).
  *      'hostspec'      The hostname of the database server.
  *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
  *      'username'      The username with which to connect to the database.
  *      'password'      The password associated with 'username'.
  *      'database'      The name of the database.
  *      'table'         The name of the preferences table in 'database'.
  *      'sequenceName'  The name of the sequence to compute an ID
  *
  * The table structure for the preferences is as follows:
  *
  *CREATE TABLE category (
  * id INT  not null ,
  * groupid MEDIUMINT  not null ,
  * name VARCHAR (250)  not null ,
  * parent INT not null  ,
  * PRIMARY KEY (id)
  * )
  * comment = 'Horde Cateogory Manager';
  * ALTER TABLE category ADD INDEX(groupid);
  * ALTER TABLE category ADD UNIQUE(name,groupid);
  *
  * @author  Stephane Huther <shuther@bigfoot.com>
  * @version $Revision: 1.0 $
  * @since   Horde 1.3
  * @package horde.category
  *
  *
  */
class Category_sql extends Category {

     /**
      * Handle for the current database connection.
      * @var resource $db
      */
     var $db;

     /**
      * Boolean indicating whether or not we're connected to the SQL server.
      * @var boolean $connected
      */
     var $connected = false;


     /**
      * Constructs a new SQL authentication object.
      *
      * @param array $params   A hash containing connection parameters.
      */
     function Category_sql($params)
     {
         $this->Category($params);
     }


     /**
      * Attempts to open a persistent connection to the SQL server.
      *
      * @return constant CATEGORY_OK on success, CATEGORY_ERROR_* on failure.
      */
     function connect()
     {
	
         if (!$this->connected) {
             if (!is_array($this->params)) return CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['phptype'])) return 
CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['hostspec'])) return 
CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['username'])) return 
CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['password'])) return 
CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['database'])) return 
CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['table'])) return CATEGORY_ERROR_PARAMS;
             if (!isset($this->params['sequenceName'])) return 
CATEGORY_ERROR_PARAMS;

             /* Connect to the SQL server using the supplied parameters. */
             include_once 'DB.php';
             $this->db = &DB::connect($this->params, true);
             if (DB::isError($this->db) || DB::isWarning($this->db)) {
                 return CATEGORY_ERROR_CONNECT;
             }

             $this->connected = true;
		$r=$this->fullyload();	//Load all the database in memory
		if ($r!=CATEGORY_OK)
			return $r;
         }

         return CATEGORY_OK;
     }

     /**
      * Disconnect from the SQL server and clean up the connection.
      *
      * @return boolean true on success, false on failure.
      */
     function disconnect()
     {
         if ($this->connected) {
             $this->connected = false;
             return $this->db->disconnect();
         }

         return true;
     }

     /**
      * Dump the database to the array of this class
      * it is a way to do a one way synchronize
      *
      * @return mixed may return a DB error if not an array
      *
      * note: there is no check against circular reference!!!
      */
	function fullyload()
	{
		$query=sprintf('SELECT t.name name, t2.name parent FROM %s t, %s t2 WHERE 
t.groupid=%s and t.parent=t2.id and t2.groupid=%s ', 
$this->params['table'], $this->params['table'], $this->params['groupid'], 
$this->params['groupid']);

         /* Execute the query. */
         $result = $this->db->getAssoc($query);

         if (DB::isError($result)) {
		return $result;
	  }
	  $this->categories=$result;

	$query=sprintf('SELECT t.name name, -1 parent FROM %s t WHERE t.groupid=%s 
and t.parent="-1"', $this->params['table'], $this->params['groupid']);

         /* Execute the query. */
         $result2 = $this->db->getAssoc($query);

         if (DB::isError($result2)) {
		return $result2;
         }
	  $this->categories=array_merge($result,$result2);
	return CATEGORY_OK;

	}

     /**
      * Add a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param optional string $parent	the name of the parent category
      */
      function add ($name, $parent=-1)
	{
           if ($this->connect() != AUTH_OK) {
             // PEAR error
             return false;
         }

		if (isset($this->categories[$name]))
			return CATEGORY_ERROR_ALREADYEXISTS;

		if ($parent!=-1 && !isset($this->categories[$parent]))
			return CATEGORY_ERROR_NOTEXIST;

		$id=$this->db->nextId($this->params['sequenceName']);
		if (DB::isError($id))
		{
			return $id; //Error
		}

		if (-1==$parent)
		{
			$query=sprintf('INSERT INTO %s (id, groupid, name, parent) VALUES (%s, 
%s, "%s", -1)', $this->params['table'], $id, $this->params['groupid'], 
$this->db->quoteString($name));
		}
		else
		{
			$query=sprintf('SELECT t.id id FROM %s t WHERE t.name="%s"', 
$this->params['table'], $this->db->quoteString($parent));
			$parentid=$this->db->getCol($query);
		        if (DB::isError($parentid))
			{
				  return $parentid; //Error
			  }
			if (0==$parentid) $parentid[0]=-1;

			$query=sprintf('INSERT INTO %s (id, groupid, name, parent) VALUES (%s, 
%s, "%s", %s)', $this->params['table'], $id, $this->params['groupid'], 
$this->db->quoteString($name), $parentid[0]);
		}

         /* Execute the query. */
         $result = $this->db->query($query);

         if (!DB::isError($result)) {
   		$this->categories[$name]=$parent;
             return CATEGORY_OK;
         }
		return CATEGORY_ERROR;
	}

     /**
      * Remove a group
      *
      * @param string $name       The name of the category.
      */
      function remove ($name)
	{
		if (!isset($this->categories[$name]))
		     return CATEGORY_ERROR_NOTEXIST;
		if (in_array($name, array_values($this->categories)))
		     return CATEGORY_ERROR_REMOVE;

		$query=sprintf('DELETE FROM %s WHERE groupid=%s AND name="%s"', 
$this->params['table'], $this->params['groupid'], 
$this->db->quoteString($name));

         /* Execute the query. */
         $result = $this->db->query($query);

         if (!DB::isError($result)) {
		$this->categories[$name]=null;
		unset($this->categories[$name]);
             return CATEGORY_OK;
         }
	return CATEGORY_ERROR;

	}

     /**
      * Move a category
      *
      * note: there is no check against circular reference!!!
      * @param string $name       The name of the category.
      * @param string $new_parent The name of the new parent.
      */
      function move ($name,$new_parent=-1)
	{
		if (!isset($this->categories[$name]))
			return CATEGORY_ERROR_NOTEXIST;
		if ($new_parent!=-1 && !isset($this->categories[$new_parent]))
			return CATEGORY_ERROR_NOTEXIST;

		if (-1==$new_parent)
		{
			$query=sprintf('UPDATE %s SET parent="-1" WHERE groupid=%s AND 
name="%s"', $this->params['table'], $this->params['groupid'], 
$this->db->quoteString($name));
		}
		else
		{
			$query=sprintf('SELECT t.id id FROM %s t WHERE t.name="%s" AND 
t.groupid=%s', $this->params['table'], $this->db->quoteString($new_parent), 
$this->params['groupid']);
			$parentid=$this->db->getCol($query);
			if (DB::isError($parentid))
			{
				return $parentid; //Error
			}

			$query=sprintf('UPDATE %s SET parent=%s WHERE groupid=%s AND name="%s"', 
$this->params['table'], $parentid[0], $this->params['groupid'], 
$this->db->quoteString($name));
		}

         /* Execute the query. */
         $result = $this->db->query($query);

         if (!DB::isError($result)) {
		$this->categories[$name]=$new_parent;
		return CATEGORY_OK;
	  }

	return CATEGORY_ERROR;

	}
}

?>

--
we have to add in lib/base.php
require_once HORDE_BASE . '/lib/Category.php';

and in horde.php.dist
/**
  ** Horde Category Manager
  **/

// What backend should we use for category to Horde? Valid
// options are currently 'none' and 'sql'.
// In the case of none, the categories are kept in the page/memory.
// They are destroyed just after
$conf['category']['driver'] = 'sql';

// An array holding any parameters that the Category object will need to
// function correctly.
$conf['category']['params'] = array();
$conf['category']['params']['phptype'] = 'mysql';
$conf['category']['params']['hostspec'] = 'localhost';
$conf['category']['params']['username'] = 'login';
$conf['category']['params']['password'] = 'password';
$conf['category']['params']['database'] = 'horde';
$conf['category']['params']['table'] = 'category';
$conf['category']['params']['sequenceName'] = 'categorySQ';

-----------
Exemple:
	$driver = $conf['category']['driver'];
	$params = $conf['category']['params'];
	$params['groupid']= CATEGORY_GROUPID_GROUP;

	$categories = &Category::singleton($driver, $params);
	$categories->add('name A');
	$categories->add('name B');

	$categories->add('name C','name A');
	$categories->add('name D','name A');
	$categories->add('name E','name C');
	$categories->add('name F','name Z');

	$categories->move('name C','name D');

	var_dump($categories->export(CATEGORY_FORMAT_TREE));

---
Any comments are welcomed