ChangeHandler::handleChanges()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 4
nc 4
nop 2
1
<?php
2
3
namespace Wikibase\Client\Changes;
4
5
use Hooks;
6
use InvalidArgumentException;
7
use Psr\Log\LoggerInterface;
8
use SiteLookup;
9
use Title;
10
use TitleFactory;
11
use Wikibase\Client\Usage\PageEntityUsages;
12
use Wikibase\Lib\Changes\Change;
13
use Wikibase\Lib\Changes\EntityChange;
14
15
/**
16
 * Interface for change handling. Whenever a change is detected,
17
 * it should be fed to this service which then takes care handling it.
18
 *
19
 * @see @ref md_docs_topics_change-propagation for an overview of the change propagation mechanism.
20
 *
21
 * @license GPL-2.0-or-later
22
 * @author Daniel Kinzler
23
 */
24
class ChangeHandler {
25
26
	/**
27
	 * @var AffectedPagesFinder
28
	 */
29
	private $affectedPagesFinder;
30
31
	/**
32
	 * @var TitleFactory
33
	 */
34
	private $titleFactory;
35
36
	/**
37
	 * @var PageUpdater
38
	 */
39
	private $updater;
40
41
	/**
42
	 * @var ChangeRunCoalescer
43
	 */
44
	private $changeRunCoalescer;
45
46
	/**
47
	 * @var SiteLookup
48
	 */
49
	private $siteLookup;
50
51
	/**
52
	 * @var LoggerInterface
53
	 */
54
	private $logger;
55
56
	/**
57
	 * @var bool
58
	 */
59
	private $injectRecentChanges;
60
61
	/**
62
	 * @param AffectedPagesFinder $affectedPagesFinder
63
	 * @param TitleFactory $titleFactory
64
	 * @param PageUpdater $updater
65
	 * @param ChangeRunCoalescer $changeRunCoalescer
66
	 * @param SiteLookup $siteLookup
67
	 * @param LoggerInterface $logger
68
	 * @param bool $injectRecentChanges
69
	 *
70
	 * @throws InvalidArgumentException
71
	 */
72
	public function __construct(
73
		AffectedPagesFinder $affectedPagesFinder,
74
		TitleFactory $titleFactory,
75
		PageUpdater $updater,
76
		ChangeRunCoalescer $changeRunCoalescer,
77
		SiteLookup $siteLookup,
78
		LoggerInterface $logger,
79
		$injectRecentChanges = true
80
	) {
81
		if ( !is_bool( $injectRecentChanges ) ) {
82
			throw new InvalidArgumentException( '$injectRecentChanges must be a bool' );
83
		}
84
85
		$this->affectedPagesFinder = $affectedPagesFinder;
86
		$this->titleFactory = $titleFactory;
87
		$this->updater = $updater;
88
		$this->changeRunCoalescer = $changeRunCoalescer;
89
		$this->siteLookup = $siteLookup;
90
		$this->logger = $logger;
91
		$this->injectRecentChanges = $injectRecentChanges;
92
	}
93
94
	/**
95
	 * @param EntityChange[] $changes
96
	 * @param array $rootJobParams any relevant root job parameters to be inherited by new jobs.
97
	 */
98
	public function handleChanges( array $changes, array $rootJobParams = [] ) {
99
		$changes = $this->changeRunCoalescer->transformChangeList( $changes );
100
101
		if ( !Hooks::run( 'WikibaseHandleChanges', [ $changes, $rootJobParams ] ) ) {
102
			return;
103
		}
104
105
		foreach ( $changes as $change ) {
106
			if ( !Hooks::run( 'WikibaseHandleChange', [ $change, $rootJobParams ] ) ) {
107
				continue;
108
			}
109
110
			$this->handleChange( $change, $rootJobParams );
111
		}
112
	}
113
114
	/**
115
	 * Main entry point for handling changes
116
	 *
117
	 * @todo process multiple changes at once!
118
	 *
119
	 * @param EntityChange $change
120
	 * @param array $rootJobParams any relevant root job parameters to be inherited by new jobs.
121
	 */
122
	public function handleChange( EntityChange $change, array $rootJobParams = [] ) {
123
		$changeId = $this->getChangeIdForLog( $change );
124
125
		$this->logger->debug(
126
			'{method}: handling change #{changeId} ({changeType})',
127
			[
128
				'method' => __METHOD__,
129
				'changeId' => $changeId,
130
				'changeType' => $change->getType(),
131
			]
132
		);
133
134
		$usagesPerPage = $this->affectedPagesFinder->getAffectedUsagesByPage( $change );
135
136
		$this->logger->debug(
137
			'{method}: updating {pageCount} page(s) for change #{changeId}.',
138
			[
139
				'method' => __METHOD__,
140
				'changeId' => $changeId,
141
				'pageCount' => count( $usagesPerPage ),
142
			]
143
		);
144
145
		// if no usages we can abort early
146
		if ( $usagesPerPage === [] ) {
147
			return;
148
		}
149
150
		// Run all updates on all affected pages
151
		$titlesToUpdate = $this->getTitlesForUsages( $usagesPerPage );
152
153
		// if no titles we can abort early
154
		if ( $titlesToUpdate === [] ) {
155
			return;
156
		}
157
158
		// NOTE: deduplicate
159
		$titleBatchSignature = $this->getTitleBatchSignature( $titlesToUpdate );
160
		$rootJobParams['rootJobSignature'] = $titleBatchSignature;
161
162
		if ( !isset( $rootJobParams['rootJobTimestamp'] ) ) {
163
			$rootJobParams['rootJobTimestamp'] = wfTimestampNow();
164
		}
165
166
		$this->updater->purgeWebCache(
167
			$titlesToUpdate,
168
			$rootJobParams,
169
			$change->getAction(),
170
			$change->hasField( 'user_id' ) ? 'uid:' . $change->getUserId() : 'uid:?'
171
		);
172
173
		// Removing root job timestamp to make it work: T233520
174
		$refreshLinksRootParams = $rootJobParams;
175
		unset( $refreshLinksRootParams['rootJobTimestamp'] );
176
177
		$this->updater->scheduleRefreshLinks(
178
			$titlesToUpdate,
179
			$refreshLinksRootParams,
180
			$change->getAction(),
181
			'uid:' . ( $change->getUserId() ?: '?' )
182
		);
183
184
		// NOTE: signature depends on change ID, effectively disabling deduplication
185
		$changeSignature = $this->getChangeSignature( $change );
186
		$rootJobParams['rootJobSignature'] = $titleBatchSignature . '&' . $changeSignature;
187
		if ( $this->injectRecentChanges ) {
188
			$this->updater->injectRCRecords( $titlesToUpdate, $change, $rootJobParams );
189
		}
190
	}
191
192
	/**
193
	 * @param Title[] $titles
194
	 *
195
	 * @return string a signature based on the hash of the given titles
196
	 */
197
	private function getTitleBatchSignature( array $titles ) {
198
		$pages = [];
199
200
		/** @see WikiPageUpdater::getPageParamForRefreshLinksJob */
201
		foreach ( $titles as $title ) {
202
			$id = $title->getArticleID();
203
			$pages[$id] = [ $title->getNamespace(), $title->getDBkey() ];
204
		}
205
206
		ksort( $pages );
207
		return 'title-batch:' . sha1( json_encode( $pages ) );
208
	}
209
210
	/**
211
	 * @param EntityChange $change
212
	 *
213
	 * @return string a signature representing the change's identity.
214
	 */
215
	private function getChangeSignature( EntityChange $change ) {
216
		if ( $change->getId() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $change->getId() of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
217
			return 'change-id:' . $change->getId();
218
		} else {
219
			// synthetic change!
220
			$changeData = $change->getFields();
221
222
			if ( isset( $changeData['info']['change-ids'] ) ) {
223
				$ids = $changeData['info']['change-ids'];
224
				sort( $ids );
225
				return 'change-batch:' . implode( ',', $ids );
226
			} else {
227
				ksort( $changeData );
228
				return 'change-hash:' . sha1( json_encode( $changeData ) );
229
			}
230
		}
231
	}
232
233
	/**
234
	 * @param PageEntityUsages[] $usagesPerPage
235
	 *
236
	 * @return Title[]
237
	 */
238
	private function getTitlesForUsages( $usagesPerPage ) {
239
		$pageIds = [];
240
241
		foreach ( $usagesPerPage as $usages ) {
242
			$pageIds[] = $usages->getPageId();
243
		}
244
245
		return $this->titleFactory->newFromIDs( $pageIds );
246
	}
247
248
	/**
249
	 * Returns a human readable change ID, containing multiple IDs in case of a
250
	 * coalesced change.
251
	 *
252
	 * @param Change $change
253
	 *
254
	 * @return string
255
	 */
256
	private function getChangeIdForLog( Change $change ) {
257
		if ( $change instanceof EntityChange ) {
258
			$info = $change->getInfo();
259
260
			if ( isset( $info['change-ids'] ) ) {
261
				return implode( '|', $info['change-ids'] );
262
			}
263
		}
264
265
		return $change->getId();
266
	}
267
268
}
269