MediaWiki  master
Category.php
Go to the documentation of this file.
1 <?php
26 
32 class 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 
50  private $loadBalancer;
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();
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 }
Category\getTitle
getTitle()
Definition: Category.php:258
Category\ROW_COUNT_SMALL
const ROW_COUNT_SMALL
Definition: Category.php:47
Category\refreshCountsIfEmpty
refreshCountsIfEmpty()
Call refreshCounts() if there are no entries in the categorylinks table or if the category table has ...
Definition: Category.php:442
Category\getID
getID()
Definition: Category.php:230
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:42
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:157
Category\$mName
$mName
Name of the category, normalized to DB-key form.
Definition: Category.php:34
Category
Category objects are immutable, strictly speaking.
Definition: Category.php:32
NS_FILE
const NS_FILE
Definition: Defines.php:75
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1126
Category\LAZY_INIT_ROW
const LAZY_INIT_ROW
Definition: Category.php:45
Category\initialize
initialize( $mode=self::LOAD_ONLY)
Set up all member variables using a database query.
Definition: Category.php:62
$dbr
$dbr
Definition: testCompression.php:54
MWException
MediaWiki exception.
Definition: MWException.php:29
Category\refreshCountsIfSmall
refreshCountsIfSmall( $maxSize=self::ROW_COUNT_SMALL)
Call refreshCounts() if there are few entries in the categorylinks table.
Definition: Category.php:459
Category\getX
getX( $key)
Generic accessor.
Definition: Category.php:311
Category\$mFiles
$mFiles
Definition: Category.php:42
Category\$mID
$mID
Definition: Category.php:35
Category\refreshCounts
refreshCounts()
Refresh the counts for this category.
Definition: Category.php:323
Category\getName
getName()
Definition: Category.php:223
Category\getSubcatCount
getSubcatCount()
Definition: Category.php:244
$title
$title
Definition: testCompression.php:38
Category\getMembers
getMembers( $limit=false, $offset='')
Fetch a TitleArray of up to $limit category members, beginning after the category sort key $offset.
Definition: Category.php:278
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
DB_MASTER
const DB_MASTER
Definition: defines.php:26
Category\$mSubcats
$mSubcats
Definition: Category.php:42
Category\newFromRow
static newFromRow( $row, $title=null)
Factory function, for constructing a Category object from a result set.
Definition: Category.php:186
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:153
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
Category\getPageCount
getPageCount()
Definition: Category.php:237
Category\LOAD_ONLY
const LOAD_ONLY
Definition: Category.php:44
Category\newFromID
static newFromID( $id)
Factory function.
Definition: Category.php:168
Title
Represents a title within MediaWiki.
Definition: Title.php:41
Category\getFileCount
getFileCount()
Definition: Category.php:251
Category\$loadBalancer
ILoadBalancer $loadBalancer
Definition: Category.php:50
Category\$mTitle
Title $mTitle
Category page title.
Definition: Category.php:40
Category\__construct
__construct()
Definition: Category.php:52
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:133
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
Category\$mPages
$mPages
Counts of membership (cat_pages, cat_subcats, cat_files)
Definition: Category.php:42