PageProps.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <?php
  2. /**
  3. * Access to properties of a page.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use Wikimedia\ScopedCallback;
  23. /**
  24. * Gives access to properties of a page.
  25. *
  26. * @since 1.27
  27. */
  28. class PageProps {
  29. /**
  30. * @var PageProps
  31. */
  32. private static $instance;
  33. /**
  34. * Overrides the default instance of this class
  35. * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
  36. *
  37. * If this method is used it MUST also be called with null after a test to ensure a new
  38. * default instance is created next time getInstance is called.
  39. *
  40. * @since 1.27
  41. *
  42. * @param PageProps|null $store
  43. *
  44. * @return ScopedCallback to reset the overridden value
  45. * @throws MWException
  46. */
  47. public static function overrideInstance( PageProps $store = null ) {
  48. if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
  49. throw new MWException(
  50. 'Cannot override ' . __CLASS__ . 'default instance in operation.'
  51. );
  52. }
  53. $previousValue = self::$instance;
  54. self::$instance = $store;
  55. return new ScopedCallback( function () use ( $previousValue ) {
  56. self::$instance = $previousValue;
  57. } );
  58. }
  59. /**
  60. * @return PageProps
  61. */
  62. public static function getInstance() {
  63. if ( self::$instance === null ) {
  64. self::$instance = new self();
  65. }
  66. return self::$instance;
  67. }
  68. /** Cache parameters */
  69. const CACHE_TTL = 10; // integer; TTL in seconds
  70. const CACHE_SIZE = 100; // integer; max cached pages
  71. /** Property cache */
  72. private $cache = null;
  73. /**
  74. * Create a PageProps object
  75. */
  76. private function __construct() {
  77. $this->cache = new MapCacheLRU( self::CACHE_SIZE );
  78. }
  79. /**
  80. * Ensure that cache has at least this size
  81. * @param int $size
  82. */
  83. public function ensureCacheSize( $size ) {
  84. if ( $this->cache->getMaxSize() < $size ) {
  85. $this->cache->setMaxSize( $size );
  86. }
  87. }
  88. /**
  89. * Given one or more Titles and one or more names of properties,
  90. * returns an associative array mapping page ID to property value.
  91. * Pages in the provided set of Titles that do not have a value for
  92. * the given properties will not appear in the returned array. If a
  93. * single Title is provided, it does not need to be passed in an array,
  94. * but an array will always be returned. If a single property name is
  95. * provided, it does not need to be passed in an array. In that case,
  96. * an associative array mapping page ID to property value will be
  97. * returned; otherwise, an associative array mapping page ID to
  98. * an associative array mapping property name to property value will be
  99. * returned. An empty array will be returned if no matching properties
  100. * were found.
  101. *
  102. * @param Title[]|Title $titles
  103. * @param string[]|string $propertyNames
  104. * @return array associative array mapping page ID to property value
  105. */
  106. public function getProperties( $titles, $propertyNames ) {
  107. if ( is_array( $propertyNames ) ) {
  108. $gotArray = true;
  109. } else {
  110. $propertyNames = [ $propertyNames ];
  111. $gotArray = false;
  112. }
  113. $values = [];
  114. $goodIDs = $this->getGoodIDs( $titles );
  115. $queryIDs = [];
  116. foreach ( $goodIDs as $pageID ) {
  117. foreach ( $propertyNames as $propertyName ) {
  118. $propertyValue = $this->getCachedProperty( $pageID, $propertyName );
  119. if ( $propertyValue === false ) {
  120. $queryIDs[] = $pageID;
  121. break;
  122. } elseif ( $gotArray ) {
  123. $values[$pageID][$propertyName] = $propertyValue;
  124. } else {
  125. $values[$pageID] = $propertyValue;
  126. }
  127. }
  128. }
  129. if ( $queryIDs ) {
  130. $dbr = wfGetDB( DB_REPLICA );
  131. $result = $dbr->select(
  132. 'page_props',
  133. [
  134. 'pp_page',
  135. 'pp_propname',
  136. 'pp_value'
  137. ],
  138. [
  139. 'pp_page' => $queryIDs,
  140. 'pp_propname' => $propertyNames
  141. ],
  142. __METHOD__
  143. );
  144. foreach ( $result as $row ) {
  145. $pageID = $row->pp_page;
  146. $propertyName = $row->pp_propname;
  147. $propertyValue = $row->pp_value;
  148. $this->cacheProperty( $pageID, $propertyName, $propertyValue );
  149. if ( $gotArray ) {
  150. $values[$pageID][$propertyName] = $propertyValue;
  151. } else {
  152. $values[$pageID] = $propertyValue;
  153. }
  154. }
  155. }
  156. return $values;
  157. }
  158. /**
  159. * Get all page property values.
  160. * Given one or more Titles, returns an associative array mapping page
  161. * ID to an associative array mapping property names to property
  162. * values. Pages in the provided set of Titles that do not have any
  163. * properties will not appear in the returned array. If a single Title
  164. * is provided, it does not need to be passed in an array, but an array
  165. * will always be returned. An empty array will be returned if no
  166. * matching properties were found.
  167. *
  168. * @param Title[]|Title $titles
  169. * @return array associative array mapping page ID to property value array
  170. */
  171. public function getAllProperties( $titles ) {
  172. $values = [];
  173. $goodIDs = $this->getGoodIDs( $titles );
  174. $queryIDs = [];
  175. foreach ( $goodIDs as $pageID ) {
  176. $pageProperties = $this->getCachedProperties( $pageID );
  177. if ( $pageProperties === false ) {
  178. $queryIDs[] = $pageID;
  179. } else {
  180. $values[$pageID] = $pageProperties;
  181. }
  182. }
  183. if ( $queryIDs != [] ) {
  184. $dbr = wfGetDB( DB_REPLICA );
  185. $result = $dbr->select(
  186. 'page_props',
  187. [
  188. 'pp_page',
  189. 'pp_propname',
  190. 'pp_value'
  191. ],
  192. [
  193. 'pp_page' => $queryIDs,
  194. ],
  195. __METHOD__
  196. );
  197. $currentPageID = 0;
  198. $pageProperties = [];
  199. foreach ( $result as $row ) {
  200. $pageID = $row->pp_page;
  201. if ( $currentPageID != $pageID ) {
  202. if ( $pageProperties != [] ) {
  203. $this->cacheProperties( $currentPageID, $pageProperties );
  204. $values[$currentPageID] = $pageProperties;
  205. }
  206. $currentPageID = $pageID;
  207. $pageProperties = [];
  208. }
  209. $pageProperties[$row->pp_propname] = $row->pp_value;
  210. }
  211. if ( $pageProperties != [] ) {
  212. $this->cacheProperties( $pageID, $pageProperties );
  213. $values[$pageID] = $pageProperties;
  214. }
  215. }
  216. return $values;
  217. }
  218. /**
  219. * @param Title[]|Title $titles
  220. * @return array array of good page IDs
  221. */
  222. private function getGoodIDs( $titles ) {
  223. $result = [];
  224. if ( is_array( $titles ) ) {
  225. ( new LinkBatch( $titles ) )->execute();
  226. foreach ( $titles as $title ) {
  227. $pageID = $title->getArticleID();
  228. if ( $pageID > 0 ) {
  229. $result[] = $pageID;
  230. }
  231. }
  232. } else {
  233. $pageID = $titles->getArticleID();
  234. if ( $pageID > 0 ) {
  235. $result[] = $pageID;
  236. }
  237. }
  238. return $result;
  239. }
  240. /**
  241. * Get a property from the cache.
  242. *
  243. * @param int $pageID page ID of page being queried
  244. * @param string $propertyName name of property being queried
  245. * @return string|bool property value array or false if not found
  246. */
  247. private function getCachedProperty( $pageID, $propertyName ) {
  248. if ( $this->cache->hasField( $pageID, $propertyName, self::CACHE_TTL ) ) {
  249. return $this->cache->getField( $pageID, $propertyName );
  250. }
  251. if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
  252. $pageProperties = $this->cache->getField( 0, $pageID );
  253. if ( isset( $pageProperties[$propertyName] ) ) {
  254. return $pageProperties[$propertyName];
  255. }
  256. }
  257. return false;
  258. }
  259. /**
  260. * Get properties from the cache.
  261. *
  262. * @param int $pageID page ID of page being queried
  263. * @return string|bool property value array or false if not found
  264. */
  265. private function getCachedProperties( $pageID ) {
  266. if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
  267. return $this->cache->getField( 0, $pageID );
  268. }
  269. return false;
  270. }
  271. /**
  272. * Save a property to the cache.
  273. *
  274. * @param int $pageID page ID of page being cached
  275. * @param string $propertyName name of property being cached
  276. * @param mixed $propertyValue value of property
  277. */
  278. private function cacheProperty( $pageID, $propertyName, $propertyValue ) {
  279. $this->cache->setField( $pageID, $propertyName, $propertyValue );
  280. }
  281. /**
  282. * Save properties to the cache.
  283. *
  284. * @param int $pageID page ID of page being cached
  285. * @param string[] $pageProperties associative array of page properties to be cached
  286. */
  287. private function cacheProperties( $pageID, $pageProperties ) {
  288. $this->cache->clear( $pageID );
  289. $this->cache->setField( 0, $pageID, $pageProperties );
  290. }
  291. }