MediaWiki  master
Category.php
Go to the documentation of this file.
1 <?php
29 class Category {
31  private $mName = null;
32  private $mID = null;
37  private $mTitle = null;
39  private $mPages = null, $mSubcats = null, $mFiles = null;
40 
41  const LOAD_ONLY = 0;
42  const LAZY_INIT_ROW = 1;
43 
44  const ROW_COUNT_SMALL = 100;
45 
46  private function __construct() {
47  }
48 
55  protected function initialize( $mode = self::LOAD_ONLY ) {
56  if ( $this->mName === null && $this->mID === null ) {
57  throw new MWException( __METHOD__ . ' has both names and IDs null' );
58  } elseif ( $this->mID === null ) {
59  $where = [ 'cat_title' => $this->mName ];
60  } elseif ( $this->mName === null ) {
61  $where = [ 'cat_id' => $this->mID ];
62  } else {
63  # Already initialized
64  return true;
65  }
66 
67  $dbr = wfGetDB( DB_REPLICA );
68  $row = $dbr->selectRow(
69  'category',
70  [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
71  $where,
72  __METHOD__
73  );
74 
75  if ( !$row ) {
76  # Okay, there were no contents. Nothing to initialize.
77  if ( $this->mTitle ) {
78  # If there is a title object but no record in the category table,
79  # treat this as an empty category.
80  $this->mID = false;
81  $this->mName = $this->mTitle->getDBkey();
82  $this->mPages = 0;
83  $this->mSubcats = 0;
84  $this->mFiles = 0;
85 
86  # If the title exists, call refreshCounts to add a row for it.
87  if ( $mode === self::LAZY_INIT_ROW && $this->mTitle->exists() ) {
88  DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
89  }
90 
91  return true;
92  } else {
93  return false; # Fail
94  }
95  }
96 
97  $this->mID = $row->cat_id;
98  $this->mName = $row->cat_title;
99  $this->mPages = $row->cat_pages;
100  $this->mSubcats = $row->cat_subcats;
101  $this->mFiles = $row->cat_files;
102 
103  # (T15683) If the count is negative, then 1) it's obviously wrong
104  # and should not be kept, and 2) we *probably* don't have to scan many
105  # rows to obtain the correct figure, so let's risk a one-time recount.
106  if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
107  $this->mPages = max( $this->mPages, 0 );
108  $this->mSubcats = max( $this->mSubcats, 0 );
109  $this->mFiles = max( $this->mFiles, 0 );
110 
111  if ( $mode === self::LAZY_INIT_ROW ) {
112  DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
113  }
114  }
115 
116  return true;
117  }
118 
126  public static function newFromName( $name ) {
127  $cat = new self();
129 
130  if ( !is_object( $title ) ) {
131  return false;
132  }
133 
134  $cat->mTitle = $title;
135  $cat->mName = $title->getDBkey();
136 
137  return $cat;
138  }
139 
146  public static function newFromTitle( $title ) {
147  $cat = new self();
148 
149  $cat->mTitle = $title;
150  $cat->mName = $title->getDBkey();
151 
152  return $cat;
153  }
154 
161  public static function newFromID( $id ) {
162  $cat = new self();
163  $cat->mID = intval( $id );
164  return $cat;
165  }
166 
179  public static function newFromRow( $row, $title = null ) {
180  $cat = new self();
181  $cat->mTitle = $title;
182 
183  # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
184  # all the cat_xxx fields being null, if the category page exists, but nothing
185  # was ever added to the category. This case should be treated link an empty
186  # category, if possible.
187 
188  if ( $row->cat_title === null ) {
189  if ( $title === null ) {
190  # the name is probably somewhere in the row, for example as page_title,
191  # but we can't know that here...
192  return false;
193  } else {
194  # if we have a title object, fetch the category name from there
195  $cat->mName = $title->getDBkey();
196  }
197 
198  $cat->mID = false;
199  $cat->mSubcats = 0;
200  $cat->mPages = 0;
201  $cat->mFiles = 0;
202  } else {
203  $cat->mName = $row->cat_title;
204  $cat->mID = $row->cat_id;
205  $cat->mSubcats = $row->cat_subcats;
206  $cat->mPages = $row->cat_pages;
207  $cat->mFiles = $row->cat_files;
208  }
209 
210  return $cat;
211  }
212 
216  public function getName() {
217  return $this->getX( 'mName' );
218  }
219 
223  public function getID() {
224  return $this->getX( 'mID' );
225  }
226 
230  public function getPageCount() {
231  return $this->getX( 'mPages' );
232  }
233 
237  public function getSubcatCount() {
238  return $this->getX( 'mSubcats' );
239  }
240 
244  public function getFileCount() {
245  return $this->getX( 'mFiles' );
246  }
247 
251  public function getTitle() {
252  if ( $this->mTitle ) {
253  return $this->mTitle;
254  }
255 
256  if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
257  return false;
258  }
259 
260  $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
261  return $this->mTitle;
262  }
263 
271  public function getMembers( $limit = false, $offset = '' ) {
272  $dbr = wfGetDB( DB_REPLICA );
273 
274  $conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ];
275  $options = [ 'ORDER BY' => 'cl_sortkey' ];
276 
277  if ( $limit ) {
278  $options['LIMIT'] = $limit;
279  }
280 
281  if ( $offset !== '' ) {
282  $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
283  }
284 
285  $result = TitleArray::newFromResult(
286  $dbr->select(
287  [ 'page', 'categorylinks' ],
288  [ 'page_id', 'page_namespace', 'page_title', 'page_len',
289  'page_is_redirect', 'page_latest' ],
290  $conds,
291  __METHOD__,
292  $options
293  )
294  );
295 
296  return $result;
297  }
298 
304  private function getX( $key ) {
305  if ( $this->{$key} === null && !$this->initialize( self::LAZY_INIT_ROW ) ) {
306  return false;
307  }
308  return $this->{$key};
309  }
310 
316  public function refreshCounts() {
317  if ( wfReadOnly() ) {
318  return false;
319  }
320 
321  # If we have just a category name, find out whether there is an
322  # existing row. Or if we have just an ID, get the name, because
323  # that's what categorylinks uses.
324  if ( !$this->initialize( self::LOAD_ONLY ) ) {
325  return false;
326  }
327 
328  $dbw = wfGetDB( DB_MASTER );
329  # Avoid excess contention on the same category (T162121)
330  $name = __METHOD__ . ':' . md5( $this->mName );
331  $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 );
332  if ( !$scopedLock ) {
333  return false;
334  }
335 
336  $dbw->startAtomic( __METHOD__ );
337 
338  // Lock the `category` row before locking `categorylinks` rows to try
339  // to avoid deadlocks with LinksDeletionUpdate (T195397)
340  $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
341 
342  // Lock all the `categorylinks` records and gaps for this category;
343  // this is a separate query due to postgres limitations
344  $dbw->selectRowCount(
345  [ 'categorylinks', 'page' ],
346  '*',
347  [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
348  __METHOD__,
349  [ 'LOCK IN SHARE MODE' ]
350  );
351  // Get the aggregate `categorylinks` row counts for this category
352  $catCond = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], 1, 'NULL' );
353  $fileCond = $dbw->conditional( [ 'page_namespace' => NS_FILE ], 1, 'NULL' );
354  $result = $dbw->selectRow(
355  [ 'categorylinks', 'page' ],
356  [
357  'pages' => 'COUNT(*)',
358  'subcats' => "COUNT($catCond)",
359  'files' => "COUNT($fileCond)"
360  ],
361  [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
362  __METHOD__
363  );
364 
365  $shouldExist = $result->pages > 0 || $this->getTitle()->exists();
366 
367  if ( $this->mID ) {
368  if ( $shouldExist ) {
369  # The category row already exists, so do a plain UPDATE instead
370  # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
371  # in the cat_id sequence. The row may or may not be "affected".
372  $dbw->update(
373  'category',
374  [
375  'cat_pages' => $result->pages,
376  'cat_subcats' => $result->subcats,
377  'cat_files' => $result->files
378  ],
379  [ 'cat_title' => $this->mName ],
380  __METHOD__
381  );
382  } else {
383  # The category is empty and has no description page, delete it
384  $dbw->delete(
385  'category',
386  [ 'cat_title' => $this->mName ],
387  __METHOD__
388  );
389  $this->mID = false;
390  }
391  } elseif ( $shouldExist ) {
392  # The category row doesn't exist but should, so create it. Use
393  # upsert in case of races.
394  $dbw->upsert(
395  'category',
396  [
397  'cat_title' => $this->mName,
398  'cat_pages' => $result->pages,
399  'cat_subcats' => $result->subcats,
400  'cat_files' => $result->files
401  ],
402  [ 'cat_title' ],
403  [
404  'cat_pages' => $result->pages,
405  'cat_subcats' => $result->subcats,
406  'cat_files' => $result->files
407  ],
408  __METHOD__
409  );
410  // @todo: Should we update $this->mID here? Or not since Category
411  // objects tend to be short lived enough to not matter?
412  }
413 
414  $dbw->endAtomic( __METHOD__ );
415 
416  # Now we should update our local counts.
417  $this->mPages = $result->pages;
418  $this->mSubcats = $result->subcats;
419  $this->mFiles = $result->files;
420 
421  return true;
422  }
423 
435  public function refreshCountsIfEmpty() {
436  return $this->refreshCountsIfSmall( 0 );
437  }
438 
452  public function refreshCountsIfSmall( $maxSize = self::ROW_COUNT_SMALL ) {
453  $dbw = wfGetDB( DB_MASTER );
454  $dbw->startAtomic( __METHOD__ );
455 
456  $typeOccurances = $dbw->selectFieldValues(
457  'categorylinks',
458  'cl_type',
459  [ 'cl_to' => $this->getName() ],
460  __METHOD__,
461  [ 'LIMIT' => $maxSize + 1 ]
462  );
463 
464  if ( !$typeOccurances ) {
465  $doRefresh = true; // delete any category table entry
466  } elseif ( count( $typeOccurances ) <= $maxSize ) {
467  $countByType = array_count_values( $typeOccurances );
468  $doRefresh = !$dbw->selectField(
469  'category',
470  '1',
471  [
472  'cat_title' => $this->getName(),
473  'cat_pages' => $countByType['page'] ?? 0,
474  'cat_subcats' => $countByType['subcat'] ?? 0,
475  'cat_files' => $countByType['file'] ?? 0
476  ],
477  __METHOD__
478  );
479  } else {
480  $doRefresh = false; // category is too big
481  }
482 
483  $dbw->endAtomic( __METHOD__ );
484 
485  if ( $doRefresh ) {
486  $this->refreshCounts(); // update the row
487 
488  return true;
489  }
490 
491  return false;
492  }
493 }
$mName
Name of the category, normalized to DB-key form.
Definition: Category.php:31
static newFromRow( $row, $title=null)
Factory function, for constructing a Category object from a result set.
Definition: Category.php:179
static newFromName( $name)
Factory function.
Definition: Category.php:126
const LOAD_ONLY
Definition: Category.php:41
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
Title $mTitle
Category page title.
Definition: Category.php:37
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
getSubcatCount()
Definition: Category.php:237
refreshCountsIfEmpty()
Call refreshCounts() if there are no entries in the categorylinks table or if the category table has ...
Definition: Category.php:435
static newFromID( $id)
Factory function.
Definition: Category.php:161
const DB_MASTER
Definition: defines.php:26
getMembers( $limit=false, $offset='')
Fetch a TitleArray of up to $limit category members, beginning after the category sort key $offset...
Definition: Category.php:271
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getTitle()
Definition: Category.php:251
wfReadOnly()
Check whether the wiki is in read-only mode.
refreshCounts()
Refresh the counts for this category.
Definition: Category.php:316
getFileCount()
Definition: Category.php:244
const NS_CATEGORY
Definition: Defines.php:74
$mPages
Counts of membership (cat_pages, cat_subcats, cat_files)
Definition: Category.php:39
static newFromResult( $res)
Definition: TitleArray.php:42
getPageCount()
Definition: Category.php:230
const NS_FILE
Definition: Defines.php:66
getX( $key)
Generic accessor.
Definition: Category.php:304
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:612
const LAZY_INIT_ROW
Definition: Category.php:42
__construct()
Definition: Category.php:46
const DB_REPLICA
Definition: defines.php:25
refreshCountsIfSmall( $maxSize=self::ROW_COUNT_SMALL)
Call refreshCounts() if there are few entries in the categorylinks table.
Definition: Category.php:452
const ROW_COUNT_SMALL
Definition: Category.php:44
initialize( $mode=self::LOAD_ONLY)
Set up all member variables using a database query.
Definition: Category.php:55