MediaWiki fundraising/REL1_35
Category.php
Go to the documentation of this file.
1<?php
26
32class Category {
34 private $mName = null;
35 private $mID = null;
40 private $mTitle = null;
42 private $mPages = null, $mSubcats = null, $mFiles = null;
43
44 protected const LOAD_ONLY = 0;
45 protected const LAZY_INIT_ROW = 1;
46
47 public const ROW_COUNT_SMALL = 100;
48
51
52 private function __construct() {
53 $this->loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
54 }
55
62 protected function initialize( $mode = self::LOAD_ONLY ) {
63 if ( $this->mName === null && $this->mID === null ) {
64 throw new MWException( __METHOD__ . ' has both names and IDs null' );
65 } elseif ( $this->mID === null ) {
66 $where = [ 'cat_title' => $this->mName ];
67 } elseif ( $this->mName === null ) {
68 $where = [ 'cat_id' => $this->mID ];
69 } else {
70 # Already initialized
71 return true;
72 }
73
74 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
75 $row = $dbr->selectRow(
76 'category',
77 [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
78 $where,
79 __METHOD__
80 );
81
82 if ( !$row ) {
83 # Okay, there were no contents. Nothing to initialize.
84 if ( $this->mTitle ) {
85 # If there is a title object but no record in the category table,
86 # treat this as an empty category.
87 $this->mID = false;
88 $this->mName = $this->mTitle->getDBkey();
89 $this->mPages = 0;
90 $this->mSubcats = 0;
91 $this->mFiles = 0;
92
93 # If the title exists, call refreshCounts to add a row for it.
94 if ( $mode === self::LAZY_INIT_ROW && $this->mTitle->exists() ) {
95 DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
96 }
97
98 return true;
99 } else {
100 return false; # Fail
101 }
102 }
103
104 $this->mID = $row->cat_id;
105 $this->mName = $row->cat_title;
106 $this->mPages = $row->cat_pages;
107 $this->mSubcats = $row->cat_subcats;
108 $this->mFiles = $row->cat_files;
109
110 # (T15683) If the count is negative, then 1) it's obviously wrong
111 # and should not be kept, and 2) we *probably* don't have to scan many
112 # rows to obtain the correct figure, so let's risk a one-time recount.
113 if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
114 $this->mPages = max( $this->mPages, 0 );
115 $this->mSubcats = max( $this->mSubcats, 0 );
116 $this->mFiles = max( $this->mFiles, 0 );
117
118 if ( $mode === self::LAZY_INIT_ROW ) {
119 DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
120 }
121 }
122
123 return true;
124 }
125
133 public static function newFromName( $name ) {
134 $cat = new self();
135 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
136
137 if ( !is_object( $title ) ) {
138 return false;
139 }
140
141 $cat->mTitle = $title;
142 $cat->mName = $title->getDBkey();
143
144 return $cat;
145 }
146
153 public static function newFromTitle( $title ) {
154 $cat = new self();
155
156 $cat->mTitle = $title;
157 $cat->mName = $title->getDBkey();
158
159 return $cat;
160 }
161
168 public static function newFromID( $id ) {
169 $cat = new self();
170 $cat->mID = intval( $id );
171 return $cat;
172 }
173
186 public static function newFromRow( $row, $title = null ) {
187 $cat = new self();
188 $cat->mTitle = $title;
189
190 # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
191 # all the cat_xxx fields being null, if the category page exists, but nothing
192 # was ever added to the category. This case should be treated link an empty
193 # category, if possible.
194
195 if ( $row->cat_title === null ) {
196 if ( $title === null ) {
197 # the name is probably somewhere in the row, for example as page_title,
198 # but we can't know that here...
199 return false;
200 } else {
201 # if we have a title object, fetch the category name from there
202 $cat->mName = $title->getDBkey();
203 }
204
205 $cat->mID = false;
206 $cat->mSubcats = 0;
207 $cat->mPages = 0;
208 $cat->mFiles = 0;
209 } else {
210 $cat->mName = $row->cat_title;
211 $cat->mID = $row->cat_id;
212 $cat->mSubcats = $row->cat_subcats;
213 $cat->mPages = $row->cat_pages;
214 $cat->mFiles = $row->cat_files;
215 }
216
217 return $cat;
218 }
219
223 public function getName() {
224 return $this->getX( 'mName' );
225 }
226
230 public function getID() {
231 return $this->getX( 'mID' );
232 }
233
237 public function getPageCount() {
238 return $this->getX( 'mPages' );
239 }
240
244 public function getSubcatCount() {
245 return $this->getX( 'mSubcats' );
246 }
247
251 public function getFileCount() {
252 return $this->getX( 'mFiles' );
253 }
254
258 public function getTitle() {
259 if ( $this->mTitle ) {
260 return $this->mTitle;
261 }
262
263 if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
264 return false;
265 }
266
267 $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
268 return $this->mTitle;
269 }
270
278 public function getMembers( $limit = false, $offset = '' ) {
279 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
280
281 $conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ];
282 $options = [ 'ORDER BY' => 'cl_sortkey' ];
283
284 if ( $limit ) {
285 $options['LIMIT'] = $limit;
286 }
287
288 if ( $offset !== '' ) {
289 $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
290 }
291
292 $result = TitleArray::newFromResult(
293 $dbr->select(
294 [ 'page', 'categorylinks' ],
295 [ 'page_id', 'page_namespace', 'page_title', 'page_len',
296 'page_is_redirect', 'page_latest' ],
297 $conds,
298 __METHOD__,
299 $options
300 )
301 );
302
303 return $result;
304 }
305
311 private function getX( $key ) {
312 if ( $this->{$key} === null && !$this->initialize( self::LAZY_INIT_ROW ) ) {
313 return false;
314 }
315 return $this->{$key};
316 }
317
323 public function refreshCounts() {
324 if ( wfReadOnly() ) {
325 return false;
326 }
327
328 # If we have just a category name, find out whether there is an
329 # existing row. Or if we have just an ID, get the name, because
330 # that's what categorylinks uses.
331 if ( !$this->initialize( self::LOAD_ONLY ) ) {
332 return false;
333 }
334
335 $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
336 # Avoid excess contention on the same category (T162121)
337 $name = __METHOD__ . ':' . md5( $this->mName );
338 $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 );
339 if ( !$scopedLock ) {
340 return false;
341 }
342
343 $dbw->startAtomic( __METHOD__ );
344
345 // Lock the `category` row before locking `categorylinks` rows to try
346 // to avoid deadlocks with LinksDeletionUpdate (T195397)
347 $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
348
349 // Lock all the `categorylinks` records and gaps for this category;
350 // this is a separate query due to postgres limitations
351 $dbw->selectRowCount(
352 [ 'categorylinks', 'page' ],
353 '*',
354 [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
355 __METHOD__,
356 [ 'LOCK IN SHARE MODE' ]
357 );
358 // Get the aggregate `categorylinks` row counts for this category
359 $catCond = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], '1', 'NULL' );
360 $fileCond = $dbw->conditional( [ 'page_namespace' => NS_FILE ], '1', 'NULL' );
361 $result = $dbw->selectRow(
362 [ 'categorylinks', 'page' ],
363 [
364 'pages' => 'COUNT(*)',
365 'subcats' => "COUNT($catCond)",
366 'files' => "COUNT($fileCond)"
367 ],
368 [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
369 __METHOD__
370 );
371
372 $shouldExist = $result->pages > 0 || $this->getTitle()->exists();
373
374 if ( $this->mID ) {
375 if ( $shouldExist ) {
376 # The category row already exists, so do a plain UPDATE instead
377 # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
378 # in the cat_id sequence. The row may or may not be "affected".
379 $dbw->update(
380 'category',
381 [
382 'cat_pages' => $result->pages,
383 'cat_subcats' => $result->subcats,
384 'cat_files' => $result->files
385 ],
386 [ 'cat_title' => $this->mName ],
387 __METHOD__
388 );
389 } else {
390 # The category is empty and has no description page, delete it
391 $dbw->delete(
392 'category',
393 [ 'cat_title' => $this->mName ],
394 __METHOD__
395 );
396 $this->mID = false;
397 }
398 } elseif ( $shouldExist ) {
399 # The category row doesn't exist but should, so create it. Use
400 # upsert in case of races.
401 $dbw->upsert(
402 'category',
403 [
404 'cat_title' => $this->mName,
405 'cat_pages' => $result->pages,
406 'cat_subcats' => $result->subcats,
407 'cat_files' => $result->files
408 ],
409 'cat_title',
410 [
411 'cat_pages' => $result->pages,
412 'cat_subcats' => $result->subcats,
413 'cat_files' => $result->files
414 ],
415 __METHOD__
416 );
417 // @todo: Should we update $this->mID here? Or not since Category
418 // objects tend to be short lived enough to not matter?
419 }
420
421 $dbw->endAtomic( __METHOD__ );
422
423 # Now we should update our local counts.
424 $this->mPages = $result->pages;
425 $this->mSubcats = $result->subcats;
426 $this->mFiles = $result->files;
427
428 return true;
429 }
430
442 public function refreshCountsIfEmpty() {
443 return $this->refreshCountsIfSmall( 0 );
444 }
445
459 public function refreshCountsIfSmall( $maxSize = self::ROW_COUNT_SMALL ) {
460 $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
461 $dbw->startAtomic( __METHOD__ );
462
463 $typeOccurances = $dbw->selectFieldValues(
464 'categorylinks',
465 'cl_type',
466 [ 'cl_to' => $this->getName() ],
467 __METHOD__,
468 [ 'LIMIT' => $maxSize + 1 ]
469 );
470
471 if ( !$typeOccurances ) {
472 $doRefresh = true; // delete any category table entry
473 } elseif ( count( $typeOccurances ) <= $maxSize ) {
474 $countByType = array_count_values( $typeOccurances );
475 $doRefresh = !$dbw->selectField(
476 'category',
477 '1',
478 [
479 'cat_title' => $this->getName(),
480 'cat_pages' => $countByType['page'] ?? 0,
481 'cat_subcats' => $countByType['subcat'] ?? 0,
482 'cat_files' => $countByType['file'] ?? 0
483 ],
484 __METHOD__
485 );
486 } else {
487 $doRefresh = false; // category is too big
488 }
489
490 $dbw->endAtomic( __METHOD__ );
491
492 if ( $doRefresh ) {
493 $this->refreshCounts(); // update the row
494
495 return true;
496 }
497
498 return false;
499 }
500}
wfReadOnly()
Check whether the wiki is in read-only mode.
Title null $mTitle
Category objects are immutable, strictly speaking.
Definition Category.php:32
static newFromID( $id)
Factory function.
Definition Category.php:168
getFileCount()
Definition Category.php:251
static newFromName( $name)
Factory function.
Definition Category.php:133
refreshCountsIfEmpty()
Call refreshCounts() if there are no entries in the categorylinks table or if the category table has ...
Definition Category.php:442
$mName
Name of the category, normalized to DB-key form.
Definition Category.php:34
static newFromTitle( $title)
Factory function.
Definition Category.php:153
initialize( $mode=self::LOAD_ONLY)
Set up all member variables using a database query.
Definition Category.php:62
$mPages
Counts of membership (cat_pages, cat_subcats, cat_files)
Definition Category.php:42
getMembers( $limit=false, $offset='')
Fetch a TitleArray of up to $limit category members, beginning after the category sort key $offset.
Definition Category.php:278
refreshCounts()
Refresh the counts for this category.
Definition Category.php:323
const LOAD_ONLY
Definition Category.php:44
getX( $key)
Generic accessor.
Definition Category.php:311
refreshCountsIfSmall( $maxSize=self::ROW_COUNT_SMALL)
Call refreshCounts() if there are few entries in the categorylinks table.
Definition Category.php:459
ILoadBalancer $loadBalancer
Definition Category.php:50
static newFromRow( $row, $title=null)
Factory function, for constructing a Category object from a result set.
Definition Category.php:186
getPageCount()
Definition Category.php:237
const LAZY_INIT_ROW
Definition Category.php:45
const ROW_COUNT_SMALL
Definition Category.php:47
__construct()
Definition Category.php:52
getSubcatCount()
Definition Category.php:244
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:42
const NS_FILE
Definition Defines.php:76
const NS_CATEGORY
Definition Defines.php:84
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29