This is a schema change. It's only a table creation, but the table must be created...
[lhc/web/wiklou.git] / includes / Category.php
1 <?php
2 /**
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.
9 *
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.
13 *
14 * TODO: Move some stuff from CategoryPage.php to here, and use that.
15 *
16 * @author Simetrical
17 */
18
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;
26 /**
27 * Counts of membership (cat_pages, cat_subcats, cat_files) for all member
28 * categories
29 */
30 protected $mPages = null, $mSubcats = null, $mFiles = null;
31
32 protected function __construct() {}
33
34 /** See CategoryList::newFromNames for details. */
35 protected function setNames( $names ) {
36 if( !is_array( $names ) ) {
37 throw new MWException( __METHOD__.' passed non-array' );
38 }
39 $this->mNames = array_diff(
40 array_map(
41 array( 'CategoryListBase', 'setNamesCallback' ),
42 $names
43 ),
44 array( false )
45 );
46 }
47
48 /**
49 * @param string $name Name of a putative category
50 * @return mixed Normalized name, or false if the name was invalid.
51 */
52 private static function setNamesCallback( $name ) {
53 $title = Title::newFromText( $name );
54 if( !is_object( $title ) ) {
55 return false;
56 }
57 return $title->getDBKey();
58 }
59
60 /**
61 * Set up all member variables using a database query.
62 * @return bool True on success, false on failure.
63 */
64 protected function initialize() {
65 if( $this->mNames === null && $this->mIDs === null ) {
66 throw new MWException( __METHOD__.' has both names and IDs null' );
67 }
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 );
73 } else {
74 # Already initialized
75 return true;
76 }
77 $res = $dbr->select(
78 'category',
79 array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats',
80 'cat_files' ),
81 $where,
82 __METHOD__
83 );
84 if( !$res->fetchRow() ) {
85 # Okay, there were no contents. Nothing to initialize.
86 return false;
87 }
88 $res->rewind();
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'];
97 }
98 $res->free();
99 }
100 }
101
102 /** @todo make iterable. */
103 class CategoryList extends CategoryListBase {
104 /**
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?
108 *
109 * @param array $names An array of category names. They need not be norma-
110 * lized, with spaces replaced by underscores.
111 * @return CategoryList
112 */
113 public static function newFromNames( $names ) {
114 $cat = new self();
115 $cat->setNames( $names );
116 return $cat;
117 }
118
119 /**
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?
123 *
124 * @param array $ids An array of category ids
125 * @return CategoryList
126 */
127 public static function newFromIDs( $ids ) {
128 if( !is_array( $ids ) ) {
129 throw new MWException( __METHOD__.' passed non-array' );
130 }
131 $cat = new self();
132 $cat->mIds = $ids;
133 return $cat;
134 }
135
136 /** @return array Simple array of DB key names */
137 public function getNames() {
138 $this->initialize();
139 return $this->mNames;
140 }
141 /**
142 * FIXME: Is this a good return type?
143 *
144 * @return array Associative array of DB key name => ID
145 */
146 public function getIDs() {
147 $this->initialize();
148 return array_fill_keys( $this->mNames, $this->mIDs );
149 }
150 /**
151 * FIXME: Is this a good return type?
152 *
153 * @return array Associative array of DB key name => array(pages, subcats,
154 * files)
155 */
156 public function getCounts() {
157 $this->initialize();
158 $ret = array();
159 foreach( array_keys( $this->mNames ) as $i ) {
160 $ret[$this->mNames[$i]] = array(
161 $this->mPages[$i],
162 $this->mSubcats[$i],
163 $this->mFiles[$i]
164 );
165 }
166 return $ret;
167 }
168 }
169
170 class Category extends CategoryListBase {
171 /**
172 * Factory function.
173 *
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
177 */
178 public static function newFromName( $name ) {
179 $cat = new self();
180 $cat->setNames( array( $name ) );
181 if( count( $cat->mNames ) !== 1 ) {
182 return false;
183 }
184 return $cat;
185 }
186
187 /**
188 * Factory function.
189 *
190 * @param array $id A category id
191 * @return Category
192 */
193 public static function newFromIDs( $id ) {
194 $cat = new self();
195 $cat->mIDs = array( $id );
196 return $cat;
197 }
198
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' ); }
209 /**
210 * This is not implemented in the base class, because arrays of Titles are
211 * evil.
212 *
213 * @return mixed The Title for this category, or false on failure.
214 */
215 public function getTitle() {
216 if( !$this->initialize() ) {
217 return false;
218 }
219 # FIXME is there a better way to do this?
220 return Title::newFromText( "Category:{$this->mNames[0]}" );
221 }
222
223 /** Generic accessor */
224 private function getX( $key ) {
225 if( !$this->initialize() ) {
226 return false;
227 }
228 return $this->{$key}[0];
229 }
230
231 /**
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.
235 *
236 * @return bool True on success, false on failure.
237 */
238 protected function initialize() {
239 parent::initialize();
240 if( count( $this->mNames ) != 1 || count( $this->mIDs ) != 1 ) {
241 return false;
242 }
243 return true;
244 }
245
246 /**
247 * Refresh the counts for this category.
248 *
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.
251 *
252 * @return bool True on success, false on failure
253 */
254 public function refreshCounts() {
255 if( wfReadOnly() ) {
256 return false;
257 }
258 $dbw = wfGetDB( DB_MASTER );
259 $dbw->begin();
260 # Note, we must use names for this, since categorylinks does.
261 if( $this->mNames === null ) {
262 if( !$this->initialize() ) {
263 return false;
264 }
265 } else {
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!
268 $dbw->insert(
269 'category',
270 array( 'cat_title' => $this->mNames[0] ),
271 __METHOD__,
272 'IGNORE'
273 );
274 }
275
276 $result = $dbw->selectRow(
277 array( 'categorylinks', 'page' ),
278 array( 'COUNT(*) AS pages',
279 'COUNT(IF(page_namespace='.NS_CATEGORY.',1,NULL)) AS subcats',
280 'COUNT(IF(page_namespace='.NS_IMAGE.',1,NULL)) AS files'
281 ),
282 array( 'cl_to' => $this->mNames[0], 'page_id = cl_from' ),
283 __METHOD__,
284 'LOCK IN SHARE MODE'
285 );
286 $ret = $dbw->update(
287 'category',
288 array(
289 'cat_pages' => $result->pages,
290 'cat_subcats' => $result->subcats,
291 'cat_files' => $result->files
292 ),
293 array( 'cat_title' => $this->mNames[0] ),
294 __METHOD__
295 );
296 $dbw->commit();
297
298 # Now we should update our local counts.
299 $this->mPages = array( $result->pages );
300 $this->mSubcats = array( $result->subcats );
301 $this->mFiles = array( $result->files );
302
303 return $ret;
304 }
305 }