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

From Meta, a Wikimedia project coordination wiki
Jump to: navigation, search

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;
        }
}
?>