MediaWiki master
WatchedItemQueryService.php
Go to the documentation of this file.
1<?php
2
7namespace MediaWiki\Watchlist;
8
13use Wikimedia\Assert\Assert;
17
26
27 // FILTER_* constants are part of public API (are used in ApiQueryWatchlistRaw) and
28 // should not be changed.
29 // Changing values of those constants will result in a breaking change in the API
30 public const FILTER_CHANGED = 'changed';
31 public const FILTER_NOT_CHANGED = '!changed';
32
33 public const SORT_ASC = 'ASC';
34 public const SORT_DESC = 'DESC';
35
39 private $dbProvider;
40
42 private $watchedItemStore;
43
47 private $expiryEnabled;
48
52 private $maxQueryExecutionTime;
53
54 public function __construct(
55 IConnectionProvider $dbProvider,
56 WatchedItemStoreInterface $watchedItemStore,
57 bool $expiryEnabled = false,
58 int $maxQueryExecutionTime = 0
59 ) {
60 $this->dbProvider = $dbProvider;
61 $this->watchedItemStore = $watchedItemStore;
62 $this->expiryEnabled = $expiryEnabled;
63 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
64 }
65
85 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
86 if ( !$user->isRegistered() ) {
87 // TODO: should this just return an empty array or rather complain loud at this point
88 // as e.g. ApiBase::getWatchlistUser does?
89 return [];
90 }
91
92 $options += [ 'namespaceIds' => [] ];
93
94 Assert::parameter(
95 !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
96 '$options[\'sort\']',
97 'must be SORT_ASC or SORT_DESC'
98 );
99 Assert::parameter(
100 !isset( $options['filter'] ) || in_array(
101 $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
102 ),
103 '$options[\'filter\']',
104 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
105 );
106 Assert::parameter(
107 ( !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] ) )
108 || isset( $options['sort'] ),
109 '$options[\'sort\']',
110 'must be provided if any of "from", "until", "startFrom" options is provided'
111 );
112
113 $db = $this->dbProvider->getReplicaDatabase();
114
115 $queryBuilder = $db->newSelectQueryBuilder()
116 ->select( [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ] )
117 ->from( 'watchlist' )
118 ->caller( __METHOD__ );
119 $this->addQueryCondsForWatchedItemsForUser( $db, $user, $options, $queryBuilder );
120 $this->addQueryDbOptionsForWatchedItemsForUser( $options, $queryBuilder );
121
122 if ( $this->expiryEnabled ) {
123 // If expiries are enabled, join with the watchlist_expiry table and exclude expired items.
124 $queryBuilder->leftJoin( 'watchlist_expiry', null, 'wl_id = we_item' )
125 ->andWhere( $db->expr( 'we_expiry', '>', $db->timestamp() )->or( 'we_expiry', '=', null ) );
126 }
127 $res = $queryBuilder->fetchResultSet();
128
129 $watchedItems = [];
130 foreach ( $res as $row ) {
131 $target = PageReferenceValue::localReference( (int)$row->wl_namespace, $row->wl_title );
132 // todo these could all be cached at some point?
133 $watchedItems[] = new WatchedItem(
134 $user,
135 $target,
136 $this->watchedItemStore->getLatestNotificationTimestamp(
137 $row->wl_notificationtimestamp, $user, $target
138 ),
139 $row->we_expiry ?? null
140 );
141 }
142
143 return $watchedItems;
144 }
145
146 private function addQueryCondsForWatchedItemsForUser(
147 IReadableDatabase $db, UserIdentity $user, array $options, SelectQueryBuilder $queryBuilder
148 ) {
149 $queryBuilder->where( [ 'wl_user' => $user->getId() ] );
150 if ( $options['namespaceIds'] ) {
151 $queryBuilder->where( [ 'wl_namespace' => array_map( 'intval', $options['namespaceIds'] ) ] );
152 }
153 if ( isset( $options['filter'] ) ) {
154 $filter = $options['filter'];
155 if ( $filter === self::FILTER_CHANGED ) {
156 $queryBuilder->where( 'wl_notificationtimestamp IS NOT NULL' );
157 } else {
158 $queryBuilder->where( 'wl_notificationtimestamp IS NULL' );
159 }
160 }
161
162 if ( isset( $options['from'] ) ) {
163 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
164 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['from'], $op ) );
165 }
166 if ( isset( $options['until'] ) ) {
167 $op = $options['sort'] === self::SORT_ASC ? '<=' : '>=';
168 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['until'], $op ) );
169 }
170 if ( isset( $options['startFrom'] ) ) {
171 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
172 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['startFrom'], $op ) );
173 }
174 }
175
185 private function getFromUntilTargetConds( IReadableDatabase $db, PageReference|LinkTarget $target, $op ) {
186 return $db->buildComparison( $op, [
187 'wl_namespace' => $target->getNamespace(),
188 'wl_title' => $target->getDBkey(),
189 ] );
190 }
191
192 private function addQueryDbOptionsForWatchedItemsForUser( array $options, SelectQueryBuilder $queryBuilder ) {
193 if ( array_key_exists( 'sort', $options ) ) {
194 if ( count( $options['namespaceIds'] ) !== 1 ) {
195 $queryBuilder->orderBy( 'wl_namespace', $options['sort'] );
196 }
197 $queryBuilder->orderBy( 'wl_title', $options['sort'] );
198 }
199 if ( array_key_exists( 'limit', $options ) ) {
200 $queryBuilder->limit( (int)$options['limit'] );
201 }
202 if ( $this->maxQueryExecutionTime ) {
203 $queryBuilder->setMaxExecutionTime( $this->maxQueryExecutionTime );
204 }
205 }
206
207}
209class_alias( WatchedItemQueryService::class, 'WatchedItemQueryService' );
Immutable value object representing a page reference.
Class performing complex database queries related to WatchedItems.
__construct(IConnectionProvider $dbProvider, WatchedItemStoreInterface $watchedItemStore, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
Representation of a pair of user and title for watchlist entries.
Build SELECT queries with a fluent interface.
where( $conds)
Add conditions to the query.
Represents the target of a wiki link.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
Provide primary and replica IDatabase connections.
A database connection without write operations.