Passed
Push — master ( 5e3306...f3413b )
by MusikAnimal
05:40
created

EditSummary::getTotalEditsMinor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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