Completed
Push — master ( 190d9b...e1c46c )
by MusikAnimal
13s
created

EditCounter::approvals()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 8
nop 0
1
<?php
2
/**
3
 * This file contains only the EditCounter class.
4
 */
5
6
namespace Xtools;
7
8
use DateTime;
9
use Exception;
10
use DatePeriod;
11
use DateInterval;
12
use GuzzleHttp;
13
use GuzzleHttp\Promise\Promise;
14
15
/**
16
 * An EditCounter provides statistics about a user's edits on a project.
17
 */
18
class EditCounter extends Model
19
{
20
21
    /** @var Project The project. */
22
    protected $project;
23
24
    /** @var User The user. */
25
    protected $user;
26
27
    /** @var int[] Revision and page counts etc. */
28
    protected $pairData;
29
30
    /** @var string[] The start and end dates of revisions. */
31
    protected $revisionDates;
32
33
    /** @var int[] The total page counts. */
34
    protected $pageCounts;
35
36
    /** @var int[] The lot totals. */
37
    protected $logCounts;
38
39
    /** @var mixed[] Total numbers of edits per month */
40
    protected $monthCounts;
41
42
    /** @var mixed[] Total numbers of edits per year */
43
    protected $yearCounts;
44
45
    /** @var int[] Keys are project DB names. */
46
    protected $globalEditCounts;
47
48
    /** @var array Block data, with keys 'set' and 'received'. */
49
    protected $blocks;
50
51
    /** @var integer[] Array keys are namespace IDs, values are the edit counts */
52
    protected $namespaceTotals;
53
54
    /** @var int Number of semi-automated edits */
55
    protected $autoEditCount;
56
57
    /** @var string[] Data needed for time card chart */
58
    protected $timeCardData;
59
60
    /**
61
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
62
     * @var string[] As returned by the DB, unconverted to int or float
63
     */
64
    protected $editSizeData;
65
66
    /**
67
     * Duration of the longest block in days; -1 if indefinite,
68
     *   or false if could not be parsed from log params
69
     * @var int|bool
70
     */
71
    protected $longestBlockDays;
72
73
    /**
74
     * EditCounter constructor.
75
     * @param Project $project The base project to count edits
76
     * @param User $user
77
     */
78
    public function __construct(Project $project, User $user)
79
    {
80
        $this->project = $project;
81
        $this->user = $user;
82
    }
83
84
    /**
85
     * This method asynchronously fetches all the expensive data, waits
86
     * for each request to finish, and copies the values to the class instance.
87
     * @return null
88
     */
89
    public function prepareData()
90
    {
91
        $project = $this->project->getDomain();
92
        $username = $this->user->getUsername();
93
94
        /**
95
         * The URL of each endpoint, keyed by the name of the corresponding class-level
96
         * instance variable.
97
         * @var array[]
98
         */
99
        $endpoints = [
100
            "pairData" => "ec/pairdata/$project/$username",
101
            "logCounts" => "ec/logcounts/$project/$username",
102
            "namespaceTotals" => "ec/namespacetotals/$project/$username",
103
            "editSizeData" => "ec/editsizes/$project/$username",
104
            "monthCounts" => "ec/monthcounts/$project/$username",
105
            // "globalEditCounts" => "ec-globaleditcounts/$project/$username",
1 ignored issue
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
106
            "autoEditCount" => "api/user/automated_editcount/$project/$username",
107
        ];
108
109
        /**
110
         * Keep track of all promises so we can wait for all of them to complete.
111
         * @var GuzzleHttp\Promise\Promise[]
112
         */
113
        $promises = [];
114
115
        foreach ($endpoints as $key => $endpoint) {
116
            $promise = $this->getRepository()->queryXToolsApi($endpoint, true);
117
            $promises[] = $promise;
118
119
            // Handle response of $promise asynchronously.
120
            $promise->then(function ($response) use ($key, $endpoint) {
121
                $result = (array) json_decode($response->getBody()->getContents());
122
123
                if (isset($result)) {
124
                    // Copy result to the class class instance. From here any subsequent
125
                    // calls to the getters (e.g. getPairData()) will return these cached values.
126
                    $this->{$key} = $result;
127
                } else {
128
                    // The API should *always* return something, so if $result is not set,
129
                    // something went wrong, so we simply won't set it and the getters will in
130
                    // turn re-attempt to get the data synchronously.
131
                    // We'll log this to see how often it happens.
132
                    $this->getRepository()
133
                        ->getLog()
134
                        ->error("Failed to fetch data for $endpoint via async, " .
135
                            "re-attempting synchoronously.");
136
                }
137
            });
138
        }
139
140
        // Wait for all promises to complete, even if some of them fail.
141
        GuzzleHttp\Promise\settle($promises)->wait();
142
143
        // Everything we need now lives on the class instance, so we're done.
144
        return;
145
    }
146
147
    /**
148
     * Get revision and page counts etc.
149
     * @return int[]
150
     */
151
    public function getPairData()
152
    {
153
        if (!is_array($this->pairData)) {
154
            $this->pairData = $this->getRepository()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getPairData() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
155
                ->getPairData($this->project, $this->user);
156
        }
157
        return $this->pairData;
158
    }
159
160
    /**
161
     * Get revision dates.
162
     * @return int[]
163
     */
164
    public function getLogCounts()
165
    {
166
        if (!is_array($this->logCounts)) {
167
            $this->logCounts = $this->getRepository()
0 ignored issues
show
Bug introduced by
The method getLogCounts() does not exist on Xtools\Repository. Did you maybe mean getLog()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
168
                ->getLogCounts($this->project, $this->user);
169
        }
170
        return $this->logCounts;
171
    }
172
173
    /**
174
     * Get block data.
175
     * @param string $type Either 'set' or 'received'.
176
     * @return array
177
     */
178
    public function getBlocks($type)
179
    {
180
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
181
            return $this->blocks[$type];
182
        }
183
        $method = "getBlocks".ucfirst($type);
184
        $blocks = $this->getRepository()->$method($this->project, $this->user);
185
        $this->blocks[$type] = $blocks;
186
        return $this->blocks[$type];
187
    }
188
189
    /**
190
     * Get the total number of currently-live revisions.
191
     * @return int
192
     */
193
    public function countLiveRevisions()
194
    {
195
        $revCounts = $this->getPairData();
196
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
197
    }
198
199
    /**
200
     * Get the total number of the user's revisions that have been deleted.
201
     * @return int
202
     */
203
    public function countDeletedRevisions()
204
    {
205
        $revCounts = $this->getPairData();
206
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
207
    }
208
209
    /**
210
     * Get the total edit count (live + deleted).
211
     * @return int
212
     */
213
    public function countAllRevisions()
214
    {
215
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
216
    }
217
218
    /**
219
     * Get the total number of live revisions with comments.
220
     * @return int
221
     */
222
    public function countRevisionsWithComments()
223
    {
224
        $revCounts = $this->getPairData();
225
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
226
    }
227
228
    /**
229
     * Get the total number of live revisions without comments.
230
     * @return int
231
     */
232
    public function countRevisionsWithoutComments()
233
    {
234
        return $this->countLiveRevisions() - $this->countRevisionsWithComments();
235
    }
236
237
    /**
238
     * Get the total number of revisions marked as 'minor' by the user.
239
     * @return int
240
     */
241
    public function countMinorRevisions()
242
    {
243
        $revCounts = $this->getPairData();
244
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
245
    }
246
247
    /**
248
     * Get the total number of non-deleted pages edited by the user.
249
     * @return int
250
     */
251
    public function countLivePagesEdited()
252
    {
253
        $pageCounts = $this->getPairData();
254
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
255
    }
256
257
    /**
258
     * Get the total number of deleted pages ever edited by the user.
259
     * @return int
260
     */
261
    public function countDeletedPagesEdited()
262
    {
263
        $pageCounts = $this->getPairData();
264
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
265
    }
266
267
    /**
268
     * Get the total number of pages ever edited by this user (both live and deleted).
269
     * @return int
270
     */
271
    public function countAllPagesEdited()
272
    {
273
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
274
    }
275
276
    /**
277
     * Get the total number of pages (both still live and those that have been deleted) created
278
     * by the user.
279
     * @return int
280
     */
281
    public function countPagesCreated()
282
    {
283
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
284
    }
285
286
    /**
287
     * Get the total number of pages created by the user, that have not been deleted.
288
     * @return int
289
     */
290
    public function countCreatedPagesLive()
291
    {
292
        $pageCounts = $this->getPairData();
293
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
294
    }
295
296
    /**
297
     * Get the total number of pages created by the user, that have since been deleted.
298
     * @return int
299
     */
300
    public function countPagesCreatedDeleted()
301
    {
302
        $pageCounts = $this->getPairData();
303
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
304
    }
305
306
    /**
307
     * Get the total number of pages that have been deleted by the user.
308
     * @return int
309
     */
310
    public function countPagesDeleted()
311
    {
312
        $logCounts = $this->getLogCounts();
313
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
314
    }
315
316
    /**
317
     * Get the total number of pages moved by the user.
318
     * @return int
319
     */
320
    public function countPagesMoved()
321
    {
322
        $logCounts = $this->getLogCounts();
323
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
324
    }
325
326
    /**
327
     * Get the total number of times the user has blocked a user.
328
     * @return int
329
     */
330
    public function countBlocksSet()
331
    {
332
        $logCounts = $this->getLogCounts();
333
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
334
        return $reBlock;
335
    }
336
337
    /**
338
     * Get the total number of times the user has re-blocked a user.
339
     * @return int
340
     */
341
    public function countReblocksSet()
342
    {
343
        $logCounts = $this->getLogCounts();
344
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
345
        return $reBlock;
346
    }
347
348
    /**
349
     * Get the total number of times the user has unblocked a user.
350
     * @return int
351
     */
352
    public function countUnblocksSet()
353
    {
354
        $logCounts = $this->getLogCounts();
355
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
356
    }
357
358
    /**
359
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
360
     * @return int
361
     */
362
    public function countBlocksLifted()
363
    {
364
        $logCounts = $this->getLogCounts();
365
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
366
    }
367
368
    /**
369
     * Get the total number of times the user has been blocked.
370
     * @return int
371
     */
372
    public function countBlocksReceived()
373
    {
374
        $blocks = $this->getBlocks('received');
375
        return count($blocks);
376
    }
377
378
    /**
379
     * Get the length of the longest block the user received.
380
     * @return int|bool Number of days or false if it could not be determined.
381
     *                  If the longest duration is indefinite, -1 is returned.
382
     */
383
    public function getLongestBlockDays()
384
    {
385
        if (isset($this->longestBlockDays)) {
386
            return $this->longestBlockDays;
387
        }
388
389
        $blocks = $this->getBlocks('received'); // FIXME: make sure this is only called once
390
        $this->longestBlockDays = false;
391
392
        foreach ($blocks as $block) {
393
            $timestamp = strtotime($block['log_timestamp']);
394
395
            // First check if the string is serialized, and if so parse it to get the block duration
396
            if (@unserialize($block['log_params']) !== false) {
397
                $parsedParams = unserialize($block['log_params']);
398
                $durationStr = $parsedParams['5::duration'];
399
            } else {
400
                // Old format, the duration in English + block options separated by new lines
401
                $durationStr = explode("\n", $block['log_params'])[0];
402
            }
403
404
            if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
405
                return -1;
406
            }
407
408
            // Try block just in case there are older, unpredictable formats
409
            try {
410
                $expiry = strtotime($durationStr, $timestamp);
411
                $duration = ($expiry - $timestamp) / (60 * 60 * 24);
412
413
                if (!$duration || $duration > $this->longestBlockDays) {
414
                    $this->longestBlockDays = $duration;
415
                }
416
            } catch (Exception $error) {
417
                // do nothing, leaving the longest block at false
418
            }
419
        }
420
421
        return $this->longestBlockDays;
422
    }
423
424
    /**
425
     * Get the total number of pages protected by the user.
426
     * @return int
427
     */
428
    public function countPagesProtected()
429
    {
430
        $logCounts = $this->getLogCounts();
431
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
432
    }
433
434
    /**
435
     * Get the total number of pages reprotected by the user.
436
     * @return int
437
     */
438
    public function countPagesReprotected()
439
    {
440
        $logCounts = $this->getLogCounts();
441
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
442
    }
443
444
    /**
445
     * Get the total number of pages unprotected by the user.
446
     * @return int
447
     */
448
    public function countPagesUnprotected()
449
    {
450
        $logCounts = $this->getLogCounts();
451
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
452
    }
453
454
    /**
455
     * Get the total number of edits deleted by the user.
456
     * @return int
457
     */
458
    public function countEditsDeleted()
459
    {
460
        $logCounts = $this->getLogCounts();
461
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
462
    }
463
464
    /**
465
     * Get the total number of pages restored by the user.
466
     * @return int
467
     */
468
    public function countPagesRestored()
469
    {
470
        $logCounts = $this->getLogCounts();
471
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
472
    }
473
474
    /**
475
     * Get the total number of times the user has modified the rights of a user.
476
     * @return int
477
     */
478
    public function countRightsModified()
479
    {
480
        $logCounts = $this->getLogCounts();
481
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
482
    }
483
484
    /**
485
     * Get the total number of pages imported by the user (through any import mechanism:
486
     * interwiki, or XML upload).
487
     * @return int
488
     */
489
    public function countPagesImported()
490
    {
491
        $logCounts = $this->getLogCounts();
492
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
493
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
494
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
495
        return $import + $interwiki + $upload;
496
    }
497
498
    /**
499
     * Get the average number of edits per page (including deleted revisions and pages).
500
     * @return float
501
     */
502
    public function averageRevisionsPerPage()
503
    {
504
        if ($this->countAllPagesEdited() == 0) {
505
            return 0;
506
        }
507
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
508
    }
509
510
    /**
511
     * Average number of edits made per day.
512
     * @return float
513
     */
514
    public function averageRevisionsPerDay()
515
    {
516
        if ($this->getDays() == 0) {
517
            return 0;
518
        }
519
        return round($this->countAllRevisions() / $this->getDays(), 3);
520
    }
521
522
    /**
523
     * Get the total number of edits made by the user with semi-automating tools.
524
     */
525
    public function countAutomatedEdits()
526
    {
527
        if ($this->autoEditCount) {
528
            return $this->autoEditCount;
529
        }
530
        $this->autoEditCount = $this->user->countAutomatedEdits($this->project);
531
        return $this->autoEditCount;
532
    }
533
534
    /**
535
     * Get the count of (non-deleted) edits made in the given timeframe to now.
536
     * @param string $time One of 'day', 'week', 'month', or 'year'.
537
     * @return int The total number of live edits.
538
     */
539
    public function countRevisionsInLast($time)
540
    {
541
        $revCounts = $this->getPairData();
542
        return isset($revCounts[$time]) ? $revCounts[$time] : 0;
543
    }
544
545
    /**
546
     * Get the date and time of the user's first edit.
547
     * @return DateTime|bool The time of the first revision, or false.
548
     */
549
    public function datetimeFirstRevision()
550
    {
551
        $revDates = $this->getPairData();
552
        return isset($revDates['first']) ? new DateTime($revDates['first']) : false;
553
    }
554
555
    /**
556
     * Get the date and time of the user's first edit.
557
     * @return DateTime|bool The time of the last revision, or false.
558
     */
559
    public function datetimeLastRevision()
560
    {
561
        $revDates = $this->getPairData();
562
        return isset($revDates['last']) ? new DateTime($revDates['last']) : false;
563
    }
564
565
    /**
566
     * Get the number of days between the first and last edits.
567
     * If there's only one edit, this is counted as one day.
568
     * @return int
569
     */
570
    public function getDays()
571
    {
572
        $first = $this->datetimeFirstRevision();
573
        $last = $this->datetimeLastRevision();
574
        if ($first === false || $last === false) {
575
            return 0;
576
        }
577
        $days = $last->diff($first)->days;
578
        return $days > 0 ? $days : 1;
579
    }
580
581
    /**
582
     * Get the total number of files uploaded (including those now deleted).
583
     * @return int
584
     */
585
    public function countFilesUploaded()
586
    {
587
        $logCounts = $this->getLogCounts();
588
        return $logCounts['upload-upload'] ?: 0;
589
    }
590
591
    /**
592
     * Get the total number of files uploaded to Commons (including those now deleted).
593
     * This is only applicable for WMF labs installations.
594
     * @return int
595
     */
596
    public function countFilesUploadedCommons()
597
    {
598
        $logCounts = $this->getLogCounts();
599
        return $logCounts['files_uploaded_commons'] ?: 0;
600
    }
601
602
    /**
603
     * Get the total number of revisions the user has sent thanks for.
604
     * @return int
605
     */
606
    public function thanks()
607
    {
608
        $logCounts = $this->getLogCounts();
609
        return $logCounts['thanks-thank'] ?: 0;
610
    }
611
612
    /**
613
     * Get the total number of approvals
614
     * @return int
615
     */
616
    public function approvals()
617
    {
618
        $logCounts = $this->getLogCounts();
619
        $total = $logCounts['review-approve'] +
620
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
621
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
622
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
623
        return $total;
624
    }
625
626
    /**
627
     * Get the total number of patrols performed by the user.
628
     * @return int
629
     */
630
    public function patrols()
631
    {
632
        $logCounts = $this->getLogCounts();
633
        return $logCounts['patrol-patrol'] ?: 0;
634
    }
635
636
    /**
637
     * Get the total number of accounts created by the user.
638
     * @return int
639
     */
640
    public function accountsCreated()
641
    {
642
        $logCounts = $this->getLogCounts();
643
        $create2 = $logCounts['newusers-create2'] ?: 0;
644
        $byemail = $logCounts['newusers-byemail'] ?: 0;
645
        return $create2 + $byemail;
646
    }
647
648
    /**
649
     * Get the given user's total edit counts per namespace.
650
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
651
     */
652
    public function namespaceTotals()
653
    {
654
        if ($this->namespaceTotals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaceTotals of type integer[] 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...
655
            return $this->namespaceTotals;
656
        }
657
        $counts = $this->getRepository()->getNamespaceTotals($this->project, $this->user);
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getNamespaceTotals() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
658
        arsort($counts);
659
        $this->namespaceTotals = $counts;
660
        return $counts;
661
    }
662
663
    /**
664
     * Get a summary of the times of day and the days of the week that the user has edited.
665
     * @return string[]
666
     */
667
    public function timeCard()
668
    {
669
        if ($this->timeCardData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->timeCardData of type 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...
670
            return $this->timeCardData;
671
        }
672
        $totals = $this->getRepository()->getTimeCard($this->project, $this->user);
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getTimeCard() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
673
        $this->timeCardData = $totals;
674
        return $totals;
675
    }
676
677
    /**
678
     * Get the total numbers of edits per month.
679
     * @param null|DateTime [$currentTime] - *USED ONLY FOR UNIT TESTING*
680
     *   so we can mock the current DateTime.
681
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
682
     *   the latter keyed by namespace, year and then month.
683
     */
684
    public function monthCounts($currentTime = null)
685
    {
686
        if (isset($this->monthCounts)) {
687
            return $this->monthCounts;
688
        }
689
690
        // Set to current month if we're not unit-testing
691
        if (!($currentTime instanceof DateTime)) {
692
            $currentTime = new DateTime('last day of this month');
693
        }
694
695
        $totals = $this->getRepository()->getMonthCounts($this->project, $this->user);
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getMonthCounts() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
696
        $out = [
697
            'yearLabels' => [],  // labels for years
698
            'monthLabels' => [], // labels for months
699
            'totals' => [], // actual totals, grouped by namespace, year and then month
700
        ];
701
702
        /** @var DateTime Keep track of the date of their first edit. */
703
        $firstEdit = new DateTime();
704
705
        // Loop through the database results and fill in the values
706
        //   for the months that we have data for.
707
        foreach ($totals as $total) {
708
            // Keep track of first edit
709
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
710
            if ($date < $firstEdit) {
711
                $firstEdit = $date;
712
            }
713
714
            // Collate the counts by namespace, and then year and month.
715
            $ns = $total['page_namespace'];
716
            if (!isset($out['totals'][$ns])) {
717
                $out['totals'][$ns] = [];
718
            }
719
720
            // Start array for this year if not already present.
721
            if (!isset($out['totals'][$ns][$total['year']])) {
722
                $out['totals'][$ns][$total['year']] = [];
723
            }
724
725
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
726
        }
727
728
        $dateRange = new DatePeriod(
729
            $firstEdit,
730
            new DateInterval('P1M'),
731
            $currentTime->modify('first day of this month')
732
        );
733
734
        foreach ($dateRange as $monthObj) {
735
            $year = (int) $monthObj->format('Y');
736
            $month = (int) $monthObj->format('n');
737
738
            // Fill in labels
739
            $out['monthLabels'][] = $monthObj->format('Y-m');
740
            if (!in_array($year, $out['yearLabels'])) {
741
                $out['yearLabels'][] = $year;
742
            }
743
744
            foreach (array_keys($out['totals']) as $nsId) {
745
                if (!isset($out['totals'][$nsId][$year])) {
746
                    $out['totals'][$nsId][$year] = [];
747
                }
748
749
                if (!isset($out['totals'][$nsId][$year][$month])) {
750
                    $out['totals'][$nsId][$year][$month] = 0;
751
                }
752
            }
753
        }
754
755
        // One more set of loops to sort by year/month
756
        foreach (array_keys($out['totals']) as $nsId) {
757
            ksort($out['totals'][$nsId]);
758
759
            foreach ($out['totals'][$nsId] as &$yearData) {
760
                ksort($yearData);
761
            }
762
        }
763
764
        // Finally, sort the namespaces
765
        ksort($out['totals']);
766
767
        $this->monthCounts = $out;
0 ignored issues
show
Documentation Bug introduced by
It seems like $out of type array<string,array,{"yea...ray","totals":"array"}> is incompatible with the declared type array<integer,*> of property $monthCounts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
768
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $out; (array<string,array>) is incompatible with the return type documented by Xtools\EditCounter::monthCounts of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
769
    }
770
771
    /**
772
     * Get the total numbers of edits per year.
773
     * @param null|DateTime [$currentTime] - *USED ONLY FOR UNIT TESTING*
774
     *   so we can mock the current DateTime.
775
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter
776
     *   keyed by namespace then year.
777
     */
778
    public function yearCounts($currentTime = null)
779
    {
780
        if (isset($this->yearCounts)) {
781
            return $this->yearCounts;
782
        }
783
784
        $out = $this->monthCounts($currentTime);
785
786
        foreach ($out['totals'] as $nsId => $years) {
787
            foreach ($years as $year => $months) {
788
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
789
            }
790
        }
791
792
        $this->yearCounts = $out;
793
        return $out;
794
    }
795
796
    /**
797
     * Get the total edit counts for the top n projects of this user.
798
     * @param int $numProjects
799
     * @return mixed[] Each element has 'total' and 'project' keys.
800
     */
801
    public function globalEditCountsTopN($numProjects = 10)
802
    {
803
        // Get counts.
804
        $editCounts = $this->globalEditCounts(true);
805
        // Truncate, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
806
        return array_slice($editCounts, 0, $numProjects);
807
    }
808
809
    /**
810
     * Get the total number of edits excluding the top n.
811
     * @param int $numProjects
812
     * @return int
813
     */
814
    public function globalEditCountWithoutTopN($numProjects = 10)
815
    {
816
        $editCounts = $this->globalEditCounts(true);
817
        $bottomM = array_slice($editCounts, $numProjects);
818
        $total = 0;
819
        foreach ($bottomM as $editCount) {
820
            $total += $editCount['total'];
821
        }
822
        return $total;
823
    }
824
825
    /**
826
     * Get the grand total of all edits on all projects.
827
     * @return int
828
     */
829
    public function globalEditCount()
830
    {
831
        $total = 0;
832
        foreach ($this->globalEditCounts() as $editCount) {
833
            $total += $editCount['total'];
834
        }
835
        return $total;
836
    }
837
838
    /**
839
     * Get the total revision counts for all projects for this user.
840
     * @param bool $sorted Whether to sort the list by total, or not.
841
     * @return mixed[] Each element has 'total' and 'project' keys.
842
     */
843
    public function globalEditCounts($sorted = false)
844
    {
845
        if (empty($this->globalEditCounts)) {
846
            $this->globalEditCounts = $this->getRepository()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method globalEditCounts() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
847
                ->globalEditCounts($this->user, $this->project);
848
        }
849
850
        if ($sorted) {
851
            // Sort.
852
            uasort($this->globalEditCounts, function ($a, $b) {
853
                return $b['total'] - $a['total'];
854
            });
855
        }
856
857
        return $this->globalEditCounts;
858
    }
859
860
    /**
861
     * Get the most recent n revisions across all projects.
862
     * @param int $max The maximum number of revisions to return.
863
     * @return Edit[]
864
     */
865
    public function globalEdits($max)
866
    {
867
        // Collect all projects with any edits.
868
        $projects = [];
869
        foreach ($this->globalEditCounts() as $editCount) {
870
            // Don't query revisions if there aren't any.
871
            if ($editCount['total'] == 0) {
872
                continue;
873
            }
874
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
875
        }
876
877
        // Get all revisions for those projects.
878
        $globalRevisionsData = $this->getRepository()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getRevisions() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository, Xtools\PagesRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
879
            ->getRevisions($projects, $this->user, $max);
880
        $globalEdits = [];
881
        foreach ($globalRevisionsData as $revision) {
882
            /** @var Project $project */
883
            $project = $projects[$revision['project_name']];
884
            $nsName = '';
885
            if ($revision['page_namespace']) {
886
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
887
            }
888
            $page = $project->getRepository()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getPage() does only exist in the following sub-classes of Xtools\Repository: Xtools\ProjectRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
889
                ->getPage($project, $nsName . ':' . $revision['page_title']);
890
            $edit = new Edit($page, $revision);
891
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
892
        }
893
894
        // Sort and prune, before adding more.
895
        krsort($globalEdits);
896
        $globalEdits = array_slice($globalEdits, 0, $max);
897
        return $globalEdits;
898
    }
899
900
    /**
901
     * Get average edit size, and number of large and small edits.
902
     * @return int[]
903
     */
904
    public function getEditSizeData()
905
    {
906
        if (!is_array($this->editSizeData)) {
907
            $this->editSizeData = $this->getRepository()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getEditSizeData() does only exist in the following sub-classes of Xtools\Repository: Xtools\EditCounterRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
908
                ->getEditSizeData($this->project, $this->user);
909
        }
910
        return $this->editSizeData;
911
    }
912
913
    /**
914
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
915
     * This is used to ensure percentages of small and large edits are computed properly.
916
     * @return int
917
     */
918
    public function countLast5000()
919
    {
920
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
921
    }
922
923
    /**
924
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
925
     * @return int
926
     */
927
    public function countSmallEdits()
928
    {
929
        $editSizeData = $this->getEditSizeData();
930
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
931
    }
932
933
    /**
934
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
935
     * @return int
936
     */
937
    public function countLargeEdits()
938
    {
939
        $editSizeData = $this->getEditSizeData();
940
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
941
    }
942
943
    /**
944
     * Get the average size of the user's past 5000 edits.
945
     * @return float Size in bytes.
946
     */
947
    public function averageEditSize()
948
    {
949
        $editSizeData = $this->getEditSizeData();
950
        if (isset($editSizeData['average_size'])) {
951
            return round($editSizeData['average_size'], 3);
952
        } else {
953
            return 0;
954
        }
955
    }
956
}
957