User:Socoljam/DynamicArticleList (enhanced)/CategoryTravelerBase.php

From Meta, a Wikimedia project coordination wiki

Save this code as CategoryTravelerBase.php in your extensions or includes directory.

<?php
require_once( 'CategoryNode.php' );

/**
* Base Class of Category Traveler
*
* @package MediaWiki
* @author Zeng Ji(zengji@gmail.com)
*/
abstract class CategoryTravelerBase {
	var $dbr;
	var $sPageTable;
	var $sCategorylinksTable;
	
	// All category nodes organized by a list. Key is CategoryName, Content is CategoryNode.
	// This variable is used to quick-orientation while constructing category tree.
	var $categoryList;
	
	// All category nodes organized by a tree. Key is CategoryName, Content is CategoryNode.
	var $categoryTree;
	
	function __construct() {
		$this->dbr =& wfGetDB( DB_SLAVE );
		$this->sPageTable = $this->dbr->tableName( 'page' );
		$this->sCategorylinksTable = $this->dbr->tableName( 'categorylinks' );
		$this->categoryList = array();
		$this->categoryTree = array();
	}

	// Allow user to define which category should the traveler start from.
	// If not defined, traveler will start from all level 1 (no parent) categories in datebase.
	function buildCategoryTree($categoryRoot=false) {
		unset($this->categoryList);
		unset($this->categoryTree);
		$this->categoryList = array();
		$this->categoryTree = array();
		$fname = get_class($this) . '::BuildCategoryTree';
		$dbr = $this->dbr;
		
		// Construct all category nodes, and fill in categoryList.
		$sql = $this->genSQLcategoryAll();
		$res = $dbr->query($sql, $fname);
		while( $obj = $dbr->fetchObject( $res ) ) {
			$cId = $obj->categoryId;
			$cName = $obj->categoryName;
			$this->categoryList[$cName] = new CategoryNode($cId, $cName);
		}
		
		$dbr->freeResult( $res );
		if ($categoryRoot == false) {
			// Get level 1 (no parent) categories to travel from.
			$sql = $this->genSQLcategoryTopLevel($this->getNotTopCategoryList());
		} else {
			// Get defined category to travel from.
			$categoryRoot = str_replace( ' ', '_', $categoryRoot);
			$sql = $this->genSQLcategoryByName($categoryRoot);
		}
		
		$res = $dbr->query($sql, $fname);
		while( $obj = $dbr->fetchObject( $res ) ) {
			$cId = $obj->categoryId;
			$cName = $obj->categoryName;
			// Generate first level categories in tree.
			$categoryNode = $this->categoryList[$cName];
			if (isset($categoryNode)) {
				$this->categoryTree[$cName] = $categoryNode;
			}
			// Recursion to build children tree.
			$this->buildOneCategory(false, $cName);
		}
		
		$dbr->freeResult( $res );
	}
	
	// Recursion Function£ºProcess current category and all its children.
	// $cParent: The parent of current processing category node.
	// If current processing category node is top level node, "cParent" should be set to false.
	// $cName: The name of current processing category node.
	function buildOneCategory($cParent, $cName) {
		$categoryNode = $this->categoryList[$cName];
		if (isset($categoryNode)) {
			// Avoid the same category node to be travelled more than once.
			// For view of "CategoryTravelerBase", all categories should only has one parent or not.
			// Although maybe in fact some categories will have more than one parents.
			$categoryNode->hasTravelled = true;
			// Get all children categories, establish children/parent relationship.
			$categoryNode->parent = $cParent;
			$fname = get_class($this) . '::buildOneCategory';
			$dbr = $this->dbr;
			$sql = $this->genSQLcategoryChildren($cName);
			$res = $dbr->query($sql, $fname);
			if ($dbr->numRows( $res ) == 0) {
				// No children categories, recursion ends here.
				$dbr->freeResult( $res );
				return;
			}
			$categoryNode->children = array();
			while( $obj = $dbr->fetchObject( $res ) ) {
				$childName = $obj->categoryName;
				$childNode = $this->categoryList[$childName];
				if ($childNode->hasTravelled == true)
					continue;
				$categoryNode->children[$childName] = $childNode;
				$this->buildOneCategory($childNode, $childName);
			}
		$dbr->freeResult( $res );
		}
	}
	
	// Deep recursion to build category tree.
	function travelCategoryTree() {
		$this->travelStart();
		// "0" means "Travel from Root"
		$this->travelOneCategory(0);
		$this->travelEnd();
	}
	
	function travelOneCategory($level, $categoryNode=false) {
		if ($level==0) {
			// Get TOP Level Categories
			$categoryList = $this->categoryTree;
		} else {
			// Get Next Level Categories
			$categoryList = $categoryNode->children;
		}
		// Process Current Category
		if ($categoryNode != false)
		$this->travel($level, $categoryNode);
		// Process Children of Current Category
		if ($categoryList != false) {
			$categoryCount = count($categoryList);
			$index = 0;
			foreach($categoryList as $cName => $cNode) {
				if ($index == 0)
					$this->travelBeforeFirst($level+1, $cNode);
				$this->travelOneCategory($level+1, $cNode);
				$index = $index + 1;
				if ($index == $categoryCount)
					$this->travelAfterLast($level+1, $cNode);
			}
		}
	}
	
	abstract function travelStart();
	abstract function travelEnd();
	abstract function travelBeforeFirst($level, $categoryNode); // Before travel the first category in one level
	abstract function travel($level, $categoryNode);
	abstract function travelAfterLast($level, $categoryNode); // After travel the last category in one level
	
	/////////// Utility Function ////////////////
	// Generate SQL: Retrieve category information by name
	function genSQLcategoryByName($cName) {
		$sql = "SELECT page_id AS categoryId, page_title AS categoryName ";
		$sql.= "FROM $this->sPageTable ";
		$sql.= "WHERE page_namespace=" . NS_CATEGORY . " AND page_title=\"" . $cName . "\"";
		return $sql;
	}
	
	// Generate SQL: Retrieve children categories by name
	function genSQLcategoryChildren($cName) {
		$sql = "SELECT cl.cl_from AS categoryId, page_title AS categoryName ";
		$sql.= "FROM $this->sPageTable INNER JOIN $this->sCategorylinksTable AS cl ";
		$sql.= "ON page_id=cl.cl_from AND (page_namespace=" . NS_CATEGORY . ") ";
		$sql.= "AND (cl.cl_to='" . $cName . "') ";
		$sql.= "ORDER BY page_title";
		return $sql;
	}
	
	// Generate SQL: Retrieve all "non-TOP level" categories.
	// "non-TOP level" means has parents.
	function genSQLcategoryNotTopLevel() {
		$sql = "SELECT cl.cl_from AS categoryId, page_title AS categoryName ";
		$sql.= "FROM $this->sPageTable INNER JOIN $this->sCategorylinksTable AS cl ";
		$sql.= "ON page_id=cl.cl_from AND (page_namespace=" . NS_CATEGORY . ") ";
		$sql.= "ORDER BY page_title";
		return $sql;
	}
	
	// Generate SQL: Retrieve all categories.
	function genSQLcategoryAll() {
		$sql = "SELECT page_id AS categoryId, page_title AS categoryName ";
		$sql.= "FROM $this->sPageTable ";
		$sql.= "WHERE page_namespace=" . NS_CATEGORY . " ";
		$sql.= "ORDER BY page_title";
		return $sql;
	}
	
	// Generate SQL: Retrieve all "TOP level" categories.
	// Use "NOT IN" clause in SQL query. "excludeList" should be all "non-TOP level" categories' ID.
	function genSQLcategoryTopLevel($excludeList) {
		$sql = "SELECT page_id AS categoryId, page_title AS categoryName ";
		$sql.= "FROM $this->sPageTable ";
		if ($excludeList) {
			$sql.= "WHERE page_namespace=" . NS_CATEGORY . " AND (page_id NOT IN (" . $excludeList . "))";
		} else {
			$sql.= "WHERE page_namespace=" . NS_CATEGORY;
		}
		return $sql;
	}
	
	// Return a string which contains all "non-TOP level" categories' ID.
	function getNotTopCategoryList() {
		$fname = get_class($this) . '::getNotTopCategoryList';
		$sql = $this->genSQLcategoryNotTopLevel();
		$dbr = $this->dbr;
		$res = $dbr->query($sql, $fname);
		
		// Query result is blank.
		if ($dbr->numRows( $res ) == 0) {
			$dbr->freeResult( $res );
			return false;
		}
		// Generate a string of categories' ID list.
		// For example: 1234,3721,4567
		$ret = '';
		while( $obj = $dbr->fetchObject( $res ) ) {
			if( isset( $obj->categoryId ) ) {
				$ret .= $obj->categoryId . ',';
			}
		}
		$ret = substr($ret, 0, strlen($ret)-1); // Delete tail comma
		$dbr->freeResult( $res );
		return $ret;
	}
}
?>