41 private $mPage =
null;
57 $services = MediaWikiServices::getInstance();
58 $this->loadBalancer = $services->getDBLoadBalancer();
59 $this->readOnlyMode = $services->getReadOnlyMode();
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 ];
81 $row =
$dbr->selectRow(
83 [
'cat_id',
'cat_title',
'cat_pages',
'cat_subcats',
'cat_files' ],
89 # Okay, there were no contents. Nothing to initialize.
91 # If there is a page object but no record in the category table,
92 # treat this as an empty category.
94 $this->mName = $this->mPage->getDBkey();
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' ] );
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;
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 );
124 if ( $mode === self::LAZY_INIT_ROW ) {
125 DeferredUpdates::addCallableUpdate( [ $this,
'refreshCounts' ] );
143 if ( !is_object(
$title ) ) {
148 $cat->mName =
$title->getDBkey();
176 $cat->mID = intval( $id );
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.
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...
205 # if we have a PageIdentity object, fetch the category name from there
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;
228 return $this->getX(
'mName' );
235 return $this->getX(
'mID' );
242 return $this->getX(
'mPages' );
249 return $this->getX(
'mSubcats' );
256 return $this->getX(
'mFiles' );
265 if ( $this->mPage ) {
269 if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
282 return Title::castFromPageIdentity( $this->getPage() ) ??
false;
295 $conds = [
'cl_to' => $this->getName(),
'cl_from = page_id' ];
296 $options = [
'ORDER BY' =>
'cl_sortkey' ];
299 $options[
'LIMIT'] = $limit;
302 if ( $offset !==
'' ) {
303 $conds[] =
'cl_sortkey > ' .
$dbr->addQuotes( $offset );
308 [
'page',
'categorylinks' ],
309 [
'page_id',
'page_namespace',
'page_title',
'page_len',
310 'page_is_redirect',
'page_latest' ],
325 private function getX( $key ) {
326 if ( $this->{$key} ===
null && !$this->initialize( self::LAZY_INIT_ROW ) ) {
329 return $this->{$key};
338 if ( $this->readOnlyMode->isReadOnly() ) {
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 ) ) {
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 ) {
357 $dbw->startAtomic( __METHOD__ );
361 $dbw->lockForUpdate(
'category', [
'cat_title' => $this->mName ], __METHOD__ );
365 $dbw->selectRowCount(
366 [
'categorylinks',
'page' ],
368 [
'cl_to' => $this->mName,
'page_id = cl_from' ],
370 [
'LOCK IN SHARE MODE' ]
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' ],
378 'pages' =>
'COUNT(*)',
379 'subcats' =>
"COUNT($catCond)",
380 'files' =>
"COUNT($fileCond)"
382 [
'cl_to' => $this->mName,
'page_id = cl_from' ],
386 $shouldExist = $result->pages > 0 || $this->getPage()->exists();
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".
396 'cat_pages' => $result->pages,
397 'cat_subcats' => $result->subcats,
398 'cat_files' => $result->files
400 [
'cat_title' => $this->mName ],
404 # The category is empty and has no description page, delete it
407 [
'cat_title' => $this->mName ],
412 } elseif ( $shouldExist ) {
413 # The category row doesn't exist but should, so create it. Use
414 # upsert in case of races.
418 'cat_title' => $this->mName,
419 'cat_pages' => $result->pages,
420 'cat_subcats' => $result->subcats,
421 'cat_files' => $result->files
425 'cat_pages' => $result->pages,
426 'cat_subcats' => $result->subcats,
427 'cat_files' => $result->files
435 $dbw->endAtomic( __METHOD__ );
437 # Now we should update our local counts.
438 $this->mPages = $result->pages;
439 $this->mSubcats = $result->subcats;
440 $this->mFiles = $result->files;
457 return $this->refreshCountsIfSmall( 0 );
474 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
475 $dbw->startAtomic( __METHOD__ );
477 $typeOccurances = $dbw->selectFieldValues(
480 [
'cl_to' => $this->getName() ],
482 [
'LIMIT' => $maxSize + 1 ]
485 if ( !$typeOccurances ) {
487 } elseif ( count( $typeOccurances ) <= $maxSize ) {
488 $countByType = array_count_values( $typeOccurances );
489 $doRefresh = !$dbw->selectField(
493 'cat_title' => $this->getName(),
494 'cat_pages' => $countByType[
'page'] ?? 0,
495 'cat_subcats' => $countByType[
'subcat'] ?? 0,
496 'cat_files' => $countByType[
'file'] ?? 0
504 $dbw->endAtomic( __METHOD__ );
507 $this->refreshCounts();
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Category objects are immutable, strictly speaking.
static newFromID( $id)
Factory function.
static newFromTitle(PageIdentity $page)
Factory function.
static newFromName( $name)
Factory function.
PageIdentity $mPage
Category page title.
refreshCountsIfEmpty()
Call refreshCounts() if there are no entries in the categorylinks table or if the category table has ...
$mName
Name of the category, normalized to DB-key form.
initialize( $mode=self::LOAD_ONLY)
Set up all member variables using a database query.
$mPages
Counts of membership (cat_pages, cat_subcats, cat_files)
ReadOnlyMode $readOnlyMode
getMembers( $limit=false, $offset='')
Fetch a TitleArray of up to $limit category members, beginning after the category sort key $offset.
static newFromRow(stdClass $row, ?PageIdentity $page=null)
Factory function, for constructing a Category object from a result set.
refreshCounts()
Refresh the counts for this category.
getX( $key)
Generic accessor.
refreshCountsIfSmall( $maxSize=self::ROW_COUNT_SMALL)
Call refreshCounts() if there are few entries in the categorylinks table.
ILoadBalancer $loadBalancer
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.
Interface for objects (potentially) representing an editable wiki page.