Category.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <?php
  2. /**
  3. * Category objects are immutable, strictly speaking. If you call methods that change the database,
  4. * like to refresh link counts, the objects will be appropriately reinitialized.
  5. * Member variables are lazy-initialized.
  6. *
  7. * TODO: Move some stuff from CategoryPage.php to here, and use that.
  8. *
  9. * @author Simetrical
  10. */
  11. class Category {
  12. /** Name of the category, normalized to DB-key form */
  13. private $mName = null;
  14. private $mID = null;
  15. /** Category page title */
  16. private $mTitle = null;
  17. /** Counts of membership (cat_pages, cat_subcats, cat_files) */
  18. private $mPages = null, $mSubcats = null, $mFiles = null;
  19. private function __construct() {}
  20. /**
  21. * Set up all member variables using a database query.
  22. * @return bool True on success, false on failure.
  23. */
  24. protected function initialize() {
  25. if ( $this->mName === null && $this->mTitle )
  26. $this->mName = $title->getDBKey();
  27. if( $this->mName === null && $this->mID === null ) {
  28. throw new MWException( __METHOD__.' has both names and IDs null' );
  29. } elseif( $this->mID === null ) {
  30. $where = array( 'cat_title' => $this->mName );
  31. } elseif( $this->mName === null ) {
  32. $where = array( 'cat_id' => $this->mID );
  33. } else {
  34. # Already initialized
  35. return true;
  36. }
  37. $dbr = wfGetDB( DB_SLAVE );
  38. $row = $dbr->selectRow(
  39. 'category',
  40. array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ),
  41. $where,
  42. __METHOD__
  43. );
  44. if( !$row ) {
  45. # Okay, there were no contents. Nothing to initialize.
  46. if ( $this->mTitle ) {
  47. # If there is a title object but no record in the category table, treat this as an empty category
  48. $this->mID = false;
  49. $this->mName = $this->mTitle->getDBKey();
  50. $this->mPages = 0;
  51. $this->mSubcats = 0;
  52. $this->mFiles = 0;
  53. return true;
  54. } else {
  55. return false; # Fail
  56. }
  57. }
  58. $this->mID = $row->cat_id;
  59. $this->mName = $row->cat_title;
  60. $this->mPages = $row->cat_pages;
  61. $this->mSubcats = $row->cat_subcats;
  62. $this->mFiles = $row->cat_files;
  63. # (bug 13683) If the count is negative, then 1) it's obviously wrong
  64. # and should not be kept, and 2) we *probably* don't have to scan many
  65. # rows to obtain the correct figure, so let's risk a one-time recount.
  66. if( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
  67. $this->refreshCounts();
  68. }
  69. return true;
  70. }
  71. /**
  72. * Factory function.
  73. *
  74. * @param $name Array: A category name (no "Category:" prefix). It need
  75. * not be normalized, with spaces replaced by underscores.
  76. * @return mixed Category, or false on a totally invalid name
  77. */
  78. public static function newFromName( $name ) {
  79. $cat = new self();
  80. $title = Title::makeTitleSafe( NS_CATEGORY, $name );
  81. if( !is_object( $title ) ) {
  82. return false;
  83. }
  84. $cat->mTitle = $title;
  85. $cat->mName = $title->getDBKey();
  86. return $cat;
  87. }
  88. /**
  89. * Factory function.
  90. *
  91. * @param $title Title for the category page
  92. * @return Mixed: category, or false on a totally invalid name
  93. */
  94. public static function newFromTitle( $title ) {
  95. $cat = new self();
  96. $cat->mTitle = $title;
  97. $cat->mName = $title->getDBKey();
  98. return $cat;
  99. }
  100. /**
  101. * Factory function.
  102. *
  103. * @param $id Integer: a category id
  104. * @return Category
  105. */
  106. public static function newFromID( $id ) {
  107. $cat = new self();
  108. $cat->mID = intval( $id );
  109. return $cat;
  110. }
  111. /**
  112. * Factory function, for constructing a Category object from a result set
  113. *
  114. * @param $row result set row, must contain the cat_xxx fields. If the fields are null,
  115. * the resulting Category object will represent an empty category if a title object
  116. * was given. If the fields are null and no title was given, this method fails and returns false.
  117. * @param $title optional title object for the category represented by the given row.
  118. * May be provided if it is already known, to avoid having to re-create a title object later.
  119. * @return Category
  120. */
  121. public static function newFromRow( $row, $title = null ) {
  122. $cat = new self();
  123. $cat->mTitle = $title;
  124. # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
  125. # all the cat_xxx fields being null, if the category page exists, but nothing
  126. # was ever added to the category. This case should be treated linke an empty
  127. # category, if possible.
  128. if ( $row->cat_title === null ) {
  129. if ( $title === null ) {
  130. # the name is probably somewhere in the row, for example as page_title,
  131. # but we can't know that here...
  132. return false;
  133. } else {
  134. $cat->mName = $title->getDBKey(); # if we have a title object, fetch the category name from there
  135. }
  136. $cat->mID = false;
  137. $cat->mSubcats = 0;
  138. $cat->mPages = 0;
  139. $cat->mFiles = 0;
  140. } else {
  141. $cat->mName = $row->cat_title;
  142. $cat->mID = $row->cat_id;
  143. $cat->mSubcats = $row->cat_subcats;
  144. $cat->mPages = $row->cat_pages;
  145. $cat->mFiles = $row->cat_files;
  146. }
  147. return $cat;
  148. }
  149. /** @return mixed DB key name, or false on failure */
  150. public function getName() { return $this->getX( 'mName' ); }
  151. /** @return mixed Category ID, or false on failure */
  152. public function getID() { return $this->getX( 'mID' ); }
  153. /** @return mixed Total number of member pages, or false on failure */
  154. public function getPageCount() { return $this->getX( 'mPages' ); }
  155. /** @return mixed Number of subcategories, or false on failure */
  156. public function getSubcatCount() { return $this->getX( 'mSubcats' ); }
  157. /** @return mixed Number of member files, or false on failure */
  158. public function getFileCount() { return $this->getX( 'mFiles' ); }
  159. /**
  160. * @return mixed The Title for this category, or false on failure.
  161. */
  162. public function getTitle() {
  163. if( $this->mTitle ) return $this->mTitle;
  164. if( !$this->initialize() ) {
  165. return false;
  166. }
  167. $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
  168. return $this->mTitle;
  169. }
  170. /**
  171. * Fetch a TitleArray of up to $limit category members, beginning after the
  172. * category sort key $offset.
  173. * @param $limit integer
  174. * @param $offset string
  175. * @return TitleArray object for category members.
  176. */
  177. public function getMembers( $limit = false, $offset = '' ) {
  178. $dbr = wfGetDB( DB_SLAVE );
  179. $conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' );
  180. $options = array( 'ORDER BY' => 'cl_sortkey' );
  181. if( $limit ) $options[ 'LIMIT' ] = $limit;
  182. if( $offset !== '' ) $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
  183. return TitleArray::newFromResult(
  184. $dbr->select(
  185. array( 'page', 'categorylinks' ),
  186. array( 'page_id', 'page_namespace','page_title', 'page_len',
  187. 'page_is_redirect', 'page_latest' ),
  188. $conds,
  189. __METHOD__,
  190. $options
  191. )
  192. );
  193. }
  194. /** Generic accessor */
  195. private function getX( $key ) {
  196. if( !$this->initialize() ) {
  197. return false;
  198. }
  199. return $this->{$key};
  200. }
  201. /**
  202. * Refresh the counts for this category.
  203. *
  204. * @return bool True on success, false on failure
  205. */
  206. public function refreshCounts() {
  207. if( wfReadOnly() ) {
  208. return false;
  209. }
  210. $dbw = wfGetDB( DB_MASTER );
  211. $dbw->begin();
  212. # Note, we must use names for this, since categorylinks does.
  213. if( $this->mName === null ) {
  214. if( !$this->initialize() ) {
  215. return false;
  216. }
  217. } else {
  218. # Let's be sure that the row exists in the table. We don't need to
  219. # do this if we got the row from the table in initialization!
  220. $dbw->insert(
  221. 'category',
  222. array( 'cat_title' => $this->mName ),
  223. __METHOD__,
  224. 'IGNORE'
  225. );
  226. }
  227. $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' );
  228. $cond2 = $dbw->conditional( 'page_namespace='.NS_FILE, 1, 'NULL' );
  229. $result = $dbw->selectRow(
  230. array( 'categorylinks', 'page' ),
  231. array( 'COUNT(*) AS pages',
  232. "COUNT($cond1) AS subcats",
  233. "COUNT($cond2) AS files"
  234. ),
  235. array( 'cl_to' => $this->mName, 'page_id = cl_from' ),
  236. __METHOD__,
  237. 'LOCK IN SHARE MODE'
  238. );
  239. $ret = $dbw->update(
  240. 'category',
  241. array(
  242. 'cat_pages' => $result->pages,
  243. 'cat_subcats' => $result->subcats,
  244. 'cat_files' => $result->files
  245. ),
  246. array( 'cat_title' => $this->mName ),
  247. __METHOD__
  248. );
  249. $dbw->commit();
  250. # Now we should update our local counts.
  251. $this->mPages = $result->pages;
  252. $this->mSubcats = $result->subcats;
  253. $this->mFiles = $result->files;
  254. return $ret;
  255. }
  256. }