MediaWiki REL1_39
LinkTargetStore.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Linker;
22
23use BagOStuff;
24use InvalidArgumentException;
25use RuntimeException;
26use stdClass;
27use TitleValue;
31
38
40 private $loadBalancer;
41
43 private $localCache;
44
46 private $wanObjectCache;
47
49 private $byIdCache;
50
52 private $byTitleCache;
53
59 public function __construct(
60 ILoadBalancer $loadBalancer,
61 BagOStuff $localCache,
62 WANObjectCache $WanObjectCache
63 ) {
64 $this->loadBalancer = $loadBalancer;
65 $this->localCache = $localCache;
66 $this->wanObjectCache = $WanObjectCache;
67 $this->byIdCache = [];
68 $this->byTitleCache = [];
69 }
70
74 public function newLinkTargetFromRow( stdClass $row ): LinkTarget {
75 $ltId = (int)$row->lt_id;
76 if ( $ltId === 0 ) {
77 throw new InvalidArgumentException(
78 "LinkTarget ID is 0 for {$row->lt_title} (ns: {$row->lt_namespace})"
79 );
80 }
81
82 $titlevalue = new TitleValue( (int)$row->lt_namespace, $row->lt_title );
83 $this->addToClassCache( $ltId, $titlevalue );
84 return $titlevalue;
85 }
86
93 public function getLinkTargetById( int $linkTargetId ): ?LinkTarget {
94 if ( !$linkTargetId ) {
95 return null;
96 }
97
98 if ( isset( $this->byIdCache[$linkTargetId] ) ) {
99 return $this->byIdCache[$linkTargetId];
100 }
101
102 $value = $this->loadBalancer->getConnectionRef( DB_REPLICA )->newSelectQueryBuilder()
103 ->caller( __METHOD__ )
104 ->table( 'linktarget' )
105 ->conds( [ 'lt_id' => $linkTargetId ] )
106 ->fields( [ 'lt_id', 'lt_namespace', 'lt_title' ] )
107 ->fetchRow();
108 if ( !$value ) {
109 return null;
110 }
111
112 // TODO: Use local and wan cache when writing read new code.
113 $linkTarget = $this->newLinkTargetFromRow( $value );
114 $this->addToClassCache( $linkTargetId, $linkTarget );
115 return $linkTarget;
116 }
117
124 public function getLinkTargetId( LinkTarget $linkTarget ): ?int {
125 // allow cache to be used, because if it is in the cache, it already has a linktarget id
126 $existingLinktargetId = $this->getLinkTargetIdFromCache( $linkTarget );
127 if ( $existingLinktargetId ) {
128 return $existingLinktargetId;
129 }
130 return null;
131 }
132
146 public function acquireLinkTargetId( LinkTarget $linkTarget, IDatabase $dbw ): int {
147 // allow cache to be used, because if it is in the cache, it already has a linktarget id
148 $existingLinktargetId = $this->getLinkTargetIdFromCache( $linkTarget );
149 if ( $existingLinktargetId ) {
150 return $existingLinktargetId;
151 }
152
153 // Checking primary when it doesn't exist in replica is not that useful but given
154 // the fact that failed inserts waste an auto_increment id better to avoid that.
155 $linkTargetId = $this->fetchIdFromDbPrimary( $linkTarget, [] );
156 if ( $linkTargetId ) {
157 $this->addToClassCache( $linkTargetId, $linkTarget );
158 return $linkTargetId;
159 }
160
161 $ns = $linkTarget->getNamespace();
162 $title = $linkTarget->getDBkey();
163
164 $dbw->insert(
165 'linktarget',
166 [
167 'lt_namespace' => $ns,
168 'lt_title' => $title,
169 ],
170 __METHOD__,
171 [ 'IGNORE' ]
172 );
173
174 if ( $dbw->affectedRows() ) {
175 $linkTargetId = $dbw->insertId();
176 } else {
177 // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
178 $linkTargetId = $this->fetchIdFromDbPrimary(
179 $linkTarget,
180 [ 'LOCK IN SHARE MODE' ]
181 );
182 if ( !$linkTargetId ) {
183 throw new RuntimeException(
184 "Failed to create link target ID for " .
185 "lt_namespace={$ns} lt_title=\"{$title}\""
186 );
187 }
188 }
189 $this->addToClassCache( $linkTargetId, $linkTarget );
190
191 return $linkTargetId;
192 }
193
201 private function fetchIdFromDbPrimary(
202 LinkTarget $linkTarget,
203 array $queryOptions = []
204 ): ?int {
205 $row = $this->loadBalancer->getConnectionRef( DB_PRIMARY )->selectRow(
206 'linktarget',
207 [ 'lt_id', 'lt_namespace', 'lt_title' ],
208 [
209 'lt_namespace' => $linkTarget->getNamespace(),
210 'lt_title' => $linkTarget->getDBkey()
211 ],
212 __METHOD__,
213 $queryOptions
214 );
215
216 if ( !$row || !$row->lt_id ) {
217 return null;
218 }
219 $this->addToClassCache( (int)$row->lt_id, $linkTarget );
220
221 return (int)$row->lt_id;
222 }
223
224 private function addToClassCache( int $id, LinkTarget $linkTarget ) {
225 $this->byIdCache[$id] = $linkTarget;
226 $this->byTitleCache[(string)$linkTarget] = $id;
227 }
228
229 /*
230 * @internal use by tests only
231 */
232 public function clearClassCache() {
233 $this->byIdCache = [];
234 $this->byTitleCache = [];
235 }
236
237 private function getLinkTargetIdFromCache( LinkTarget $linkTarget ) {
238 $linkTargetString = (string)$linkTarget;
239 if ( isset( $this->byTitleCache[$linkTargetString] ) ) {
240 return $this->byTitleCache[$linkTargetString];
241 }
242 $fname = __METHOD__;
243 $res = $this->localCache->getWithSetCallback(
244 $this->localCache->makeKey(
245 'linktargetstore-id',
246 $linkTargetString
247 ),
248 $this->localCache::TTL_HOUR,
249 function () use ( $linkTarget, $fname ) {
250 return $this->wanObjectCache->getWithSetCallback(
251 $this->wanObjectCache->makeKey(
252 'linktargetstore-id',
253 (string)$linkTarget
254 ),
255 WANObjectCache::TTL_DAY,
256 function () use ( $linkTarget, $fname ) {
257 $row = $this->loadBalancer->getConnectionRef( DB_REPLICA )->selectRow(
258 'linktarget',
259 [ 'lt_id', 'lt_namespace', 'lt_title' ],
260 [
261 'lt_namespace' => $linkTarget->getNamespace(),
262 'lt_title' => $linkTarget->getDBkey()
263 ],
264 $fname
265 );
266
267 if ( !$row || !$row->lt_id ) {
268 // Don't store in cache
269 return false;
270 }
271
272 return (int)$row->lt_id;
273 }
274 );
275 }
276 );
277
278 if ( $res ) {
279 $this->addToClassCache( $res, $linkTarget );
280 }
281
282 return $res;
283 }
284
285}
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
Service for retrieving and storing link targets.
getLinkTargetId(LinkTarget $linkTarget)
Return link target id if exists.
acquireLinkTargetId(LinkTarget $linkTarget, IDatabase $dbw)
Attempt to assign a link target ID to the given $linkTarget.
newLinkTargetFromRow(stdClass $row)
Instantiate a new LinkTarget object based on a $row from the linktarget table.Use this method when a ...
getLinkTargetById(int $linkTargetId)
Find a link target by $id.
Represents a page (or page fragment) title within MediaWiki.
Multi-datacenter aware caching interface.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
affectedRows()
Get the number of rows affected by the last write query.
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert row(s) into a table, in the provided order.
insertId()
Get the inserted value of an auto-increment row.
Create and track the database connections and transactions for a given database cluster.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28