Passed
Branch master (0917e1)
by MusikAnimal
11:12
created

ArticleInfo::updateContentSizesRevert()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 15.3773

Importance

Changes 0
Metric Value
cc 9
eloc 13
nc 9
nop 1
dl 0
loc 27
ccs 8
cts 14
cp 0.5714
crap 15.3773
rs 8.0555
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the ArticleInfo class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use AppBundle\Helper\I18nHelper;
11
use DateTime;
12
13
/**
14
 * An ArticleInfo provides statistics about a page on a project.
15
 */
16
class ArticleInfo extends ArticleInfoApi
17
{
18
    /** @var I18nHelper For i18n and l10n. */
19
    protected $i18n;
20
21
    /** @var int Maximum number of revisions to process, as configured. */
22
    protected $maxRevisions;
23
24
    /** @var int Number of revisions that were actually processed. */
25
    protected $numRevisionsProcessed;
26
27
    /**
28
     * Various statistics about editors to the page. These are not User objects
29
     * so as to preserve memory.
30
     * @var mixed[]
31
     */
32
    protected $editors = [];
33
34
    /** @var mixed[] The top 10 editors to the page by number of edits. */
35
    protected $topTenEditorsByEdits;
36
37
    /** @var mixed[] The top 10 editors to the page by added text. */
38
    protected $topTenEditorsByAdded;
39
40
    /** @var int Number of edits made by the top 10 editors. */
41
    protected $topTenCount;
42
43
    /** @var mixed[] Various counts about each individual year and month of the page's history. */
44
    protected $yearMonthCounts;
45
46
    /** @var string[] Localized labels for the years, to be used in the 'Year counts' chart. */
47
    protected $yearLabels = [];
48
49
    /** @var string[] Localized labels for the months, to be used in the 'Month counts' chart. */
50
    protected $monthLabels = [];
51
52
    /** @var Edit The first edit to the page. */
53
    protected $firstEdit;
54
55
    /** @var Edit The last edit to the page. */
56
    protected $lastEdit;
57
58
    /** @var Edit Edit that made the largest addition by number of bytes. */
59
    protected $maxAddition;
60
61
    /** @var Edit Edit that made the largest deletion by number of bytes. */
62
    protected $maxDeletion;
63
64
    /**
65
     * Maximum number of edits that were created across all months. This is used as a comparison
66
     * for the bar charts in the months section.
67
     * @var int
68
     */
69
    protected $maxEditsPerMonth;
70
71
    /** @var string[][] List of (semi-)automated tools that were used to edit the page. */
72
    protected $tools;
73
74
    /**
75
     * Total number of bytes added throughout the page's history. This is used as a comparison
76
     * when computing the top 10 editors by added text.
77
     * @var int
78
     */
79
    protected $addedBytes = 0;
80
81
    /** @var int Number of days between first and last edit. */
82
    protected $totalDays;
83
84
    /** @var int Number of minor edits to the page. */
85
    protected $minorCount = 0;
86
87
    /** @var int Number of anonymous edits to the page. */
88
    protected $anonCount = 0;
89
90
    /** @var int Number of automated edits to the page. */
91
    protected $automatedCount = 0;
92
93
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
94
    protected $revertCount = 0;
95
96
    /** @var int[] The "edits per <time>" counts. */
97
    protected $countHistory = [
98
        'day' => 0,
99
        'week' => 0,
100
        'month' => 0,
101
        'year' => 0,
102
    ];
103
104
    /**
105
     * Make the I18nHelper accessible to ArticleInfo.
106
     * @param I18nHelper $i18n
107
     * @codeCoverageIgnore
108
     */
109
    public function setI18nHelper(I18nHelper $i18n): void
110
    {
111
        $this->i18n = $i18n;
112
    }
113
114
    /**
115
     * Get the day of last date we should show in the month/year sections,
116
     * based on $this->end or the current date.
117
     * @return int As Unix timestamp.
118
     */
119 4
    private function getLastDay(): int
120
    {
121 4
        if (is_int($this->end)) {
122
            return (new DateTime("@{$this->end}"))
123
                ->modify('last day of this month')
124
                ->getTimestamp();
125
        } else {
126 4
            return strtotime('last day of this month');
127
        }
128
    }
129
130
    /**
131
     * Return the start/end date values as associative array, with YYYY-MM-DD as the date format.
132
     * This is used mainly as a helper to pass to the pageviews Twig macros.
133
     * @return array
134
     */
135 1
    public function getDateParams(): array
136
    {
137 1
        if (!$this->hasDateRange()) {
138
            return [];
139
        }
140
141
        $ret = [
142 1
            'start' => $this->firstEdit->getTimestamp()->format('Y-m-d'),
143 1
            'end' => $this->lastEdit->getTimestamp()->format('Y-m-d'),
144
        ];
145
146 1
        if (is_int($this->start)) {
147 1
            $ret['start'] = date('Y-m-d', $this->start);
148
        }
149 1
        if (is_int($this->end)) {
150 1
            $ret['end'] = date('Y-m-d', $this->end);
151
        }
152
153 1
        return $ret;
154
    }
155
156
    /**
157
     * Get the maximum number of revisions that we should process.
158
     * @return int
159
     */
160 3
    public function getMaxRevisions(): int
161
    {
162 3
        if (!isset($this->maxRevisions)) {
163 3
            $this->maxRevisions = (int) $this->container->getParameter('app.max_page_revisions');
164
        }
165 3
        return $this->maxRevisions;
166
    }
167
168
    /**
169
     * Get the number of revisions that are actually getting processed. This goes by the app.max_page_revisions
170
     * parameter, or the actual number of revisions, whichever is smaller.
171
     * @return int
172
     */
173 3
    public function getNumRevisionsProcessed(): int
174
    {
175 3
        if (isset($this->numRevisionsProcessed)) {
176 1
            return $this->numRevisionsProcessed;
177
        }
178
179 2
        if ($this->tooManyRevisions()) {
180 1
            $this->numRevisionsProcessed = $this->getMaxRevisions();
181
        } else {
182 1
            $this->numRevisionsProcessed = $this->getNumRevisions();
183
        }
184
185 2
        return $this->numRevisionsProcessed;
186
    }
187
188
    /**
189
     * Are there more revisions than we should process, based on the config?
190
     * @return bool
191
     */
192 3
    public function tooManyRevisions(): bool
193
    {
194 3
        return $this->getMaxRevisions() > 0 && $this->getNumRevisions() > $this->getMaxRevisions();
195
    }
196
197
    /**
198
     * Fetch and store all the data we need to show the ArticleInfo view.
199
     * @codeCoverageIgnore
200
     */
201
    public function prepareData(): void
202
    {
203
        $this->parseHistory();
204
        $this->setLogsEvents();
205
206
        // Bots need to be set before setting top 10 counts.
207
        $this->bots = $this->getBots();
208
209
        $this->doPostPrecessing();
210
    }
211
212
    /**
213
     * Get the number of editors that edited the page.
214
     * @return int
215
     */
216 1
    public function getNumEditors(): int
217
    {
218 1
        return count($this->editors);
219
    }
220
221
    /**
222
     * Get the number of days between the first and last edit.
223
     * @return int
224
     */
225 1
    public function getTotalDays(): int
226
    {
227 1
        if (isset($this->totalDays)) {
228 1
            return $this->totalDays;
229
        }
230 1
        $dateFirst = $this->firstEdit->getTimestamp();
231 1
        $dateLast = $this->lastEdit->getTimestamp();
232 1
        $interval = date_diff($dateLast, $dateFirst, true);
233 1
        $this->totalDays = (int)$interval->format('%a');
234 1
        return $this->totalDays;
235
    }
236
237
    /**
238
     * Returns length of the page.
239
     * @return int
240
     */
241 1
    public function getLength(): int
242
    {
243 1
        if ($this->hasDateRange()) {
244 1
            return $this->lastEdit->getLength();
245
        }
246
247
        return $this->page->getLength();
248
    }
249
250
    /**
251
     * Get the average number of days between edits to the page.
252
     * @return float
253
     */
254 1
    public function averageDaysPerEdit(): float
255
    {
256 1
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
257
    }
258
259
    /**
260
     * Get the average number of edits per day to the page.
261
     * @return float
262
     */
263 1
    public function editsPerDay(): float
264
    {
265 1
        $editsPerDay = $this->getTotalDays()
266 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
267 1
            : 0;
268 1
        return round($editsPerDay, 1);
269
    }
270
271
    /**
272
     * Get the average number of edits per month to the page.
273
     * @return float
274
     */
275 1
    public function editsPerMonth(): float
276
    {
277 1
        $editsPerMonth = $this->getTotalDays()
278 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
279 1
            : 0;
280 1
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
281
    }
282
283
    /**
284
     * Get the average number of edits per year to the page.
285
     * @return float
286
     */
287 1
    public function editsPerYear(): float
288
    {
289 1
        $editsPerYear = $this->getTotalDays()
290 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
291 1
            : 0;
292 1
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
293
    }
294
295
    /**
296
     * Get the average number of edits per editor.
297
     * @return float
298
     */
299 1
    public function editsPerEditor(): float
300
    {
301 1
        return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
302
    }
303
304
    /**
305
     * Get the percentage of minor edits to the page.
306
     * @return float
307
     */
308 1
    public function minorPercentage(): float
309
    {
310 1
        return round(
311 1
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
312 1
            1
313
        );
314
    }
315
316
    /**
317
     * Get the percentage of anonymous edits to the page.
318
     * @return float
319
     */
320 1
    public function anonPercentage(): float
321
    {
322 1
        return round(
323 1
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
324 1
            1
325
        );
326
    }
327
328
    /**
329
     * Get the percentage of edits made by the top 10 editors.
330
     * @return float
331
     */
332 1
    public function topTenPercentage(): float
333
    {
334 1
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
335
    }
336
337
    /**
338
     * Get the number of automated edits made to the page.
339
     * @return int
340
     */
341 1
    public function getAutomatedCount(): int
342
    {
343 1
        return $this->automatedCount;
344
    }
345
346
    /**
347
     * Get the number of edits to the page that were reverted with the subsequent edit.
348
     * @return int
349
     */
350 1
    public function getRevertCount(): int
351
    {
352 1
        return $this->revertCount;
353
    }
354
355
    /**
356
     * Get the number of edits to the page made by logged out users.
357
     * @return int
358
     */
359 1
    public function getAnonCount(): int
360
    {
361 1
        return $this->anonCount;
362
    }
363
364
    /**
365
     * Get the number of minor edits to the page.
366
     * @return int
367
     */
368 1
    public function getMinorCount(): int
369
    {
370 1
        return $this->minorCount;
371
    }
372
373
    /**
374
     * Get the number of edits to the page made in the past day, week, month and year.
375
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
376
     */
377
    public function getCountHistory(): array
378
    {
379
        return $this->countHistory;
380
    }
381
382
    /**
383
     * Get the number of edits to the page made by the top 10 editors.
384
     * @return int
385
     */
386 1
    public function getTopTenCount(): int
387
    {
388 1
        return $this->topTenCount;
389
    }
390
391
    /**
392
     * Get the first edit to the page.
393
     * @return Edit
394
     */
395 1
    public function getFirstEdit(): Edit
396
    {
397 1
        return $this->firstEdit;
398
    }
399
400
    /**
401
     * Get the last edit to the page.
402
     * @return Edit
403
     */
404 1
    public function getLastEdit(): Edit
405
    {
406 1
        return $this->lastEdit;
407
    }
408
409
    /**
410
     * Get the edit that made the largest addition to the page (by number of bytes).
411
     * @return Edit|null
412
     */
413 1
    public function getMaxAddition(): ?Edit
414
    {
415 1
        return $this->maxAddition;
416
    }
417
418
    /**
419
     * Get the edit that made the largest removal to the page (by number of bytes).
420
     * @return Edit|null
421
     */
422 1
    public function getMaxDeletion(): ?Edit
423
    {
424 1
        return $this->maxDeletion;
425
    }
426
427
    /**
428
     * Get the list of editors to the page, including various statistics.
429
     * @return mixed[]
430
     */
431 1
    public function getEditors(): array
432
    {
433 1
        return $this->editors;
434
    }
435
436
    /**
437
     * Get usernames of human editors (not bots).
438
     * @param int|null $limit
439
     * @return string[]
440
     */
441
    public function getHumans(?int $limit = null): array
442
    {
443
        return array_slice(array_diff(array_keys($this->getEditors()), array_keys($this->getBots())), 0, $limit);
444
    }
445
446
    /**
447
     * Get the list of the top editors to the page (by edits), including various statistics.
448
     * @return mixed[]
449
     */
450 1
    public function topTenEditorsByEdits(): array
451
    {
452 1
        return $this->topTenEditorsByEdits;
453
    }
454
455
    /**
456
     * Get the list of the top editors to the page (by added text), including various statistics.
457
     * @return mixed[]
458
     */
459 1
    public function topTenEditorsByAdded(): array
460
    {
461 1
        return $this->topTenEditorsByAdded;
462
    }
463
464
    /**
465
     * Get various counts about each individual year and month of the page's history.
466
     * @return mixed[]
467
     */
468 2
    public function getYearMonthCounts(): array
469
    {
470 2
        return $this->yearMonthCounts;
471
    }
472
473
    /**
474
     * Get the localized labels for the 'Year counts' chart.
475
     * @return string[]
476
     */
477
    public function getYearLabels(): array
478
    {
479
        return $this->yearLabels;
480
    }
481
482
    /**
483
     * Get the localized labels for the 'Month counts' chart.
484
     * @return string[]
485
     */
486
    public function getMonthLabels(): array
487
    {
488
        return $this->monthLabels;
489
    }
490
491
    /**
492
     * Get the maximum number of edits that were created across all months. This is used as a
493
     * comparison for the bar charts in the months section.
494
     * @return int
495
     */
496 1
    public function getMaxEditsPerMonth(): int
497
    {
498 1
        return $this->maxEditsPerMonth;
499
    }
500
501
    /**
502
     * Get a list of (semi-)automated tools that were used to edit the page, including
503
     * the number of times they were used, and a link to the tool's homepage.
504
     * @return string[]
505
     */
506 1
    public function getTools(): array
507
    {
508 1
        return $this->tools;
509
    }
510
511
    /**
512
     * Parse the revision history, collecting our core statistics.
513
     *
514
     * Untestable because it relies on getting a PDO statement. All the important
515
     * logic lives in other methods which are tested.
516
     * @codeCoverageIgnore
517
     */
518
    private function parseHistory(): void
519
    {
520
        if ($this->tooManyRevisions()) {
521
            $limit = $this->getMaxRevisions();
522
        } else {
523
            $limit = null;
524
        }
525
526
        // Third parameter is ignored if $limit is null.
527
        $revStmt = $this->page->getRevisionsStmt(
528
            null,
529
            $limit,
530
            $this->getNumRevisions(),
531
            $this->start,
532
            $this->end
533
        );
534
        $revCount = 0;
535
536
        /**
537
         * Data about previous edits so that we can use them as a basis for comparison.
538
         * @var Edit[]
539
         */
540
        $prevEdits = [
541
            // The previous Edit, used to discount content that was reverted.
542
            'prev' => null,
543
544
            // The SHA-1 of the edit *before* the previous edit. Used for more
545
            // accurate revert detection.
546
            'prevSha' => null,
547
548
            // The last edit deemed to be the max addition of content. This is kept track of
549
            // in case we find out the next edit was reverted (and was also a max edit),
550
            // in which case we'll want to discount it and use this one instead.
551
            'maxAddition' => null,
552
553
            // Same as with maxAddition, except the maximum amount of content deleted.
554
            // This is used to discount content that was reverted.
555
            'maxDeletion' => null,
556
        ];
557
558
        while ($rev = $revStmt->fetch()) {
559
            $edit = new Edit($this->page, $rev);
560
561
            if (0 === $revCount) {
562
                $this->firstEdit = $edit;
563
            }
564
565
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
566
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
567
                $this->firstEdit = $edit;
568
            }
569
570
            $prevEdits = $this->updateCounts($edit, $prevEdits);
571
572
            $revCount++;
573
        }
574
575
        $this->numRevisionsProcessed = $revCount;
576
577
        // Various sorts
578
        arsort($this->editors);
579
        ksort($this->yearMonthCounts);
580
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type array<mixed,string[]> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
581
            arsort($this->tools);
582
        }
583
    }
584
585
    /**
586
     * Update various counts based on the current edit.
587
     * @param Edit $edit
588
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'
589
     * @return Edit[] Updated version of $prevEdits.
590
     */
591 4
    private function updateCounts(Edit $edit, array $prevEdits): array
592
    {
593
        // Update the counts for the year and month of the current edit.
594 4
        $this->updateYearMonthCounts($edit);
595
596
        // Update counts for the user who made the edit.
597 4
        $this->updateUserCounts($edit);
598
599
        // Update the year/month/user counts of anon and minor edits.
600 4
        $this->updateAnonMinorCounts($edit);
601
602
        // Update counts for automated tool usage, if applicable.
603 4
        $this->updateToolCounts($edit);
604
605
        // Increment "edits per <time>" counts
606 4
        $this->updateCountHistory($edit);
607
608
        // Update figures regarding content addition/removal, and the revert count.
609 4
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
610
611
        // Now that we've updated all the counts, we can reset
612
        // the prev and last edits, which are used for tracking.
613
        // But first, let's copy over the SHA of the actual previous edit
614
        // and put it in our $prevEdits['prev'], so that we'll know
615
        // that content added after $prevEdit['prev'] was reverted.
616 4
        if (null !== $prevEdits['prev']) {
617 4
            $prevEdits['prevSha'] = $prevEdits['prev']->getSha();
618
        }
619 4
        $prevEdits['prev'] = $edit;
620 4
        $this->lastEdit = $edit;
621
622 4
        return $prevEdits;
623
    }
624
625
    /**
626
     * Update various figures about content sizes based on the given edit.
627
     * @param Edit $edit
628
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
629
     * @return Edit[] Updated version of $prevEdits.
630
     */
631 4
    private function updateContentSizes(Edit &$edit, array $prevEdits): array
632
    {
633
        // Check if it was a revert
634 4
        if ($this->isRevert($edit, $prevEdits)) {
635 4
            $edit->setReverted(true);
636 4
            return $this->updateContentSizesRevert($prevEdits);
637
        } else {
638 4
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
639
        }
640
    }
641
642
    /**
643
     * Is the given Edit a revert?
644
     * @param Edit $edit
645
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
646
     * @return bool
647
     */
648 4
    private function isRevert(Edit $edit, array $prevEdits): bool
649
    {
650 4
        return $edit->getSha() === $prevEdits['prevSha'] || $edit->isRevert($this->container);
651
    }
652
653
    /**
654
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
655
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
656
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
657
     * @return Edit[] Updated version of $prevEdits, for tracking.
658
     */
659 4
    private function updateContentSizesRevert(array $prevEdits): array
660
    {
661 4
        $this->revertCount++;
662
663
        // Adjust addedBytes given this edit was a revert of the previous one.
664 4
        if ($prevEdits['prev'] && !$prevEdits['prev']->isReverted() && $prevEdits['prev']->getSize() > 0) {
665
            $this->addedBytes -= $prevEdits['prev']->getSize();
666
667
            // Also deduct from the user's individual added byte count.
668
            // We don't do this if the previous edit was reverted, since that would make the net bytes zero.
669
            if ($prevEdits['prev']->getUser()) {
670
                $username = $prevEdits['prev']->getUser()->getUsername();
671
                $this->editors[$username]['added'] -= $prevEdits['prev']->getSize();
672
            }
673
        }
674
675
        // @TODO: Test this against an edit war (use your sandbox).
676
        // Also remove as max added or deleted, if applicable.
677 4
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
678
            $this->maxAddition = $prevEdits['maxAddition'];
679
            $prevEdits['maxAddition'] = $prevEdits['prev']; // In the event of edit wars.
680 4
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
681 4
            $this->maxDeletion = $prevEdits['maxDeletion'];
682 4
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // In the event of edit wars.
683
        }
684
685 4
        return $prevEdits;
686
    }
687
688
    /**
689
     * Updates the figures on content sizes assuming the given edit was NOT a revert of the previous edit.
690
     * @param Edit $edit
691
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
692
     * @return Edit[] Updated version of $prevEdits, for tracking.
693
     */
694 4
    private function updateContentSizesNonRevert(Edit $edit, array $prevEdits): array
695
    {
696 4
        $editSize = $this->getEditSize($edit, $prevEdits);
697
698
        // Edit was not a revert, so treat size > 0 as content added.
699 4
        if ($editSize > 0) {
700 4
            $this->addedBytes += $editSize;
701
702 4
            if ($edit->getUser()) {
703 4
                $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
704
            }
705
706
            // Keep track of edit with max addition.
707 4
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
708
                // Keep track of old maxAddition in case we find out the next $edit was reverted
709
                // (and was also a max edit), in which case we'll want to use this one ($edit).
710 4
                $prevEdits['maxAddition'] = $this->maxAddition;
711
712 4
                $this->maxAddition = $edit;
713
            }
714 4
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
715
            // Keep track of old maxDeletion in case we find out the next edit was reverted
716
            // (and was also a max deletion), in which case we'll want to use this one.
717 4
            $prevEdits['maxDeletion'] = $this->maxDeletion;
718
719 4
            $this->maxDeletion = $edit;
720
        }
721
722 4
        return $prevEdits;
723
    }
724
725
    /**
726
     * Get the size of the given edit, based on the previous edit (if present).
727
     * We also don't return the actual edit size if last revision had a length of null.
728
     * This happens when the edit follows other edits that were revision-deleted.
729
     * @see T148857 for more information.
730
     * @todo Remove once T101631 is resolved.
731
     * @param Edit $edit
732
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
733
     * @return int
734
     */
735 4
    private function getEditSize(Edit $edit, array $prevEdits): int
736
    {
737 4
        if ($prevEdits['prev'] && null === $prevEdits['prev']->getLength()) {
0 ignored issues
show
introduced by
The condition null === $prevEdits['prev']->getLength() is always false.
Loading history...
738
            return 0;
739
        } else {
740 4
            return $edit->getSize();
741
        }
742
    }
743
744
    /**
745
     * Update counts of automated tool usage for the given edit.
746
     * @param Edit $edit
747
     */
748 4
    private function updateToolCounts(Edit $edit): void
749
    {
750 4
        $automatedTool = $edit->getTool($this->container);
751
752 4
        if (false === $automatedTool) {
753
            // Nothing to do.
754 4
            return;
755
        }
756
757 4
        $editYear = $edit->getYear();
758 4
        $editMonth = $edit->getMonth();
759
760 4
        $this->automatedCount++;
761 4
        $this->yearMonthCounts[$editYear]['automated']++;
762 4
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
763
764 4
        if (!isset($this->tools[$automatedTool['name']])) {
765 4
            $this->tools[$automatedTool['name']] = [
766 4
                'count' => 1,
767 4
                'link' => $automatedTool['link'],
768
            ];
769
        } else {
770
            $this->tools[$automatedTool['name']]['count']++;
771
        }
772 4
    }
773
774
    /**
775
     * Update various counts for the year and month of the given edit.
776
     * @param Edit $edit
777
     */
778 4
    private function updateYearMonthCounts(Edit $edit): void
779
    {
780 4
        $editYear = $edit->getYear();
781 4
        $editMonth = $edit->getMonth();
782
783
        // Fill in the blank arrays for the year and 12 months if needed.
784 4
        if (!isset($this->yearMonthCounts[$editYear])) {
785 4
            $this->addYearMonthCountEntry($edit);
786
        }
787
788
        // Increment year and month counts for all edits
789 4
        $this->yearMonthCounts[$editYear]['all']++;
790 4
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
791
        // This will ultimately be the size of the page by the end of the year
792 4
        $this->yearMonthCounts[$editYear]['size'] = (int) $edit->getLength();
793
794
        // Keep track of which month had the most edits
795 4
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
796 4
        if ($editsThisMonth > $this->maxEditsPerMonth) {
797 4
            $this->maxEditsPerMonth = $editsThisMonth;
798
        }
799 4
    }
800
801
    /**
802
     * Add a new entry to $this->yearMonthCounts for the given year,
803
     * with blank values for each month. This called during self::parseHistory().
804
     * @param Edit $edit
805
     */
806 4
    private function addYearMonthCountEntry(Edit $edit): void
807
    {
808 4
        $this->yearLabels[] = $this->i18n->dateFormat($edit->getTimestamp(), 'yyyy');
809 4
        $editYear = $edit->getYear();
810
811
        // Beginning of the month at 00:00:00.
812 4
        $firstEditTime = mktime(0, 0, 0, (int)$this->firstEdit->getMonth(), 1, (int)$this->firstEdit->getYear());
813
814 4
        $this->yearMonthCounts[$editYear] = [
815
            'all' => 0,
816
            'minor' => 0,
817
            'anon' => 0,
818
            'automated' => 0,
819
            'size' => 0, // Keep track of the size by the end of the year.
820
            'events' => [],
821
            'months' => [],
822
        ];
823
824 4
        for ($i = 1; $i <= 12; $i++) {
825 4
            $timeObj = mktime(0, 0, 0, $i, 1, (int)$editYear);
826
827
            // Don't show zeros for months before the first edit or after the current month.
828 4
            if ($timeObj < $firstEditTime || $timeObj > $this->getLastDay()) {
829 4
                continue;
830
            }
831
832 4
            $this->monthLabels[] = $this->i18n->dateFormat($timeObj, 'yyyy-MM');
833 4
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
834
                'all' => 0,
835
                'minor' => 0,
836
                'anon' => 0,
837
                'automated' => 0,
838
            ];
839
        }
840 4
    }
841
842
    /**
843
     * Update the counts of anon and minor edits for year, month, and user of the given edit.
844
     * @param Edit $edit
845
     */
846 4
    private function updateAnonMinorCounts(Edit $edit): void
847
    {
848 4
        $editYear = $edit->getYear();
849 4
        $editMonth = $edit->getMonth();
850
851
        // If anonymous, increase counts
852 4
        if ($edit->isAnon()) {
853 4
            $this->anonCount++;
854 4
            $this->yearMonthCounts[$editYear]['anon']++;
855 4
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
856
        }
857
858
        // If minor edit, increase counts
859 4
        if ($edit->isMinor()) {
860 4
            $this->minorCount++;
861 4
            $this->yearMonthCounts[$editYear]['minor']++;
862 4
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
863
        }
864 4
    }
865
866
    /**
867
     * Update various counts for the user of the given edit.
868
     * @param Edit $edit
869
     */
870 4
    private function updateUserCounts(Edit $edit): void
871
    {
872 4
        if (!$edit->getUser()) {
873
            return;
874
        }
875
876 4
        $username = $edit->getUser()->getUsername();
877
878
        // Initialize various user stats if needed.
879 4
        if (!isset($this->editors[$username])) {
880 4
            $this->editors[$username] = [
881 4
                'all' => 0,
882 4
                'minor' => 0,
883 4
                'minorPercentage' => 0,
884 4
                'first' => $edit->getTimestamp(),
885 4
                'firstId' => $edit->getId(),
886
                'last' => null,
887
                'atbe' => null,
888 4
                'added' => 0,
889
            ];
890
        }
891
892
        // Increment user counts
893 4
        $this->editors[$username]['all']++;
894 4
        $this->editors[$username]['last'] = $edit->getTimestamp();
895 4
        $this->editors[$username]['lastId'] = $edit->getId();
896
897
        // Increment minor counts for this user
898 4
        if ($edit->isMinor()) {
899 4
            $this->editors[$username]['minor']++;
900
        }
901 4
    }
902
903
    /**
904
     * Increment "edits per <time>" counts based on the given edit.
905
     * @param Edit $edit
906
     */
907 4
    private function updateCountHistory(Edit $edit): void
908
    {
909 4
        $editTimestamp = $edit->getTimestamp();
910
911 4
        if ($editTimestamp > new DateTime('-1 day')) {
912
            $this->countHistory['day']++;
913
        }
914 4
        if ($editTimestamp > new DateTime('-1 week')) {
915
            $this->countHistory['week']++;
916
        }
917 4
        if ($editTimestamp > new DateTime('-1 month')) {
918
            $this->countHistory['month']++;
919
        }
920 4
        if ($editTimestamp > new DateTime('-1 year')) {
921
            $this->countHistory['year']++;
922
        }
923 4
    }
924
925
    /**
926
     * Query for log events during each year of the article's history, and set the results in $this->yearMonthCounts.
927
     */
928 1
    private function setLogsEvents(): void
929
    {
930 1
        $logData = $this->getRepository()->getLogEvents(
0 ignored issues
show
Bug introduced by
The method getLogEvents() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\ArticleInfoRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

930
        $logData = $this->getRepository()->/** @scrutinizer ignore-call */ getLogEvents(
Loading history...
931 1
            $this->page,
932 1
            $this->start,
933 1
            $this->end
934
        );
935
936 1
        foreach ($logData as $event) {
937 1
            $time = strtotime($event['timestamp']);
938 1
            $year = date('Y', $time);
939
940 1
            if (!isset($this->yearMonthCounts[$year])) {
941
                break;
942
            }
943
944 1
            $yearEvents = $this->yearMonthCounts[$year]['events'];
945
946
            // Convert log type value to i18n key.
947 1
            switch ($event['log_type']) {
948
                // count pending-changes protections along with normal protections.
949 1
                case 'stable':
950 1
                case 'protect':
951 1
                    $action = 'protections';
952 1
                    break;
953 1
                case 'delete':
954 1
                    $action = 'deletions';
955 1
                    break;
956
                case 'move':
957
                    $action = 'moves';
958
                    break;
959
            }
960
961 1
            if (empty($yearEvents[$action])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $action does not seem to be defined for all execution paths leading up to this point.
Loading history...
962 1
                $yearEvents[$action] = 1;
963
            } else {
964
                $yearEvents[$action]++;
965
            }
966
967 1
            $this->yearMonthCounts[$year]['events'] = $yearEvents;
968
        }
969 1
    }
970
971
    /**
972
     * Set statistics about the top 10 editors by added text and number of edits.
973
     * This is ran *after* parseHistory() since we need the grand totals first.
974
     * Various stats are also set for each editor in $this->editors to be used in the charts.
975
     */
976 4
    private function doPostPrecessing(): void
977
    {
978 4
        $topTenCount = $counter = 0;
979 4
        $topTenEditorsByEdits = [];
980
981 4
        foreach ($this->editors as $editor => $info) {
982
            // Count how many users are in the top 10% by number of edits, excluding bots.
983 4
            if ($counter < 10 && !in_array($editor, array_keys($this->bots))) {
984 4
                $topTenCount += $info['all'];
985 4
                $counter++;
986
987
                // To be used in the Top Ten charts.
988 4
                $topTenEditorsByEdits[] = [
989 4
                    'label' => $editor,
990 4
                    'value' => $info['all'],
991
                ];
992
            }
993
994
            // Compute the percentage of minor edits the user made.
995 4
            $this->editors[$editor]['minorPercentage'] = $info['all']
996 4
                ? ($info['minor'] / $info['all']) * 100
997
                : 0;
998
999 4
            if ($info['all'] > 1) {
1000
                // Number of seconds/days between first and last edit.
1001 4
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
1002 4
                $days = $secs / (60 * 60 * 24);
1003
1004
                // Average time between edits (in days).
1005 4
                $this->editors[$editor]['atbe'] = round($days / $info['all'], 1);
1006
            }
1007
        }
1008
1009
        // Loop through again and add percentages.
1010
        $this->topTenEditorsByEdits = array_map(function ($editor) use ($topTenCount) {
1011 4
            $editor['percentage'] = 100 * ($editor['value'] / $topTenCount);
1012 4
            return $editor;
1013 4
        }, $topTenEditorsByEdits);
1014
1015 4
        $this->topTenEditorsByAdded = $this->getTopTenByAdded();
1016
1017 4
        $this->topTenCount = $topTenCount;
1018 4
    }
1019
1020
    /**
1021
     * Get the top ten editors by added text.
1022
     * @return array With keys 'label', 'value' and 'percentage', ready to be used by the pieChart Twig helper.
1023
     */
1024 4
    private function getTopTenByAdded(): array
1025
    {
1026
        // First sort editors array by the amount of text they added.
1027 4
        $topTenEditorsByAdded = $this->editors;
1028
        uasort($topTenEditorsByAdded, function ($a, $b) {
1029 4
            if ($a['added'] === $b['added']) {
1030 4
                return 0;
1031
            }
1032 4
            return $a['added'] > $b['added'] ? -1 : 1;
1033 4
        });
1034
1035
        // Slice to the top 10.
1036 4
        $topTenEditorsByAdded = array_keys(array_slice($topTenEditorsByAdded, 0, 10, true));
1037
1038
         // Get the sum of added text so that we can add in percentages.
1039
         $topTenTotalAdded = array_sum(array_map(function ($editor) {
1040 4
             return $this->editors[$editor]['added'];
1041 4
         }, $topTenEditorsByAdded));
1042
1043
        // Then build a new array of top 10 editors by added text in the data structure needed for the chart.
1044
        return array_map(function ($editor) use ($topTenTotalAdded) {
1045 4
            $added = $this->editors[$editor]['added'];
1046
            return [
1047 4
                'label' => $editor,
1048 4
                'value' => $added,
1049 4
                'percentage' => 0 === $this->addedBytes
1050
                    ? 0
1051 4
                    : 100 * ($added / $topTenTotalAdded),
1052
            ];
1053 4
        }, $topTenEditorsByAdded);
1054
    }
1055
1056
    /**
1057
     * Get the number of times the page has been viewed in the given timeframe. If the ArticleInfo instance has a
1058
     * date range, it is used instead of the value of the $latest parameter.
1059
     * @param  int $latest Last N days.
1060
     * @return int
1061
     */
1062
    public function getPageviews(int $latest): int
1063
    {
1064
        if (!$this->hasDateRange()) {
1065
            return $this->page->getLastPageviews($latest);
1066
        }
1067
1068
        $daterange = $this->getDateParams();
1069
        return $this->page->getPageviews($daterange['start'], $daterange['end']);
1070
    }
1071
}
1072