3 * Two classes, Category and CategoryList, to deal with categories. To reduce
4 * code duplication, most of the logic is implemented for lists of categories,
5 * and then single categories are a special case. We use a separate class for
6 * CategoryList so as to discourage stupid slow memory-hogging stuff like manu-
7 * ally iterating through arrays of Titles and Articles, which we do way too
8 * much, when a smarter class can do stuff all in one query.
10 * Category(List) objects are immutable, strictly speaking. If you call me-
11 * thods that change the database, like to refresh link counts, the objects
12 * will be appropriately reinitialized. Member variables are lazy-initialized.
14 * TODO: Move some stuff from CategoryPage.php to here, and use that.
19 abstract class CategoryListBase
{
20 # FIXME: Is storing all member variables as simple arrays a good idea?
21 # Should we use some kind of associative array instead?
22 /** Names of all member categories, normalized to DB-key form */
23 protected $mNames = null;
24 /** IDs of all member categories */
25 protected $mIDs = null;
27 * Counts of membership (cat_pages, cat_subcats, cat_files) for all member
30 protected $mPages = null, $mSubcats = null, $mFiles = null;
32 protected function __construct() {}
34 /** See CategoryList::newFromNames for details. */
35 protected function setNames( $names ) {
36 if( !is_array( $names ) ) {
37 throw new MWException( __METHOD__
.' passed non-array' );
39 $this->mNames
= array_diff(
41 array( 'CategoryListBase', 'setNamesCallback' ),
49 * @param string $name Name of a putative category
50 * @return mixed Normalized name, or false if the name was invalid.
52 private static function setNamesCallback( $name ) {
53 $title = Title
::newFromText( "Category:$name" );
54 if( !is_object( $title ) ) {
57 return $title->getDBKey();
61 * Set up all member variables using a database query.
62 * @return bool True on success, false on failure.
64 protected function initialize() {
65 if( $this->mNames
=== null && $this->mIDs
=== null ) {
66 throw new MWException( __METHOD__
.' has both names and IDs null' );
68 $dbr = wfGetDB( DB_SLAVE
);
69 if( $this->mIDs
=== null ) {
70 $where = array( 'cat_title' => $this->mNames
);
71 } elseif( $this->mNames
=== null ) {
72 $where = array( 'cat_id' => $this->mIDs
);
79 array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats',
84 if( !$res->fetchRow() ) {
85 # Okay, there were no contents. Nothing to initialize.
89 $this->mIDs
= $this->mNames
= $this->mPages
= $this->mSubcats
=
90 $this->mFiles
= array();
91 while( $row = $res->fetchRow() ) {
92 $this->mIDs
[]= $row['cat_id'];
93 $this->mNames
[]= $row['cat_title'];
94 $this->mPages
[]= $row['cat_pages'];
95 $this->mSubcats
[]= $row['cat_subcats'];
96 $this->mFiles
[]= $row['cat_files'];
102 /** @todo make iterable. */
103 class CategoryList
extends CategoryListBase
{
105 * Factory function. Any provided elements that don't correspond to a cat-
106 * egory that actually exists will be silently dropped. FIXME: Is this
107 * sane error-handling?
109 * @param array $names An array of category names. They need not be norma-
110 * lized, with spaces replaced by underscores.
111 * @return CategoryList
113 public static function newFromNames( $names ) {
115 $cat->setNames( $names );
120 * Factory function. Any provided elements that don't correspond to a cat-
121 * egory that actually exists will be silently dropped. FIXME: Is this
122 * sane error-handling?
124 * @param array $ids An array of category ids
125 * @return CategoryList
127 public static function newFromIDs( $ids ) {
128 if( !is_array( $ids ) ) {
129 throw new MWException( __METHOD__
.' passed non-array' );
136 /** @return array Simple array of DB key names */
137 public function getNames() {
139 return $this->mNames
;
142 * FIXME: Is this a good return type?
144 * @return array Associative array of DB key name => ID
146 public function getIDs() {
148 return array_fill_keys( $this->mNames
, $this->mIDs
);
151 * FIXME: Is this a good return type?
153 * @return array Associative array of DB key name => array(pages, subcats,
156 public function getCounts() {
159 foreach( array_keys( $this->mNames
) as $i ) {
160 $ret[$this->mNames
[$i]] = array(
170 class Category
extends CategoryListBase
{
174 * @param array $name A category name (no "Category:" prefix). It need
175 * not be normalized, with spaces replaced by underscores.
176 * @return mixed Category, or false on a totally invalid name
178 public static function newFromName( $name ) {
180 $cat->setNames( array( $name ) );
181 if( count( $cat->mNames
) !== 1 ) {
190 * @param array $id A category id
193 public static function newFromIDs( $id ) {
195 $cat->mIDs
= array( $id );
199 /** @return mixed DB key name, or false on failure */
200 public function getName() { return $this->getX( 'mNames' ); }
201 /** @return mixed Category ID, or false on failure */
202 public function getID() { return $this->getX( 'mIDs' ); }
203 /** @return mixed Total number of member pages, or false on failure */
204 public function getPageCount() { return $this->getX( 'mPages' ); }
205 /** @return mixed Number of subcategories, or false on failure */
206 public function getSubcatCount() { return $this->getX( 'mSubcats' ); }
207 /** @return mixed Number of member files, or false on failure */
208 public function getFileCount() { return $this->getX( 'mFiles' ); }
211 * This is not implemented in the base class, because arrays of Titles are
214 * @return mixed The Title for this category, or false on failure.
216 public function getTitle() {
217 if( !$this->initialize() ) {
220 return Title
::makeTitleSafe( NS_CATEGORY
, $this->mNames
[0] );
223 /** Generic accessor */
224 private function getX( $key ) {
225 if( !$this->initialize() ) {
228 return $this->{$key}[0];
232 * Override the parent class so that we can return false if things muck
233 * up, i.e., the name/ID we got was invalid. Currently CategoryList si-
234 * lently eats errors so as not to kill the whole array for one bad name.
236 * @return bool True on success, false on failure.
238 protected function initialize() {
239 parent
::initialize();
240 if( count( $this->mNames
) != 1 ||
count( $this->mIDs
) != 1 ) {
247 * Refresh the counts for this category.
249 * FIXME: If there were some way to do this in MySQL 4 without an UPDATE
250 * for every row, it would be nice to move this to the parent class.
252 * @return bool True on success, false on failure
254 public function refreshCounts() {
258 $dbw = wfGetDB( DB_MASTER
);
260 # Note, we must use names for this, since categorylinks does.
261 if( $this->mNames
=== null ) {
262 if( !$this->initialize() ) {
266 # Let's be sure that the row exists in the table. We don't need to
267 # do this if we got the row from the table in initialization!
270 array( 'cat_title' => $this->mNames
[0] ),
276 $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY
, 1, 'NULL' );
277 $cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE
, 1, 'NULL' );
278 $result = $dbw->selectRow(
279 array( 'categorylinks', 'page' ),
280 array( 'COUNT(*) AS pages',
281 "COUNT($cond1) AS subcats",
282 "COUNT($cond2) AS files"
284 array( 'cl_to' => $this->mNames
[0], 'page_id = cl_from' ),
291 'cat_pages' => $result->pages
,
292 'cat_subcats' => $result->subcats
,
293 'cat_files' => $result->files
295 array( 'cat_title' => $this->mNames
[0] ),
300 # Now we should update our local counts.
301 $this->mPages
= array( $result->pages
);
302 $this->mSubcats
= array( $result->subcats
);
303 $this->mFiles
= array( $result->files
);