MediaWiki REL1_37
Category.php
Go to the documentation of this file.
1<?php
27
33class Category {
35 private $mName = null;
36 private $mID = null;
41 private $mPage = null;
43 private $mPages = null, $mSubcats = null, $mFiles = null;
44
45 protected const LOAD_ONLY = 0;
46 protected const LAZY_INIT_ROW = 1;
47
48 public const ROW_COUNT_SMALL = 100;
49
52
55
56 private function __construct() {
57 $services = MediaWikiServices::getInstance();
58 $this->loadBalancer = $services->getDBLoadBalancer();
59 $this->readOnlyMode = $services->getReadOnlyMode();
60 }
61
68 protected function initialize( $mode = self::LOAD_ONLY ) {
69 if ( $this->mName === null && $this->mID === null ) {
70 throw new MWException( __METHOD__ . ' has both names and IDs null' );
71 } elseif ( $this->mID === null ) {
72 $where = [ 'cat_title' => $this->mName ];
73 } elseif ( $this->mName === null ) {
74 $where = [ 'cat_id' => $this->mID ];
75 } else {
76 # Already initialized
77 return true;
78 }
79
80 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
81 $row = $dbr->selectRow(
82 'category',
83 [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
84 $where,
85 __METHOD__
86 );
87
88 if ( !$row ) {
89 # Okay, there were no contents. Nothing to initialize.
90 if ( $this->mPage ) {
91 # If there is a page object but no record in the category table,
92 # treat this as an empty category.
93 $this->mID = false;
94 $this->mName = $this->mPage->getDBkey();
95 $this->mPages = 0;
96 $this->mSubcats = 0;
97 $this->mFiles = 0;
98
99 # If the page exists, call refreshCounts to add a row for it.
100 if ( $mode === self::LAZY_INIT_ROW && $this->mPage->exists() ) {
101 DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
102 }
103
104 return true;
105 } else {
106 return false; # Fail
107 }
108 }
109
110 $this->mID = $row->cat_id;
111 $this->mName = $row->cat_title;
112 $this->mPages = $row->cat_pages;
113 $this->mSubcats = $row->cat_subcats;
114 $this->mFiles = $row->cat_files;
115
116 # (T15683) If the count is negative, then 1) it's obviously wrong
117 # and should not be kept, and 2) we *probably* don't have to scan many
118 # rows to obtain the correct figure, so let's risk a one-time recount.
119 if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
120 $this->mPages = max( $this->mPages, 0 );
121 $this->mSubcats = max( $this->mSubcats, 0 );
122 $this->mFiles = max( $this->mFiles, 0 );
123
124 if ( $mode === self::LAZY_INIT_ROW ) {
125 DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
126 }
127 }
128
129 return true;
130 }
131
139 public static function newFromName( $name ) {
140 $cat = new self();
141 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
142
143 if ( !is_object( $title ) ) {
144 return false;
145 }
146
147 $cat->mPage = $title;
148 $cat->mName = $title->getDBkey();
149
150 return $cat;
151 }
152
159 public static function newFromTitle( PageIdentity $page ): self {
160 $cat = new self();
161
162 $cat->mPage = $page;
163 $cat->mName = $page->getDBkey();
164
165 return $cat;
166 }
167
174 public static function newFromID( $id ) {
175 $cat = new self();
176 $cat->mID = intval( $id );
177 return $cat;
178 }
179
190 public static function newFromRow( stdClass $row, ?PageIdentity $page = null ) {
191 $cat = new self();
192 $cat->mPage = $page;
193
194 # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
195 # all the cat_xxx fields being null, if the category page exists, but nothing
196 # was ever added to the category. This case should be treated link an empty
197 # category, if possible.
198
199 if ( $row->cat_title === null ) {
200 if ( $page === null ) {
201 # the name is probably somewhere in the row, for example as page_title,
202 # but we can't know that here...
203 return false;
204 } else {
205 # if we have a PageIdentity object, fetch the category name from there
206 $cat->mName = $page->getDBkey();
207 }
208
209 $cat->mID = false;
210 $cat->mSubcats = 0;
211 $cat->mPages = 0;
212 $cat->mFiles = 0;
213 } else {
214 $cat->mName = $row->cat_title;
215 $cat->mID = $row->cat_id;
216 $cat->mSubcats = $row->cat_subcats;
217 $cat->mPages = $row->cat_pages;
218 $cat->mFiles = $row->cat_files;
219 }
220
221 return $cat;
222 }
223
227 public function getName() {
228 return $this->getX( 'mName' );
229 }
230
234 public function getID() {
235 return $this->getX( 'mID' );
236 }
237
241 public function getPageCount() {
242 return $this->getX( 'mPages' );
243 }
244
248 public function getSubcatCount() {
249 return $this->getX( 'mSubcats' );
250 }
251
255 public function getFileCount() {
256 return $this->getX( 'mFiles' );
257 }
258
264 public function getPage(): ?PageIdentity {
265 if ( $this->mPage ) {
266 return $this->mPage;
267 }
268
269 if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
270 return null;
271 }
272
273 $this->mPage = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
274 return $this->mPage;
275 }
276
281 public function getTitle() {
282 return Title::castFromPageIdentity( $this->getPage() ) ?? false;
283 }
284
292 public function getMembers( $limit = false, $offset = '' ) {
293 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
294
295 $conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ];
296 $options = [ 'ORDER BY' => 'cl_sortkey' ];
297
298 if ( $limit ) {
299 $options['LIMIT'] = $limit;
300 }
301
302 if ( $offset !== '' ) {
303 $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
304 }
305
307 $dbr->select(
308 [ 'page', 'categorylinks' ],
309 [ 'page_id', 'page_namespace', 'page_title', 'page_len',
310 'page_is_redirect', 'page_latest' ],
311 $conds,
312 __METHOD__,
313 $options
314 )
315 );
316
317 return $result;
318 }
319
325 private function getX( $key ) {
326 if ( $this->{$key} === null && !$this->initialize( self::LAZY_INIT_ROW ) ) {
327 return false;
328 }
329 return $this->{$key};
330 }
331
337 public function refreshCounts() {
338 if ( $this->readOnlyMode->isReadOnly() ) {
339 return false;
340 }
341
342 # If we have just a category name, find out whether there is an
343 # existing row. Or if we have just an ID, get the name, because
344 # that's what categorylinks uses.
345 if ( !$this->initialize( self::LOAD_ONLY ) ) {
346 return false;
347 }
348
349 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
350 # Avoid excess contention on the same category (T162121)
351 $name = __METHOD__ . ':' . md5( $this->mName );
352 $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 );
353 if ( !$scopedLock ) {
354 return false;
355 }
356
357 $dbw->startAtomic( __METHOD__ );
358
359 // Lock the `category` row before locking `categorylinks` rows to try
360 // to avoid deadlocks with LinksDeletionUpdate (T195397)
361 $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
362
363 // Lock all the `categorylinks` records and gaps for this category;
364 // this is a separate query due to postgres limitations
365 $dbw->selectRowCount(
366 [ 'categorylinks', 'page' ],
367 '*',
368 [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
369 __METHOD__,
370 [ 'LOCK IN SHARE MODE' ]
371 );
372 // Get the aggregate `categorylinks` row counts for this category
373 $catCond = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], '1', 'NULL' );
374 $fileCond = $dbw->conditional( [ 'page_namespace' => NS_FILE ], '1', 'NULL' );
375 $result = $dbw->selectRow(
376 [ 'categorylinks', 'page' ],
377 [
378 'pages' => 'COUNT(*)',
379 'subcats' => "COUNT($catCond)",
380 'files' => "COUNT($fileCond)"
381 ],
382 [ 'cl_to' => $this->mName, 'page_id = cl_from' ],
383 __METHOD__
384 );
385
386 $shouldExist = $result->pages > 0 || $this->getPage()->exists();
387
388 if ( $this->mID ) {
389 if ( $shouldExist ) {
390 # The category row already exists, so do a plain UPDATE instead
391 # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
392 # in the cat_id sequence. The row may or may not be "affected".
393 $dbw->update(
394 'category',
395 [
396 'cat_pages' => $result->pages,
397 'cat_subcats' => $result->subcats,
398 'cat_files' => $result->files
399 ],
400 [ 'cat_title' => $this->mName ],
401 __METHOD__
402 );
403 } else {
404 # The category is empty and has no description page, delete it
405 $dbw->delete(
406 'category',
407 [ 'cat_title' => $this->mName ],
408 __METHOD__
409 );
410 $this->mID = false;
411 }
412 } elseif ( $shouldExist ) {
413 # The category row doesn't exist but should, so create it. Use
414 # upsert in case of races.
415 $dbw->upsert(
416 'category',
417 [
418 'cat_title' => $this->mName,
419 'cat_pages' => $result->pages,
420 'cat_subcats' => $result->subcats,
421 'cat_files' => $result->files
422 ],
423 'cat_title',
424 [
425 'cat_pages' => $result->pages,
426 'cat_subcats' => $result->subcats,
427 'cat_files' => $result->files
428 ],
429 __METHOD__
430 );
431 // @todo: Should we update $this->mID here? Or not since Category
432 // objects tend to be short lived enough to not matter?
433 }
434
435 $dbw->endAtomic( __METHOD__ );
436
437 # Now we should update our local counts.
438 $this->mPages = $result->pages;
439 $this->mSubcats = $result->subcats;
440 $this->mFiles = $result->files;
441
442 return true;
443 }
444
456 public function refreshCountsIfEmpty() {
457 return $this->refreshCountsIfSmall( 0 );
458 }
459
473 public function refreshCountsIfSmall( $maxSize = self::ROW_COUNT_SMALL ) {
474 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
475 $dbw->startAtomic( __METHOD__ );
476
477 $typeOccurances = $dbw->selectFieldValues(
478 'categorylinks',
479 'cl_type',
480 [ 'cl_to' => $this->getName() ],
481 __METHOD__,
482 [ 'LIMIT' => $maxSize + 1 ]
483 );
484
485 if ( !$typeOccurances ) {
486 $doRefresh = true; // delete any category table entry
487 } elseif ( count( $typeOccurances ) <= $maxSize ) {
488 $countByType = array_count_values( $typeOccurances );
489 $doRefresh = !$dbw->selectField(
490 'category',
491 '1',
492 [
493 'cat_title' => $this->getName(),
494 'cat_pages' => $countByType['page'] ?? 0,
495 'cat_subcats' => $countByType['subcat'] ?? 0,
496 'cat_files' => $countByType['file'] ?? 0
497 ],
498 __METHOD__
499 );
500 } else {
501 $doRefresh = false; // category is too big
502 }
503
504 $dbw->endAtomic( __METHOD__ );
505
506 if ( $doRefresh ) {
507 $this->refreshCounts(); // update the row
508
509 return true;
510 }
511
512 return false;
513 }
514}
const NS_FILE
Definition Defines.php:70
const NS_CATEGORY
Definition Defines.php:78
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
Category objects are immutable, strictly speaking.
Definition Category.php:33
static newFromID( $id)
Factory function.
Definition Category.php:174
getFileCount()
Definition Category.php:255
static newFromTitle(PageIdentity $page)
Factory function.
Definition Category.php:159
static newFromName( $name)
Factory function.
Definition Category.php:139
PageIdentity $mPage
Category page title.
Definition Category.php:41
refreshCountsIfEmpty()
Call refreshCounts() if there are no entries in the categorylinks table or if the category table has ...
Definition Category.php:456
$mName
Name of the category, normalized to DB-key form.
Definition Category.php:35
initialize( $mode=self::LOAD_ONLY)
Set up all member variables using a database query.
Definition Category.php:68
$mPages
Counts of membership (cat_pages, cat_subcats, cat_files)
Definition Category.php:43
ReadOnlyMode $readOnlyMode
Definition Category.php:54
getMembers( $limit=false, $offset='')
Fetch a TitleArray of up to $limit category members, beginning after the category sort key $offset.
Definition Category.php:292
static newFromRow(stdClass $row, ?PageIdentity $page=null)
Factory function, for constructing a Category object from a result set.
Definition Category.php:190
refreshCounts()
Refresh the counts for this category.
Definition Category.php:337
const LOAD_ONLY
Definition Category.php:45
getX( $key)
Generic accessor.
Definition Category.php:325
refreshCountsIfSmall( $maxSize=self::ROW_COUNT_SMALL)
Call refreshCounts() if there are few entries in the categorylinks table.
Definition Category.php:473
ILoadBalancer $loadBalancer
Definition Category.php:51
getPageCount()
Definition Category.php:241
const LAZY_INIT_ROW
Definition Category.php:46
const ROW_COUNT_SMALL
Definition Category.php:48
__construct()
Definition Category.php:56
getSubcatCount()
Definition Category.php:248
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A service class for fetching the wiki's current read-only mode.
static newFromResult( $res)
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:677
Interface for objects (potentially) representing an editable wiki page.
getDBkey()
Get the page title in DB key form.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27