Completed
Push — master ( dc7aae...2784d5 )
by MusikAnimal
02:59
created

EditCounter::thanks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
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
13
/**
14
 * An EditCounter provides statistics about a user's edits on a project.
15
 */
16
class EditCounter extends Model
17
{
18
19
    /** @var Project The project. */
20
    protected $project;
21
22
    /** @var User The user. */
23
    protected $user;
24
25
    /** @var int[] Revision and page counts etc. */
26
    protected $pairData;
27
28
    /** @var string[] The start and end dates of revisions. */
29
    protected $revisionDates;
30
31
    /** @var int[] The total page counts. */
32
    protected $pageCounts;
33
34
    /** @var int[] The lot totals. */
35
    protected $logCounts;
36
37
    /** @var mixed[] Total numbers of edits per month */
38
    protected $monthCounts;
39
40
    /** @var mixed[] Total numbers of edits per year */
41
    protected $yearCounts;
42
43
    /** @var int[] Keys are project DB names. */
44
    protected $globalEditCounts;
45
46
    /** @var array Block data, with keys 'set' and 'received'. */
47
    protected $blocks;
48
49
    /** @var int Number of semi-automated edits */
50
    protected $autoEditCount;
51
52
    /** @var string[] Data needed for time card chart */
53
    protected $timeCardData;
54
55
    /**
56
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
57
     * @var string[] As returned by the DB, unconverted to int or float
58
     */
59
    protected $editSizeData;
60
61
    /**
62
     * Duration of the longest block in days; -1 if indefinite,
63
     *   or false if could not be parsed from log params
64
     * @var int|bool
65
     */
66
    protected $longestBlockDays;
67
68
    /**
69
     * EditCounter constructor.
70
     * @param Project $project The base project to count edits
71
     * @param User $user
72
     */
73
    public function __construct(Project $project, User $user)
74
    {
75
        $this->project = $project;
76
        $this->user = $user;
77
    }
78
79
    /**
80
     * Get revision and page counts etc.
81
     * @return int[]
82
     */
83
    protected function getPairData()
84
    {
85
        if (! is_array($this->pairData)) {
86
            $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...
87
                ->getPairData($this->project, $this->user);
88
        }
89
        return $this->pairData;
90
    }
91
92
    /**
93
     * Get revision dates.
94
     * @return int[]
95
     */
96
    protected function getLogCounts()
97
    {
98
        if (! is_array($this->logCounts)) {
99
            $this->logCounts = $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 getLogCounts() 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...
100
                ->getLogCounts($this->project, $this->user);
101
        }
102
        return $this->logCounts;
103
    }
104
105
    /**
106
     * Get block data.
107
     * @param string $type Either 'set' or 'received'.
108
     * @return array
109
     */
110
    protected function getBlocks($type)
111
    {
112
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
113
            return $this->blocks[$type];
114
        }
115
        $method = "getBlocks".ucfirst($type);
116
        $blocks = $this->getRepository()->$method($this->project, $this->user);
117
        $this->blocks[$type] = $blocks;
118
        return $this->blocks[$type];
119
    }
120
121
    /**
122
     * Get the total number of currently-live revisions.
123
     * @return int
124
     */
125
    public function countLiveRevisions()
126
    {
127
        $revCounts = $this->getPairData();
128
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
129
    }
130
131
    /**
132
     * Get the total number of the user's revisions that have been deleted.
133
     * @return int
134
     */
135
    public function countDeletedRevisions()
136
    {
137
        $revCounts = $this->getPairData();
138
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
139
    }
140
141
    /**
142
     * Get the total edit count (live + deleted).
143
     * @return int
144
     */
145
    public function countAllRevisions()
146
    {
147
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
148
    }
149
150
    /**
151
     * Get the total number of revisions with comments.
152
     * @return int
153
     */
154
    public function countRevisionsWithComments()
155
    {
156
        $revCounts = $this->getPairData();
157
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
158
    }
159
160
    /**
161
     * Get the total number of revisions without comments.
162
     * @return int
163
     */
164
    public function countRevisionsWithoutComments()
165
    {
166
        return $this->countAllRevisions() - $this->countRevisionsWithComments();
167
    }
168
169
    /**
170
     * Get the total number of revisions marked as 'minor' by the user.
171
     * @return int
172
     */
173
    public function countMinorRevisions()
174
    {
175
        $revCounts = $this->getPairData();
176
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
177
    }
178
179
    /**
180
     * Get the total number of non-deleted pages edited by the user.
181
     * @return int
182
     */
183
    public function countLivePagesEdited()
184
    {
185
        $pageCounts = $this->getPairData();
186
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
187
    }
188
189
    /**
190
     * Get the total number of deleted pages ever edited by the user.
191
     * @return int
192
     */
193
    public function countDeletedPagesEdited()
194
    {
195
        $pageCounts = $this->getPairData();
196
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
197
    }
198
199
    /**
200
     * Get the total number of pages ever edited by this user (both live and deleted).
201
     * @return int
202
     */
203
    public function countAllPagesEdited()
204
    {
205
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
206
    }
207
208
    /**
209
     * Get the total number of pages (both still live and those that have been deleted) created
210
     * by the user.
211
     * @return int
212
     */
213
    public function countPagesCreated()
214
    {
215
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
216
    }
217
218
    /**
219
     * Get the total number of pages created by the user, that have not been deleted.
220
     * @return int
221
     */
222
    public function countCreatedPagesLive()
223
    {
224
        $pageCounts = $this->getPairData();
225
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
226
    }
227
228
    /**
229
     * Get the total number of pages created by the user, that have since been deleted.
230
     * @return int
231
     */
232
    public function countPagesCreatedDeleted()
233
    {
234
        $pageCounts = $this->getPairData();
235
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
236
    }
237
238
    /**
239
     * Get the total number of pages that have been deleted by the user.
240
     * @return int
241
     */
242
    public function countPagesDeleted()
243
    {
244
        $logCounts = $this->getLogCounts();
245
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
246
    }
247
248
    /**
249
     * Get the total number of pages moved by the user.
250
     * @return int
251
     */
252
    public function countPagesMoved()
253
    {
254
        $logCounts = $this->getLogCounts();
255
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
256
    }
257
258
    /**
259
     * Get the total number of times the user has blocked a user.
260
     * @return int
261
     */
262
    public function countBlocksSet()
263
    {
264
        $logCounts = $this->getLogCounts();
265
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
266
        return $reBlock;
267
    }
268
269
    /**
270
     * Get the total number of times the user has re-blocked a user.
271
     * @return int
272
     */
273
    public function countReblocksSet()
274
    {
275
        $logCounts = $this->getLogCounts();
276
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
277
        return $reBlock;
278
    }
279
280
    /**
281
     * Get the total number of times the user has unblocked a user.
282
     * @return int
283
     */
284
    public function countUnblocksSet()
285
    {
286
        $logCounts = $this->getLogCounts();
287
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
288
    }
289
290
    /**
291
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
292
     * @return int
293
     */
294
    public function countBlocksLifted()
295
    {
296
        $logCounts = $this->getLogCounts();
297
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
298
    }
299
300
    /**
301
     * Get the total number of times the user has been blocked.
302
     * @return int
303
     */
304
    public function countBlocksReceived()
305
    {
306
        $blocks = $this->getBlocks('received');
307
        return count($blocks);
308
    }
309
310
    /**
311
     * Get the length of the longest block the user received.
312
     * @return int|bool Number of days or false if it could not be determined.
313
     *                  If the longest duration is indefinite, -1 is returned.
314
     */
315
    public function getLongestBlockDays()
316
    {
317
        if (isset($this->longestBlockDays)) {
318
            return $this->longestBlockDays;
319
        }
320
321
        $blocks = $this->getBlocks('received'); // FIXME: make sure this is only called once
322
        $this->longestBlockDays = false;
323
324
        foreach ($blocks as $block) {
325
            $timestamp = strtotime($block['log_timestamp']);
326
327
            // First check if the string is serialized, and if so parse it to get the block duration
328
            if (@unserialize($block['log_params']) !== false) {
329
                $parsedParams = unserialize($block['log_params']);
330
                $durationStr = $parsedParams['5::duration'];
331
            } else {
332
                // Old format, the duration in English + block options separated by new lines
333
                $durationStr = explode("\n", $block['log_params'])[0];
334
            }
335
336
            if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
337
                return -1;
338
            }
339
340
            // Try block just in case there are older, unpredictable formats
341
            try {
342
                $expiry = strtotime($durationStr, $timestamp);
343
                $duration = ($expiry - $timestamp) / (60 * 60 * 24);
344
345
                if (!$duration || $duration > $this->longestBlockDays) {
346
                    $this->longestBlockDays = $duration;
347
                }
348
            } catch (Exception $error) {
349
                // do nothing, leaving the longest block at false
350
            }
351
        }
352
353
        return $this->longestBlockDays;
354
    }
355
356
    /**
357
     * Get the total number of pages protected by the user.
358
     * @return int
359
     */
360
    public function countPagesProtected()
361
    {
362
        $logCounts = $this->getLogCounts();
363
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
364
    }
365
366
    /**
367
     * Get the total number of pages reprotected by the user.
368
     * @return int
369
     */
370
    public function countPagesReprotected()
371
    {
372
        $logCounts = $this->getLogCounts();
373
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
374
    }
375
376
    /**
377
     * Get the total number of pages unprotected by the user.
378
     * @return int
379
     */
380
    public function countPagesUnprotected()
381
    {
382
        $logCounts = $this->getLogCounts();
383
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
384
    }
385
386
    /**
387
     * Get the total number of edits deleted by the user.
388
     * @return int
389
     */
390
    public function countEditsDeleted()
391
    {
392
        $logCounts = $this->getLogCounts();
393
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
394
    }
395
396
    /**
397
     * Get the total number of pages restored by the user.
398
     * @return int
399
     */
400
    public function countPagesRestored()
401
    {
402
        $logCounts = $this->getLogCounts();
403
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
404
    }
405
406
    /**
407
     * Get the total number of times the user has modified the rights of a user.
408
     * @return int
409
     */
410
    public function countRightsModified()
411
    {
412
        $logCounts = $this->getLogCounts();
413
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
414
    }
415
416
    /**
417
     * Get the total number of pages imported by the user (through any import mechanism:
418
     * interwiki, or XML upload).
419
     * @return int
420
     */
421
    public function countPagesImported()
422
    {
423
        $logCounts = $this->getLogCounts();
424
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
425
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
426
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
427
        return $import + $interwiki + $upload;
428
    }
429
430
    /**
431
     * Get the average number of edits per page (including deleted revisions and pages).
432
     * @return float
433
     */
434
    public function averageRevisionsPerPage()
435
    {
436
        if ($this->countAllPagesEdited() == 0) {
437
            return 0;
438
        }
439
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
440
    }
441
442
    /**
443
     * Average number of edits made per day.
444
     * @return float
445
     */
446
    public function averageRevisionsPerDay()
447
    {
448
        if ($this->getDays() == 0) {
449
            return 0;
450
        }
451
        return round($this->countAllRevisions() / $this->getDays(), 3);
452
    }
453
454
    /**
455
     * Get the total number of edits made by the user with semi-automating tools.
456
     */
457
    public function countAutomatedEdits()
458
    {
459
        if ($this->autoEditCount) {
460
            return $this->autoEditCount;
461
        }
462
        $this->autoEditCount = $this->user->countAutomatedEdits($this->project);
463
        return $this->autoEditCount;
464
    }
465
466
    /**
467
     * Get the count of (non-deleted) edits made in the given timeframe to now.
468
     * @param string $time One of 'day', 'week', 'month', or 'year'.
469
     * @return int The total number of live edits.
470
     */
471
    public function countRevisionsInLast($time)
472
    {
473
        $revCounts = $this->getPairData();
474
        return isset($revCounts[$time]) ? $revCounts[$time] : 0;
475
    }
476
477
    /**
478
     * Get the date and time of the user's first edit.
479
     * @return DateTime|bool The time of the first revision, or false.
480
     */
481
    public function datetimeFirstRevision()
482
    {
483
        $revDates = $this->getPairData();
484
        return isset($revDates['first']) ? new DateTime($revDates['first']) : false;
485
    }
486
487
    /**
488
     * Get the date and time of the user's first edit.
489
     * @return DateTime|bool The time of the last revision, or false.
490
     */
491
    public function datetimeLastRevision()
492
    {
493
        $revDates = $this->getPairData();
494
        return isset($revDates['last']) ? new DateTime($revDates['last']) : false;
495
    }
496
497
    /**
498
     * Get the number of days between the first and last edits.
499
     * If there's only one edit, this is counted as one day.
500
     * @return int
501
     */
502
    public function getDays()
503
    {
504
        $first = $this->datetimeFirstRevision();
505
        $last = $this->datetimeLastRevision();
506
        if ($first === false || $last === false) {
507
            return 0;
508
        }
509
        $days = $last->diff($first)->days;
510
        return $days > 0 ? $days : 1;
511
    }
512
513
    /**
514
     * Get the total number of files uploaded (including those now deleted).
515
     * @return int
516
     */
517
    public function countFilesUploaded()
518
    {
519
        $logCounts = $this->getLogCounts();
520
        return $logCounts['upload-upload'] ?: 0;
521
    }
522
523
    /**
524
     * Get the total number of files uploaded to Commons (including those now deleted).
525
     * This is only applicable for WMF labs installations.
526
     * @return int
527
     */
528
    public function countFilesUploadedCommons()
529
    {
530
        $logCounts = $this->getLogCounts();
531
        return $logCounts['files_uploaded_commons'] ?: 0;
532
    }
533
534
    /**
535
     * Get the total number of revisions the user has sent thanks for.
536
     * @return int
537
     */
538
    public function thanks()
539
    {
540
        $logCounts = $this->getLogCounts();
541
        return $logCounts['thanks-thank'] ?: 0;
542
    }
543
544
    /**
545
     * Get the total number of approvals
546
     * @return int
547
     */
548
    public function approvals()
549
    {
550
        $logCounts = $this->getLogCounts();
551
        $total = $logCounts['review-approve'] +
552
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
553
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
554
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
555
        return $total;
556
    }
557
558
    /**
559
     * Get the total number of patrols performed by the user.
560
     * @return int
561
     */
562
    public function patrols()
563
    {
564
        $logCounts = $this->getLogCounts();
565
        return $logCounts['patrol-patrol'] ?: 0;
566
    }
567
568
    /**
569
     * Get the total number of accounts created by the user.
570
     * @return int
571
     */
572
    public function accountsCreated()
573
    {
574
        $logCounts = $this->getLogCounts();
575
        $create2 = $logCounts['newusers-create2'] ?: 0;
576
        $byemail = $logCounts['newusers-byemail'] ?: 0;
577
        return $create2 + $byemail;
578
    }
579
580
    /**
581
     * Get the given user's total edit counts per namespace.
582
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
583
     */
584
    public function namespaceTotals()
585
    {
586
        $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...
587
        arsort($counts);
588
        return $counts;
589
    }
590
591
    /**
592
     * Get a summary of the times of day and the days of the week that the user has edited.
593
     * @return string[]
594
     */
595
    public function timeCard()
596
    {
597
        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...
598
            return $this->timeCardData;
599
        }
600
        $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...
601
        $this->timeCardData = $totals;
602
        return $totals;
603
    }
604
605
    /**
606
     * Get the total numbers of edits per month.
607
     * @param null|DateTime [$currentTime] - *USED ONLY FOR UNIT TESTING*
608
     *   so we can mock the current DateTime.
609
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
610
     *   the latter keyed by namespace, year and then month.
611
     */
612
    public function monthCounts($currentTime = null)
613
    {
614
        if (isset($this->monthCounts)) {
615
            return $this->monthCounts;
616
        }
617
618
        // Set to current month if we're not unit-testing
619
        if (!($currentTime instanceof DateTime)) {
620
            $currentTime = new DateTime('last day of this month');
621
        }
622
623
        $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...
624
        $out = [
625
            'yearLabels' => [],  // labels for years
626
            'monthLabels' => [], // labels for months
627
            'totals' => [], // actual totals, grouped by namespace, year and then month
628
        ];
629
630
        // This may be null for really old accounts, which we'll account for!
631
        $registrationDate = $this->user->getRegistrationDate($this->project);
632
        // If not empty, we know the first month/year to show.
633
        if (!empty($registrationDate)) {
634
            $minYear = (int) $registrationDate->format('Y');
635
            $minMonth = $registrationDate->format('m');
636
        } else {
637
            $minYear = (int) $currentTime->format('Y');
638
639
            // For users without a registration date, we'll just
640
            //   use January as the first month.
641
            $minMonth = 1;
642
        }
643
644
        // Loop through the database results and fill in the values
645
        //   for the months that we have data for.
646
        foreach ($totals as $total) {
647
            // Figure out the first year they made an edit if we don't have a registration date.
648
            if (empty($registrationDate)) {
649
                $minYear = (int) min($minYear, $total['year']);
650
            }
651
652
            // Collate the counts by namespace, and then year and month.
653
            $ns = $total['page_namespace'];
654
            if (!isset($out['totals'][$ns])) {
655
                $out['totals'][$ns] = [];
656
            }
657
658
            // Start array for this year if not already present.
659
            if (!isset($out['totals'][$ns][$total['year']])) {
660
                $out['totals'][$ns][$total['year']] = [];
661
            }
662
663
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
664
        }
665
666
        $dateRange = new DatePeriod(
667
            new DateTime("$minYear-$minMonth-01"),
668
            new DateInterval('P1M'),
669
            $currentTime->modify('first day of this month')
670
        );
671
672
        foreach ($dateRange as $monthObj) {
673
            $year = (int) $monthObj->format('Y');
674
            $month = (int) $monthObj->format('n');
675
676
            // Fill in labels
677
            $out['monthLabels'][] = $monthObj->format('Y/m');
678
            if (!in_array($year, $out['yearLabels'])) {
679
                $out['yearLabels'][] = $year;
680
            }
681
682
            foreach (array_keys($out['totals']) as $nsId) {
683
                if (!isset($out['totals'][$nsId][$year])) {
684
                    $out['totals'][$nsId][$year] = [];
685
                }
686
687
                if (!isset($out['totals'][$nsId][$year][$month])) {
688
                    $out['totals'][$nsId][$year][$month] = 0;
689
                }
690
            }
691
        }
692
693
        // One more set of loops to sort by year/month
694
        foreach (array_keys($out['totals']) as $nsId) {
695
            ksort($out['totals'][$nsId]);
696
697
            foreach ($out['totals'][$nsId] as &$yearData) {
698
                ksort($yearData);
699
            }
700
        }
701
702
        // Finally, sort the namespaces
703
        ksort($out['totals']);
704
705
        $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...
706
        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...
707
    }
708
709
    /**
710
     * Get the total numbers of edits per year.
711
     * @param null|DateTime [$currentTime] - *USED ONLY FOR UNIT TESTING*
712
     *   so we can mock the current DateTime.
713
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter
714
     *   keyed by namespace then year.
715
     */
716
    public function yearCounts($currentTime = null)
717
    {
718
        $out = $this->monthCounts($currentTime);
719
720
        foreach ($out['totals'] as $nsId => $years) {
721
            foreach ($years as $year => $months) {
722
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
723
            }
724
        }
725
726
        return $out;
727
    }
728
729
    /**
730
     * Get the total edit counts for the top n projects of this user.
731
     * @param int $numProjects
732
     * @return mixed[] Each element has 'total' and 'project' keys.
733
     */
734
    public function globalEditCountsTopN($numProjects = 10)
735
    {
736
        // Get counts.
737
        $editCounts = $this->globalEditCounts(true);
738
        // 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...
739
        return array_slice($editCounts, 0, $numProjects);
740
    }
741
742
    /**
743
     * Get the total number of edits excluding the top n.
744
     * @param int $numProjects
745
     * @return int
746
     */
747
    public function globalEditCountWithoutTopN($numProjects = 10)
748
    {
749
        $editCounts = $this->globalEditCounts(true);
750
        $bottomM = array_slice($editCounts, $numProjects);
751
        $total = 0;
752
        foreach ($bottomM as $editCount) {
753
            $total += $editCount['total'];
754
        }
755
        return $total;
756
    }
757
758
    /**
759
     * Get the grand total of all edits on all projects.
760
     * @return int
761
     */
762
    public function globalEditCount()
763
    {
764
        $total = 0;
765
        foreach ($this->globalEditCounts() as $editCount) {
766
            $total += $editCount['total'];
767
        }
768
        return $total;
769
    }
770
771
    /**
772
     * Get the total revision counts for all projects for this user.
773
     * @param bool $sorted Whether to sort the list by total, or not.
774
     * @return mixed[] Each element has 'total' and 'project' keys.
775
     */
776
    public function globalEditCounts($sorted = false)
777
    {
778
        if (empty($this->globalEditCounts)) {
779
            $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...
780
                ->globalEditCounts($this->user, $this->project);
781
            if ($sorted) {
782
                // Sort.
783
                uasort($this->globalEditCounts, function ($a, $b) {
784
                    return $b['total'] - $a['total'];
785
                });
786
            }
787
        }
788
        return $this->globalEditCounts;
789
    }
790
791
    /**
792
     * Get the most recent n revisions across all projects.
793
     * @param int $max The maximum number of revisions to return.
794
     * @return Edit[]
795
     */
796
    public function globalEdits($max)
797
    {
798
        // Collect all projects with any edits.
799
        $projects = [];
800
        foreach ($this->globalEditCounts() as $editCount) {
801
            // Don't query revisions if there aren't any.
802
            if ($editCount['total'] == 0) {
803
                continue;
804
            }
805
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
806
        }
807
808
        // Get all revisions for those projects.
809
        $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...
810
            ->getRevisions($projects, $this->user, $max);
811
        $globalEdits = [];
812
        foreach ($globalRevisionsData as $revision) {
813
            /** @var Project $project */
814
            $project = $projects[$revision['project_name']];
815
            $nsName = '';
816
            if ($revision['page_namespace']) {
817
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
818
            }
819
            $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...
820
                ->getPage($project, $nsName . ':' . $revision['page_title']);
821
            $edit = new Edit($page, $revision);
822
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
823
        }
824
825
        // Sort and prune, before adding more.
826
        krsort($globalEdits);
827
        $globalEdits = array_slice($globalEdits, 0, $max);
828
        return $globalEdits;
829
    }
830
831
    /**
832
     * Get average edit size, and number of large and small edits.
833
     * @return int[]
834
     */
835
    protected function getEditSizeData()
836
    {
837
        if (! is_array($this->editSizeData)) {
838
            $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...
839
                ->getEditSizeData($this->project, $this->user);
840
        }
841
        return $this->editSizeData;
842
    }
843
844
    /**
845
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
846
     * This is used to ensure percentages of small and large edits are computed properly.
847
     * @return int
848
     */
849
    public function countLast5000()
850
    {
851
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
852
    }
853
854
    /**
855
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
856
     * @return int
857
     */
858
    public function countSmallEdits()
859
    {
860
        $editSizeData = $this->getEditSizeData();
861
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
862
    }
863
864
    /**
865
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
866
     * @return int
867
     */
868
    public function countLargeEdits()
869
    {
870
        $editSizeData = $this->getEditSizeData();
871
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
872
    }
873
874
    /**
875
     * Get the average size of the user's past 5000 edits.
876
     * @return float Size in bytes.
877
     */
878
    public function averageEditSize()
879
    {
880
        $editSizeData = $this->getEditSizeData();
881
        if (isset($editSizeData['average_size'])) {
882
            return round($editSizeData['average_size'], 3);
883
        } else {
884
            return 0;
885
        }
886
    }
887
}
888