|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file contains only the EditSummary class. |
|
4
|
|
|
*/ |
|
5
|
|
|
|
|
6
|
|
|
namespace Xtools; |
|
7
|
|
|
|
|
8
|
|
|
use Symfony\Component\DependencyInjection\Container; |
|
9
|
|
|
use DateTime; |
|
10
|
|
|
use DateInterval; |
|
11
|
|
|
use Psr\Cache\CacheItemPoolInterface; |
|
12
|
|
|
|
|
13
|
|
|
/** |
|
14
|
|
|
* An EditSummary provides statistics about a user's edit summary |
|
15
|
|
|
* usage over time. |
|
16
|
|
|
*/ |
|
17
|
|
|
class EditSummary extends Model |
|
18
|
|
|
{ |
|
19
|
|
|
/** @var Container The application's DI container. */ |
|
20
|
|
|
protected $container; |
|
21
|
|
|
|
|
22
|
|
|
/** @var Project The project. */ |
|
23
|
|
|
protected $project; |
|
24
|
|
|
|
|
25
|
|
|
/** @var User The user. */ |
|
26
|
|
|
protected $user; |
|
27
|
|
|
|
|
28
|
|
|
/** @var string|int The namespace to target. */ |
|
29
|
|
|
protected $namespace; |
|
30
|
|
|
|
|
31
|
|
|
/** @var Connection $conn Connection to the replica database. */ |
|
32
|
|
|
protected $conn; |
|
33
|
|
|
|
|
34
|
|
|
/** @var int Number of edits from present to consider as 'recent'. */ |
|
35
|
|
|
protected $numEditsRecent; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* Counts of summaries, raw edits, and per-month breakdown. |
|
39
|
|
|
* Keys are underscored because this also is served in the API. |
|
40
|
|
|
* @var array |
|
41
|
|
|
*/ |
|
42
|
|
|
protected $data = [ |
|
43
|
|
|
'recent_edits_minor' => 0, |
|
44
|
|
|
'recent_edits_major' => 0, |
|
45
|
|
|
'total_edits_minor' => 0, |
|
46
|
|
|
'total_edits_major' => 0, |
|
47
|
|
|
'total_edits' => 0, |
|
48
|
|
|
'recent_summaries_minor' => 0, |
|
49
|
|
|
'recent_summaries_major' => 0, |
|
50
|
|
|
'total_summaries_minor' => 0, |
|
51
|
|
|
'total_summaries_major' => 0, |
|
52
|
|
|
'total_summaries' => 0, |
|
53
|
|
|
'month_counts' => [], |
|
54
|
|
|
]; |
|
55
|
|
|
|
|
56
|
|
|
/** |
|
57
|
|
|
* EditSummary constructor. |
|
58
|
|
|
* |
|
59
|
|
|
* @param Project $project The project we're working with. |
|
60
|
|
|
* @param User $user The user to process. |
|
61
|
|
|
* @param string $namespace Namespace ID or 'all' for all namespaces. |
|
62
|
|
|
* @param int $numEditsRecent Number of edits from present to consider as 'recent'. |
|
63
|
|
|
* @param Container $container The DI container. |
|
64
|
|
|
*/ |
|
65
|
2 |
|
public function __construct( |
|
66
|
|
|
Project $project, |
|
67
|
|
|
User $user, |
|
68
|
|
|
$namespace, |
|
69
|
|
|
$numEditsRecent, |
|
70
|
|
|
Container $container |
|
71
|
|
|
) { |
|
72
|
2 |
|
$this->project = $project; |
|
73
|
2 |
|
$this->user = $user; |
|
74
|
2 |
|
$this->namespace = $namespace; |
|
75
|
2 |
|
$this->numEditsRecent = $numEditsRecent; |
|
76
|
2 |
|
$this->container = $container; |
|
77
|
2 |
|
$this->conn = $this->container |
|
78
|
2 |
|
->get('doctrine') |
|
79
|
2 |
|
->getManager('replicas') |
|
80
|
2 |
|
->getConnection(); |
|
81
|
2 |
|
} |
|
82
|
|
|
|
|
83
|
|
|
/** |
|
84
|
|
|
* Get the total number of edits. |
|
85
|
|
|
* @return int |
|
86
|
|
|
*/ |
|
87
|
1 |
|
public function getTotalEdits() |
|
88
|
|
|
{ |
|
89
|
1 |
|
return $this->data['total_edits']; |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* Get the total number of minor edits. |
|
94
|
|
|
* @return int |
|
95
|
|
|
*/ |
|
96
|
1 |
|
public function getTotalEditsMinor() |
|
97
|
|
|
{ |
|
98
|
1 |
|
return $this->data['total_edits_minor']; |
|
99
|
|
|
} |
|
100
|
|
|
|
|
101
|
|
|
/** |
|
102
|
|
|
* Get the total number of major (non-minor) edits. |
|
103
|
|
|
* @return int |
|
104
|
|
|
*/ |
|
105
|
1 |
|
public function getTotalEditsMajor() |
|
106
|
|
|
{ |
|
107
|
1 |
|
return $this->data['total_edits_major']; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
/** |
|
111
|
|
|
* Get the total number of recent minor edits. |
|
112
|
|
|
* @return int |
|
113
|
|
|
*/ |
|
114
|
1 |
|
public function getRecentEditsMinor() |
|
115
|
|
|
{ |
|
116
|
1 |
|
return $this->data['recent_edits_minor']; |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
/** |
|
120
|
|
|
* Get the total number of recent major (non-minor) edits. |
|
121
|
|
|
* @return int |
|
122
|
|
|
*/ |
|
123
|
1 |
|
public function getRecentEditsMajor() |
|
124
|
|
|
{ |
|
125
|
1 |
|
return $this->data['recent_edits_major']; |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* Get the total number of edits with summaries. |
|
130
|
|
|
* @return int |
|
131
|
|
|
*/ |
|
132
|
1 |
|
public function getTotalSummaries() |
|
133
|
|
|
{ |
|
134
|
1 |
|
return $this->data['total_summaries']; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* Get the total number of minor edits with summaries. |
|
139
|
|
|
* @return int |
|
140
|
|
|
*/ |
|
141
|
1 |
|
public function getTotalSummariesMinor() |
|
142
|
|
|
{ |
|
143
|
1 |
|
return $this->data['total_summaries_minor']; |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
/** |
|
147
|
|
|
* Get the total number of major (non-minor) edits with summaries. |
|
148
|
|
|
* @return int |
|
149
|
|
|
*/ |
|
150
|
1 |
|
public function getTotalSummariesMajor() |
|
151
|
|
|
{ |
|
152
|
1 |
|
return $this->data['total_summaries_major']; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
/** |
|
156
|
|
|
* Get the total number of recent minor edits with with summaries. |
|
157
|
|
|
* @return int |
|
158
|
|
|
*/ |
|
159
|
1 |
|
public function getRecentSummariesMinor() |
|
160
|
|
|
{ |
|
161
|
1 |
|
return $this->data['recent_summaries_minor']; |
|
162
|
|
|
} |
|
163
|
|
|
|
|
164
|
|
|
/** |
|
165
|
|
|
* Get the total number of recent major (non-minor) edits with with summaries. |
|
166
|
|
|
* @return int |
|
167
|
|
|
*/ |
|
168
|
1 |
|
public function getRecentSummariesMajor() |
|
169
|
|
|
{ |
|
170
|
1 |
|
return $this->data['recent_summaries_major']; |
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
|
|
/** |
|
174
|
|
|
* Get the month counts. |
|
175
|
|
|
* @return array Months as 'YYYY-MM' as the keys, |
|
176
|
|
|
* with key 'total' and 'summaries' as the values. |
|
177
|
|
|
*/ |
|
178
|
1 |
|
public function getMonthCounts() |
|
179
|
|
|
{ |
|
180
|
1 |
|
return $this->data['month_counts']; |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
/** |
|
184
|
|
|
* Get the whole blob of counts. |
|
185
|
|
|
* @return array Counts of summaries, raw edits, and per-month breakdown. |
|
186
|
|
|
* @codeCoverageIgnore |
|
187
|
|
|
*/ |
|
188
|
|
|
public function getData() |
|
189
|
|
|
{ |
|
190
|
|
|
return $this->data; |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
/** |
|
194
|
|
|
* Fetch the data from the database, process, and put in memory. |
|
195
|
|
|
* @codeCoverageIgnore |
|
196
|
|
|
*/ |
|
197
|
|
|
public function prepareData() |
|
198
|
|
|
{ |
|
199
|
|
|
// First try the cache. The 'data' property will be set |
|
200
|
|
|
// if it was successfully loaded. |
|
201
|
|
|
if ($this->loadFromCache()) { |
|
202
|
|
|
return; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
$resultQuery = $this->getSqlStatement(); |
|
206
|
|
|
|
|
207
|
|
|
while ($row = $resultQuery->fetch()) { |
|
208
|
|
|
$this->processRow($row); |
|
209
|
|
|
} |
|
210
|
|
|
|
|
211
|
|
|
$cache = $this->container->get('cache.app'); |
|
212
|
|
|
|
|
213
|
|
|
// Cache for 10 minutes. |
|
214
|
|
|
$cacheItem = $cache->getItem($this->getCacheKey()) |
|
215
|
|
|
->set($this->data) |
|
216
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
217
|
|
|
$cache->save($cacheItem); |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* Process a single row from the database, updating class properties with counts. |
|
222
|
|
|
* @param string[] $row As retrieved from the revision table. |
|
223
|
|
|
*/ |
|
224
|
1 |
|
private function processRow($row) |
|
225
|
|
|
{ |
|
226
|
|
|
// Extract the date out of the date field |
|
227
|
1 |
|
$timestamp = DateTime::createFromFormat('YmdHis', $row['rev_timestamp']); |
|
228
|
|
|
|
|
229
|
1 |
|
$monthKey = date_format($timestamp, 'Y-m'); |
|
230
|
|
|
|
|
231
|
|
|
// Grand total for number of edits |
|
232
|
1 |
|
$this->data['total_edits']++; |
|
233
|
|
|
|
|
234
|
|
|
// Update total edit count for this month. |
|
235
|
1 |
|
$this->updateMonthCounts($monthKey, 'total'); |
|
236
|
|
|
|
|
237
|
|
|
// Total edit summaries |
|
238
|
1 |
|
if ($this->hasSummary($row)) { |
|
239
|
1 |
|
$this->data['total_summaries']++; |
|
240
|
|
|
|
|
241
|
|
|
// Update summary count for this month. |
|
242
|
1 |
|
$this->updateMonthCounts($monthKey, 'summaries'); |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
1 |
|
if ($this->isMinor($row)) { |
|
246
|
1 |
|
$this->updateMajorMinorCounts($row, 'minor'); |
|
247
|
|
|
} else { |
|
248
|
1 |
|
$this->updateMajorMinorCounts($row, 'major'); |
|
249
|
|
|
} |
|
250
|
1 |
|
} |
|
251
|
|
|
|
|
252
|
|
|
/** |
|
253
|
|
|
* Attempt to load data from cache, and set the 'data' class property. |
|
254
|
|
|
* @return bool Whether data was successfully pulled from the cache. |
|
255
|
|
|
* @codeCoverageIgnore |
|
256
|
|
|
*/ |
|
257
|
|
View Code Duplication |
private function loadFromCache() |
|
|
|
|
|
|
258
|
|
|
{ |
|
259
|
|
|
$cacheKey = $this->getCacheKey(); |
|
260
|
|
|
|
|
261
|
|
|
$cache = $this->container->get('cache.app'); |
|
262
|
|
|
|
|
263
|
|
|
if ($cache->hasItem($cacheKey)) { |
|
264
|
|
|
$this->data = $cache->getItem($cacheKey)->get(); |
|
265
|
|
|
return true; |
|
266
|
|
|
} |
|
267
|
|
|
|
|
268
|
|
|
return false; |
|
269
|
|
|
} |
|
270
|
|
|
|
|
271
|
|
|
/** |
|
272
|
|
|
* Build cache key using helper in Repository. |
|
273
|
|
|
* @return string |
|
274
|
|
|
* @codeCoverageIgnore |
|
275
|
|
|
*/ |
|
276
|
|
|
private function getCacheKey() |
|
277
|
|
|
{ |
|
278
|
|
|
return $this->project->getRepository()->getCacheKey( |
|
279
|
|
|
[$this->project, $this->user, $this->namespace, $this->numEditsRecent], |
|
280
|
|
|
'edit_summary_usage' |
|
281
|
|
|
); |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* Given the row in `revision`, update minor counts. |
|
286
|
|
|
* @param string[] $row As retrieved from the revision table. |
|
287
|
|
|
* @param string $type Either 'minor' or 'major'. |
|
288
|
|
|
* @codeCoverageIgnore |
|
289
|
|
|
*/ |
|
290
|
|
|
private function updateMajorMinorCounts($row, $type) |
|
291
|
|
|
{ |
|
292
|
|
|
$this->data['total_edits_'.$type]++; |
|
293
|
|
|
|
|
294
|
|
|
$hasSummary = $this->hasSummary($row); |
|
295
|
|
|
$isRecent = $this->data['recent_edits_'.$type] < $this->numEditsRecent; |
|
296
|
|
|
|
|
297
|
|
|
if ($hasSummary) { |
|
298
|
|
|
$this->data['total_summaries_'.$type]++; |
|
299
|
|
|
} |
|
300
|
|
|
|
|
301
|
|
|
// Update recent edits counts. |
|
302
|
|
|
if ($isRecent) { |
|
303
|
|
|
$this->data['recent_edits_'.$type]++; |
|
304
|
|
|
|
|
305
|
|
|
if ($hasSummary) { |
|
306
|
|
|
$this->data['recent_summaries_'.$type]++; |
|
307
|
|
|
} |
|
308
|
|
|
} |
|
309
|
|
|
} |
|
310
|
|
|
|
|
311
|
|
|
/** |
|
312
|
|
|
* Was the given row in `revision` marked as a minor edit? |
|
313
|
|
|
* @param string[] $row As retrieved from the revision table. |
|
314
|
|
|
* @return boolean |
|
315
|
|
|
*/ |
|
316
|
1 |
|
private function isMinor($row) |
|
317
|
|
|
{ |
|
318
|
1 |
|
return (int)$row['rev_minor_edit'] === 1; |
|
319
|
|
|
} |
|
320
|
|
|
|
|
321
|
|
|
/** |
|
322
|
|
|
* Taking into account automated edit summaries, does the given |
|
323
|
|
|
* row in `revision` have a user-supplied edit summary? |
|
324
|
|
|
* @param string[] $row As retrieved from the revision table. |
|
325
|
|
|
* @return boolean |
|
326
|
|
|
*/ |
|
327
|
2 |
|
private function hasSummary($row) |
|
328
|
|
|
{ |
|
329
|
2 |
|
$summary = preg_replace("/^\/\* (.*?) \*\/\s*/", '', $row['rev_comment']); |
|
330
|
2 |
|
return $summary !== ''; |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
/** |
|
334
|
|
|
* Check and see if the month is set for given $monthKey and $type. |
|
335
|
|
|
* If it is, increment it, otherwise set it to 1. |
|
336
|
|
|
* @param string $monthKey In the form 'YYYY-MM'. |
|
337
|
|
|
* @param string $type Either 'total' or 'summaries'. |
|
338
|
|
|
* @codeCoverageIgnore |
|
339
|
|
|
*/ |
|
340
|
|
|
private function updateMonthCounts($monthKey, $type) |
|
341
|
|
|
{ |
|
342
|
|
|
if (isset($this->data['month_counts'][$monthKey][$type])) { |
|
343
|
|
|
$this->data['month_counts'][$monthKey][$type]++; |
|
344
|
|
|
} else { |
|
345
|
|
|
$this->data['month_counts'][$monthKey][$type] = 1; |
|
346
|
|
|
} |
|
347
|
|
|
} |
|
348
|
|
|
|
|
349
|
|
|
/** |
|
350
|
|
|
* Build and execute SQL to get edit summary usage. |
|
351
|
|
|
* @return Doctrine\DBAL\Statement |
|
352
|
|
|
* @codeCoverageIgnore |
|
353
|
|
|
*/ |
|
354
|
|
|
private function getSqlStatement() |
|
355
|
|
|
{ |
|
356
|
|
|
$revisionTable = $this->project->getTableName('revision'); |
|
357
|
|
|
$pageTable = $this->project->getTableName('page'); |
|
358
|
|
|
|
|
359
|
|
|
$condNamespace = $this->namespace === 'all' ? '' : 'AND page_namespace = :namespace'; |
|
360
|
|
|
$pageJoin = $this->namespace === 'all' ? '' : "JOIN $pageTable ON rev_page = page_id"; |
|
361
|
|
|
$username = $this->user->getUsername(); |
|
362
|
|
|
|
|
363
|
|
|
$sql = "SELECT rev_comment, rev_timestamp, rev_minor_edit |
|
364
|
|
|
FROM $revisionTable |
|
365
|
|
|
$pageJoin |
|
366
|
|
|
WHERE rev_user_text = :username |
|
367
|
|
|
$condNamespace |
|
368
|
|
|
ORDER BY rev_timestamp DESC"; |
|
369
|
|
|
|
|
370
|
|
|
$resultQuery = $this->conn->prepare($sql); |
|
371
|
|
|
$resultQuery->bindParam('username', $username); |
|
372
|
|
|
|
|
373
|
|
|
if ($this->namespace !== 'all') { |
|
374
|
|
|
$resultQuery->bindParam('namespace', $this->namespace); |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
$resultQuery->execute(); |
|
378
|
|
|
return $resultQuery; |
|
379
|
|
|
} |
|
380
|
|
|
} |
|
381
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.