Subversion Repositories php_library

Rev

Rev 51 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

<?php

/*
 * 20190510 - RWR
 * Modified to allow us to pass in a list of acceptable keys
 * This allows us to restrict access to menu items
 */


if ( isset($TESTING) ) {
   mysql_connect("localhost", "test", "test") or die(mysql_error());
   mysql_select_db("camp") or die(mysql_error());
   ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . '/home/rodolico/Desktop/wip/common-cgi' );
}
include_once("library.php");

/* 
   class reads a database table and converts it to an Hierarchical hash (aka, parent/child).
   Table structure is assume to be:
      create table menu (
         id        int unsigned not null,
         parent_id int unsigned not null default 0,
         other columns below
      )
   where id is a unique identifier and parent_id is points to id in another row (zero indicating it is a top level)
   NOTE: column names may be changed. Also, while it is assumed id and parent_id are int's, it is quite likely it will
         work with other column types, including varchar
   Uses include any parent/child relationship, to an arbitrary depth. See class DBMenu for one example
   
   This class was written to be generic. The algorithm used attempts to not take advantage of any special language 
   capabilities, and should be generic. However, various languages can optimize this capability by using capabilities unique
   to the language in question. For an example, see David North's PERL implementation which utilizes references to allow for
   a single pass to convert from an array to a Hierarchical structure. Taking advantage of this allows his script to run
   use no additional memory (this algorithm will double the amount of memory used for the initial array) and does in a single
   iterative pass what the recursive function copyChildren does.
*/

class DBHierarchicalHash {
   protected $tableName; // table name that data will be read from
   protected $keyField;  // name of the primary id field in table
   protected $parentFieldName; // name of the field used to point to the parent of the current row
   protected $rootNodevalue; // the value (default 0) of the indicator that a row has no parent. THIS MUST NOT EXIST in the id column of the table
   protected $inputRecords; // storage for the records read in.
   protected $recordList; // if this is an array, will restrict records read to only these indicies
   protected $query; // the query used to get the records
   
   public function __construct( $tableName, $keyField = 'id', $parentFieldName = 'parent_id', $rootNodeValue = 0, $recordList = null ) {
      // standard variable load
      $this->tableName = $tableName;
      $this->keyField = $keyField;
      $this->parentFieldName = $parentFieldName;
      $this->rootNodevalue = $rootNodeValue;
      $this->recordList = $recordList;
      // load the data from the database into memory
      // upon completion, will have data in a single hash of hashes (not hiearchy)
      // will also create an emtpy entry for "element 0"
      $this->loadData();
      // find the children of each node
      $this->findChildren();
      // now that we know who all the children are, let's actually make them part of the structure
      $this->inputRecords = $this->copyChildren($this->inputRecords[$this->rootNodevalue]);
      // all we care about are the children of the root node, so simply assign that to inputRecords
      $this->inputRecords = $this->inputRecords['children'];
   } // function __construct
   
   
   /*
      Goes through each node and reverses the "who is my parent" to the parent's "who are my children"
      It does not populate the children; it simply creates an empty array for each child to be processed
      later.
   */
   private function findChildren () {
      // following loop takes inputRecords and creates (empty) elements for each child
      foreach ($this->inputRecords as $key => $values) { // go through every node
         if ( isset ($this->inputRecords[$key][$this->parentFieldName]) ) { // if it has a parent
            // tell parent about itself
            $this->inputRecords[$values[$this->parentFieldName]]['children'][$key] = array();
         }
      }
   } // function findChildren
   
   /* 
      simply returns inputRecords for processing by calling routine. This is the goal of this class, to create
      this structure from the database structure defined above
   */
   public function toHash () {
      return $this->inputRecords;
   } // function toHash

   /* 
      loads data from database. The column defined in $this->keyField will become the index into the hash, and all
      additional columns will simply be added to the array pointed by it.
      An additional entry in the table will be created with $this->rootNodeValue as its index, to allow root level
      items to have a parent.
   */
   private function loadData ( ) {
      $this->query = "select * from $this->tableName";
      if ( ! is_null( $this->recordList ) )
         $this->query .= " where $this->keyField in (" . implode( ',',$this->recordList ) . ')';
      $inputRows = queryDatabaseExtended( $this->query ); // get everything from the table
      $inputRows = $inputRows['data']; // we only care about the data
      $this->inputRecords = array(); // initialize inputRecords to empty
      // main loop, will read the query results in one row at a time
      foreach ( $inputRows as $thisRow ) {
         foreach ($thisRow as $key => $values) { // copy each value
            if ($key != $this->keyField) { // unless it is the key field
               // this is more complex as a parent of null might be used. Thus, we check for a null and, if
               // it is used, we set it to the rootNodeValue
               if ($key == $this->parentFieldName && ! $thisRow[$key] ) {
                  $this->inputRecords[$thisRow[$this->keyField]][$key] = $this->rootNodevalue;
               } else {
                  $this->inputRecords[$thisRow[$this->keyField]][$key] = $thisRow[$key];
               }
            }
         }
      }
      $this->inputRecords[$this->rootNodevalue] = array(); // create our empty root node
   } // function loadData
   
   # populates the hiearchy created in load
   # NOTE: if the current node also has children, it will recursively do those
   # main call is &copyChildren( rootNode )
   private function copyChildren($currentNode) {
      # for each child
      //print "Looking at: "; print_r($currentNode);
      if ( isset($currentNode['children']) ) {
         foreach ($currentNode['children'] as $key => $values) {
            # copy any children of this child table that may exist
            $this->inputRecords[$key] = $this->copyChildren( $this->inputRecords[$key] );
            # copy each value from the node to this sub-node
            foreach ($this->inputRecords[$key] as $child => $childValue) {
               if ($child != $this->parentFieldName ) {
                  $currentNode['children'][$key][$child] = $childValue;
               }
            }
         }
      }
      return $currentNode;
   } // function copyChildren
} // class DBHierarchicalHash


/*
   simple extension of DBHierarchicalHash allowing a menu structure to be stored in a database.
   the structure for the database is simple:
   create table menu (
      menu_id     int unsigned not null auto_increment,
      parent_id   int unsigned not null default 0,
      caption     varchar(20) not null,
      url         varchar(64),
      primary key (menu_id)
   )
   where caption is the string displayed on the menu, and url is the (possibly null) url
   to be called when the link is clicked by the user. The resulting line is something like:
      <a href="url">caption</a>
   though this can be modified by changing $menuItemString and $menuHeaderString (see below)
   
   menu_id is a unique identifier for a single row, and parent_id points to the menu that it
   is a submenu of (0 indicates a root menu item)
   
   Note: I tried to avoid any constants in the class, so column names, menu_id data type, root
         menu indicator are all modifiable
*/
class DBMenu extends DBHierarchicalHash {
   
   protected  $captionName = 'caption'; // column name for display string in database
   protected  $urlName = 'url';         // column name for URL field in database
   // string which is searched/replaced for a menu item that has a URL (<url> and <caption> are replaced)
   protected  $menuItemString = '<li class="menu_item_<level>"><a href="<url>"><caption></a></li>';
   // string which is searched/replaced for a menu item that has no URL (<caption> is replaced)
   protected  $menuHeaderString = '<li class="menu_header_<level>"><caption></li>';
   // string which is placed around <menublock>, ie this goes around a menu/submenu. <level> can be used to determine
   // which level (zero based) we are in the menu (level = 0 is top menu)
   protected  $menuBlockString = '<ul class="menu"><menublock></ul>';
   
   // simply pass fields on to DBHierarchicalHash so it can load and parse the table
   public function __construct ($tableName = 'menu', $idFieldName = 'id', $parentFieldName = 'parent_id', $recordList = null ) {
      parent::__construct($tableName, $idFieldName, $parentFieldName, 0, $recordList );
   }
   
   // simple setter/getter for the caption column name in table
   public function captionColumnName ( $newValue = '' ) {
      if ($newValue) {
         $this->captionName = $newValue;
      }
      return $this->captionName;
   }
   
   // simple setter/getter for url column name in table
   public function urlColumnName ( $newValue = '' ) {
      if ($newValue) {
         $this->urlName = $newValue;
      }
      return $this->urlName;
   }
   
   // simple setter/getter for menuItemString for output
   public function menuItemString ( $newValue = '' ) {
      if ($newValue) {
         $this->menuItemString = $newValue;
      }
      return $this->menuItemString;
   }
   
   // simple setter/getter for menuHeaderString for output
   public function menuHeaderString ( $newValue = '' ) {
      if ($newValue) {
         $this->menuHeaderString = $newValue;
      }
      return $this->menuHeaderString;
   }
   
   // simple setter/getter for menu block string for output
   public function menuBlockString ( $newValue = '' ) {
      if ($newValue) {
         $this->menuBlockString = $newValue;
      }
      return $this->menuBlockString;
   }
   
   // just an entry point to displayMenu, with root of inputRecords and level 0
   function DBMenu2String ( $rootDir = '' ) {
      return $this->htmlMenu( $this->inputRecords, 0, $rootDir );
   }
   
   // function takes a menu level and creates an HTML Menu items from it. If a node has children, will
   // recursively call itself for each child node.
   private function htmlMenu ($menu, $level=0, $rootDir = '' ) {
      $result = '';
      foreach ($menu as $key => $value) { // process each array entry
         if ($value[$this->urlName]) { // this is a link, so it is a live menu option
            $result .= insertValuesIntoQuery( $this->menuItemString, array( 'url' => $rootDir . $value[$this->urlName], 'caption' => $value[$this->captionName], 'level' => $level) );
         } else { // not a link, so just create the text
            $result .= insertValuesIntoQuery( $this->menuHeaderString, array( 'caption' => $value[$this->captionName], 'level' => $level) );
         }
         if ( isset($value['children'])) { // if it has children, process them
            $result .=  $this->htmlMenu($value['children'], $level+1, $rootDir);
         }
      }
      // place the block code around the menu, and return the result
      return insertValuesIntoQuery( $this->menuBlockString, array( 'menublock' => $result, 'level' => $level ) );
   }
} // class DBMenu

/* following block is for testing. comment out for production */
//$menu = new DBHierarchicalHash ('menu', 'menu_id');
//print_r( $menu );

//$menu = new DBMenu( 'menu', 'menu_id' );
//print $menu->DBMenu2String();
//print "Menu Structure:\n"; print_r($menu);

/* end of block for testing */

?>