PageProps.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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 ProcessCacheLRU( 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->getSize() < $size ) {
  85. $this->cache->resize( $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. } else {
  123. if ( $gotArray ) {
  124. $values[$pageID][$propertyName] = $propertyValue;
  125. } else {
  126. $values[$pageID] = $propertyValue;
  127. }
  128. }
  129. }
  130. }
  131. if ( $queryIDs ) {
  132. $dbr = wfGetDB( DB_REPLICA );
  133. $result = $dbr->select(
  134. 'page_props',
  135. [
  136. 'pp_page',
  137. 'pp_propname',
  138. 'pp_value'
  139. ],
  140. [
  141. 'pp_page' => $queryIDs,
  142. 'pp_propname' => $propertyNames
  143. ],
  144. __METHOD__
  145. );
  146. foreach ( $result as $row ) {
  147. $pageID = $row->pp_page;
  148. $propertyName = $row->pp_propname;
  149. $propertyValue = $row->pp_value;
  150. $this->cacheProperty( $pageID, $propertyName, $propertyValue );
  151. if ( $gotArray ) {
  152. $values[$pageID][$propertyName] = $propertyValue;
  153. } else {
  154. $values[$pageID] = $propertyValue;
  155. }
  156. }
  157. }
  158. return $values;
  159. }
  160. /**
  161. * Get all page property values.
  162. * Given one or more Titles, returns an associative array mapping page
  163. * ID to an associative array mapping property names to property
  164. * values. Pages in the provided set of Titles that do not have any
  165. * properties will not appear in the returned array. If a single Title
  166. * is provided, it does not need to be passed in an array, but an array
  167. * will always be returned. An empty array will be returned if no
  168. * matching properties were found.
  169. *
  170. * @param Title[]|Title $titles
  171. * @return array associative array mapping page ID to property value array
  172. */
  173. public function getAllProperties( $titles ) {
  174. $values = [];
  175. $goodIDs = $this->getGoodIDs( $titles );
  176. $queryIDs = [];
  177. foreach ( $goodIDs as $pageID ) {
  178. $pageProperties = $this->getCachedProperties( $pageID );
  179. if ( $pageProperties === false ) {
  180. $queryIDs[] = $pageID;
  181. } else {
  182. $values[$pageID] = $pageProperties;
  183. }
  184. }
  185. if ( $queryIDs != [] ) {
  186. $dbr = wfGetDB( DB_REPLICA );
  187. $result = $dbr->select(
  188. 'page_props',
  189. [
  190. 'pp_page',
  191. 'pp_propname',
  192. 'pp_value'
  193. ],
  194. [
  195. 'pp_page' => $queryIDs,
  196. ],
  197. __METHOD__
  198. );
  199. $currentPageID = 0;
  200. $pageProperties = [];
  201. foreach ( $result as $row ) {
  202. $pageID = $row->pp_page;
  203. if ( $currentPageID != $pageID ) {
  204. if ( $pageProperties != [] ) {
  205. $this->cacheProperties( $currentPageID, $pageProperties );
  206. $values[$currentPageID] = $pageProperties;
  207. }
  208. $currentPageID = $pageID;
  209. $pageProperties = [];
  210. }
  211. $pageProperties[$row->pp_propname] = $row->pp_value;
  212. }
  213. if ( $pageProperties != [] ) {
  214. $this->cacheProperties( $pageID, $pageProperties );
  215. $values[$pageID] = $pageProperties;
  216. }
  217. }
  218. return $values;
  219. }
  220. /**
  221. * @param Title[]|Title $titles
  222. * @return array array of good page IDs
  223. */
  224. private function getGoodIDs( $titles ) {
  225. $result = [];
  226. if ( is_array( $titles ) ) {
  227. ( new LinkBatch( $titles ) )->execute();
  228. foreach ( $titles as $title ) {
  229. $pageID = $title->getArticleID();
  230. if ( $pageID > 0 ) {
  231. $result[] = $pageID;
  232. }
  233. }
  234. } else {
  235. $pageID = $titles->getArticleID();
  236. if ( $pageID > 0 ) {
  237. $result[] = $pageID;
  238. }
  239. }
  240. return $result;
  241. }
  242. /**
  243. * Get a property from the cache.
  244. *
  245. * @param int $pageID page ID of page being queried
  246. * @param string $propertyName name of property being queried
  247. * @return string|bool property value array or false if not found
  248. */
  249. private function getCachedProperty( $pageID, $propertyName ) {
  250. if ( $this->cache->has( $pageID, $propertyName, self::CACHE_TTL ) ) {
  251. return $this->cache->get( $pageID, $propertyName );
  252. }
  253. if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
  254. $pageProperties = $this->cache->get( 0, $pageID );
  255. if ( isset( $pageProperties[$propertyName] ) ) {
  256. return $pageProperties[$propertyName];
  257. }
  258. }
  259. return false;
  260. }
  261. /**
  262. * Get properties from the cache.
  263. *
  264. * @param int $pageID page ID of page being queried
  265. * @return string|bool property value array or false if not found
  266. */
  267. private function getCachedProperties( $pageID ) {
  268. if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
  269. return $this->cache->get( 0, $pageID );
  270. }
  271. return false;
  272. }
  273. /**
  274. * Save a property to the cache.
  275. *
  276. * @param int $pageID page ID of page being cached
  277. * @param string $propertyName name of property being cached
  278. * @param mixed $propertyValue value of property
  279. */
  280. private function cacheProperty( $pageID, $propertyName, $propertyValue ) {
  281. $this->cache->set( $pageID, $propertyName, $propertyValue );
  282. }
  283. /**
  284. * Save properties to the cache.
  285. *
  286. * @param int $pageID page ID of page being cached
  287. * @param string[] $pageProperties associative array of page properties to be cached
  288. */
  289. private function cacheProperties( $pageID, $pageProperties ) {
  290. $this->cache->clear( $pageID );
  291. $this->cache->set( 0, $pageID, $pageProperties );
  292. }
  293. }