Completed
Push — master ( b93638...15ffeb )
by MusikAnimal
02:17
created

EditCounter::getLongestBlockDays()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 40
rs 5.3846
cc 8
eloc 21
nc 10
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 seconds; -1 if indefinite,
68
     *   or false if could not be parsed from log params
69
     * @var int|bool
70
     */
71
    protected $longestBlockSeconds;
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" => "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', 'received'
176
     * @param bool $blocksOnly Whether to include only blocks, and not reblocks and unblocks.
177
     * @return array
178
     */
179
    protected function getBlocks($type, $blocksOnly = true)
180
    {
181
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
182
            return $this->blocks[$type];
183
        }
184
        $method = "getBlocks".ucfirst($type);
185
        $blocks = $this->getRepository()->$method($this->project, $this->user);
186
        $this->blocks[$type] = $blocks;
187
188
        // Filter out unblocks unless requested.
189
        if ($blocksOnly) {
190
            $blocks = array_filter($blocks, function ($block) {
191
                return $block['log_action'] === 'block';
192
            });
193
        }
194
195
        return $blocks;
196
    }
197
198
    /**
199
     * Get the total number of currently-live revisions.
200
     * @return int
201
     */
202
    public function countLiveRevisions()
203
    {
204
        $revCounts = $this->getPairData();
205
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
206
    }
207
208
    /**
209
     * Get the total number of the user's revisions that have been deleted.
210
     * @return int
211
     */
212
    public function countDeletedRevisions()
213
    {
214
        $revCounts = $this->getPairData();
215
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
216
    }
217
218
    /**
219
     * Get the total edit count (live + deleted).
220
     * @return int
221
     */
222
    public function countAllRevisions()
223
    {
224
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
225
    }
226
227
    /**
228
     * Get the total number of live revisions with comments.
229
     * @return int
230
     */
231
    public function countRevisionsWithComments()
232
    {
233
        $revCounts = $this->getPairData();
234
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
235
    }
236
237
    /**
238
     * Get the total number of live revisions without comments.
239
     * @return int
240
     */
241
    public function countRevisionsWithoutComments()
242
    {
243
        return $this->countLiveRevisions() - $this->countRevisionsWithComments();
244
    }
245
246
    /**
247
     * Get the total number of revisions marked as 'minor' by the user.
248
     * @return int
249
     */
250
    public function countMinorRevisions()
251
    {
252
        $revCounts = $this->getPairData();
253
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
254
    }
255
256
    /**
257
     * Get the total number of non-deleted pages edited by the user.
258
     * @return int
259
     */
260
    public function countLivePagesEdited()
261
    {
262
        $pageCounts = $this->getPairData();
263
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
264
    }
265
266
    /**
267
     * Get the total number of deleted pages ever edited by the user.
268
     * @return int
269
     */
270
    public function countDeletedPagesEdited()
271
    {
272
        $pageCounts = $this->getPairData();
273
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
274
    }
275
276
    /**
277
     * Get the total number of pages ever edited by this user (both live and deleted).
278
     * @return int
279
     */
280
    public function countAllPagesEdited()
281
    {
282
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
283
    }
284
285
    /**
286
     * Get the total number of pages (both still live and those that have been deleted) created
287
     * by the user.
288
     * @return int
289
     */
290
    public function countPagesCreated()
291
    {
292
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
293
    }
294
295
    /**
296
     * Get the total number of pages created by the user, that have not been deleted.
297
     * @return int
298
     */
299
    public function countCreatedPagesLive()
300
    {
301
        $pageCounts = $this->getPairData();
302
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
303
    }
304
305
    /**
306
     * Get the total number of pages created by the user, that have since been deleted.
307
     * @return int
308
     */
309
    public function countPagesCreatedDeleted()
310
    {
311
        $pageCounts = $this->getPairData();
312
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
313
    }
314
315
    /**
316
     * Get the total number of pages that have been deleted by the user.
317
     * @return int
318
     */
319
    public function countPagesDeleted()
320
    {
321
        $logCounts = $this->getLogCounts();
322
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
323
    }
324
325
    /**
326
     * Get the total number of pages moved by the user.
327
     * @return int
328
     */
329
    public function countPagesMoved()
330
    {
331
        $logCounts = $this->getLogCounts();
332
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
333
    }
334
335
    /**
336
     * Get the total number of times the user has blocked a user.
337
     * @return int
338
     */
339
    public function countBlocksSet()
340
    {
341
        $logCounts = $this->getLogCounts();
342
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
343
        return $reBlock;
344
    }
345
346
    /**
347
     * Get the total number of times the user has re-blocked a user.
348
     * @return int
349
     */
350
    public function countReblocksSet()
351
    {
352
        $logCounts = $this->getLogCounts();
353
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
354
        return $reBlock;
355
    }
356
357
    /**
358
     * Get the total number of times the user has unblocked a user.
359
     * @return int
360
     */
361
    public function countUnblocksSet()
362
    {
363
        $logCounts = $this->getLogCounts();
364
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
365
    }
366
367
    /**
368
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
369
     * @return int
370
     */
371
    public function countBlocksLifted()
372
    {
373
        $logCounts = $this->getLogCounts();
374
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
375
    }
376
377
    /**
378
     * Get the total number of times the user has been blocked.
379
     * @return int
380
     */
381
    public function countBlocksReceived()
382
    {
383
        $blocks = $this->getBlocks('received');
384
        return count($blocks);
385
    }
386
387
    /**
388
     * Get the length of the longest block the user received, in seconds.
389
     * @return int Number of seconds or false if it could not be determined.
390
     *   If the user is blocked, the time since the block is returned. If the block is
391
     *   indefinite, -1 is returned. 0 if there was never a block.
392
     */
393
    public function getLongestBlockSeconds()
394
    {
395
        if (isset($this->longestBlockSeconds)) {
396
            return $this->longestBlockSeconds;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->longestBlockSeconds; of type integer|boolean adds the type boolean to the return on line 396 which is incompatible with the return type documented by Xtools\EditCounter::getLongestBlockSeconds of type integer.
Loading history...
397
        }
398
399
        $blocks = $this->getBlocks('received', false);
400
        $this->longestBlockSeconds = false;
401
402
        // If there was never a block, the longest was zero seconds.
403
        if (empty($blocks)) {
404
            return 0;
405
        }
406
407
        /**
408
         * Keep track of the last block so we can determine the duration
409
         * if the current block in the loop is an unblock.
410
         * @var int[] [
411
         *              Unix timestamp,
412
         *              Duration in seconds (-1 if indefinite)
413
         *            ]
414
         */
415
        $lastBlock = [null, null];
416
417
        foreach ($blocks as $index => $block) {
418
            list($timestamp, $duration) = $this->parseBlockLogEntry($block);
419
420
            if ($block['log_action'] === 'block') {
421
                // This is a new block, so first see if the duration of the last
422
                // block exceeded our longest duration. -1 duration means indefinite.
423
                if ($lastBlock[1] > $this->longestBlockSeconds || $lastBlock[1] === -1) {
424
                    $this->longestBlockSeconds = $lastBlock[1];
425
                }
426
427
                // Now set this as the last block.
428
                $lastBlock = [$timestamp, $duration];
429
            } elseif ($block['log_action'] === 'unblock') {
430
                // The last block was lifted. So the duration will be the time from when the
431
                // last block was set to the time of the unblock.
432
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
433
                if ($timeSinceLastBlock > $this->longestBlockSeconds) {
434
                    $this->longestBlockSeconds = $timeSinceLastBlock;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timeSinceLastBlock can also be of type double. However, the property $longestBlockSeconds is declared as type integer|boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
435
436
                    // Reset the last block, as it has now been accounted for.
437
                    $lastBlock = null;
438
                }
439
            } elseif ($block['log_action'] === 'reblock' && $lastBlock[1] !== -1) {
440
                // The last block was modified. So we will adjust $lastBlock to include
441
                // the difference of the duration of the new reblock, and time since the last block.
442
                // $lastBlock is left unchanged if its duration was indefinite.
443
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
444
                $lastBlock[1] = $timeSinceLastBlock + $duration;
445
            }
446
        }
447
448
        // If the last block was indefinite, we'll return that as the longest duration.
449
        if ($lastBlock[1] === -1) {
450
            return -1;
451
        }
452
453
        // Test if the last block is still active, and if so use the expiry as the duration.
454
        $lastBlockExpiry = $lastBlock[0] + $lastBlock[1];
455
        if ($lastBlockExpiry > time() && $lastBlockExpiry > $this->longestBlockSeconds) {
456
            $this->longestBlockSeconds = $lastBlock[1];
457
        // Otherwise, test if the duration of the last block is now the longest overall.
458
        } elseif ($lastBlock[1] > $this->longestBlockSeconds) {
459
            $this->longestBlockSeconds = $lastBlock[1];
460
        }
461
462
        return $this->longestBlockSeconds;
463
    }
464
465
    /**
466
     * Given a block log entry from the database, get the timestamp and duration in seconds.
467
     * @param  mixed[] $block Block log entry as fetched via self::getBlocks()
468
     * @return int[] [
469
     *                 Unix timestamp,
470
     *                 Duration in seconds (-1 if indefinite, null if unparsable or unblock)
471
     *               ]
472
     */
473
    public function parseBlockLogEntry($block)
474
    {
475
        $timestamp = strtotime($block['log_timestamp']);
476
        $duration = null;
477
478
        // First check if the string is serialized, and if so parse it to get the block duration.
479
        if (@unserialize($block['log_params']) !== false) {
480
            $parsedParams = unserialize($block['log_params']);
481
            $durationStr = isset($parsedParams['5::duration']) ? $parsedParams['5::duration'] : null;
482
        } else {
483
            // Old format, the duration in English + block options separated by new lines.
484
            $durationStr = explode("\n", $block['log_params'])[0];
485
        }
486
487
        if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
488
            $duration = -1;
489
        }
490
491
        // Make sure $durationStr is valid just in case it is in an older, unpredictable format.
492
        // If invalid, $duration is left as null.
493
        if (strtotime($durationStr)) {
494
            $expiry = strtotime($durationStr, $timestamp);
495
            $duration = $expiry - $timestamp;
496
        }
497
498
        return [$timestamp, $duration];
499
    }
500
501
    /**
502
     * Get the total number of pages protected by the user.
503
     * @return int
504
     */
505
    public function countPagesProtected()
506
    {
507
        $logCounts = $this->getLogCounts();
508
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
509
    }
510
511
    /**
512
     * Get the total number of pages reprotected by the user.
513
     * @return int
514
     */
515
    public function countPagesReprotected()
516
    {
517
        $logCounts = $this->getLogCounts();
518
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
519
    }
520
521
    /**
522
     * Get the total number of pages unprotected by the user.
523
     * @return int
524
     */
525
    public function countPagesUnprotected()
526
    {
527
        $logCounts = $this->getLogCounts();
528
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
529
    }
530
531
    /**
532
     * Get the total number of edits deleted by the user.
533
     * @return int
534
     */
535
    public function countEditsDeleted()
536
    {
537
        $logCounts = $this->getLogCounts();
538
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
539
    }
540
541
    /**
542
     * Get the total number of pages restored by the user.
543
     * @return int
544
     */
545
    public function countPagesRestored()
546
    {
547
        $logCounts = $this->getLogCounts();
548
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
549
    }
550
551
    /**
552
     * Get the total number of times the user has modified the rights of a user.
553
     * @return int
554
     */
555
    public function countRightsModified()
556
    {
557
        $logCounts = $this->getLogCounts();
558
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
559
    }
560
561
    /**
562
     * Get the total number of pages imported by the user (through any import mechanism:
563
     * interwiki, or XML upload).
564
     * @return int
565
     */
566
    public function countPagesImported()
567
    {
568
        $logCounts = $this->getLogCounts();
569
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
570
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
571
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
572
        return $import + $interwiki + $upload;
573
    }
574
575
    /**
576
     * Get the average number of edits per page (including deleted revisions and pages).
577
     * @return float
578
     */
579
    public function averageRevisionsPerPage()
580
    {
581
        if ($this->countAllPagesEdited() == 0) {
582
            return 0;
583
        }
584
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
585
    }
586
587
    /**
588
     * Average number of edits made per day.
589
     * @return float
590
     */
591
    public function averageRevisionsPerDay()
592
    {
593
        if ($this->getDays() == 0) {
594
            return 0;
595
        }
596
        return round($this->countAllRevisions() / $this->getDays(), 3);
597
    }
598
599
    /**
600
     * Get the total number of edits made by the user with semi-automating tools.
601
     */
602
    public function countAutomatedEdits()
603
    {
604
        if ($this->autoEditCount) {
605
            return $this->autoEditCount;
606
        }
607
        $this->autoEditCount = $this->user->countAutomatedEdits($this->project);
608
        return $this->autoEditCount;
609
    }
610
611
    /**
612
     * Get the count of (non-deleted) edits made in the given timeframe to now.
613
     * @param string $time One of 'day', 'week', 'month', or 'year'.
614
     * @return int The total number of live edits.
615
     */
616
    public function countRevisionsInLast($time)
617
    {
618
        $revCounts = $this->getPairData();
619
        return isset($revCounts[$time]) ? $revCounts[$time] : 0;
620
    }
621
622
    /**
623
     * Get the date and time of the user's first edit.
624
     * @return DateTime|bool The time of the first revision, or false.
625
     */
626
    public function datetimeFirstRevision()
627
    {
628
        $revDates = $this->getPairData();
629
        return isset($revDates['first']) ? new DateTime($revDates['first']) : false;
630
    }
631
632
    /**
633
     * Get the date and time of the user's first edit.
634
     * @return DateTime|bool The time of the last revision, or false.
635
     */
636
    public function datetimeLastRevision()
637
    {
638
        $revDates = $this->getPairData();
639
        return isset($revDates['last']) ? new DateTime($revDates['last']) : false;
640
    }
641
642
    /**
643
     * Get the number of days between the first and last edits.
644
     * If there's only one edit, this is counted as one day.
645
     * @return int
646
     */
647
    public function getDays()
648
    {
649
        $first = $this->datetimeFirstRevision();
650
        $last = $this->datetimeLastRevision();
651
        if ($first === false || $last === false) {
652
            return 0;
653
        }
654
        $days = $last->diff($first)->days;
655
        return $days > 0 ? $days : 1;
656
    }
657
658
    /**
659
     * Get the total number of files uploaded (including those now deleted).
660
     * @return int
661
     */
662
    public function countFilesUploaded()
663
    {
664
        $logCounts = $this->getLogCounts();
665
        return $logCounts['upload-upload'] ?: 0;
666
    }
667
668
    /**
669
     * Get the total number of files uploaded to Commons (including those now deleted).
670
     * This is only applicable for WMF labs installations.
671
     * @return int
672
     */
673
    public function countFilesUploadedCommons()
674
    {
675
        $logCounts = $this->getLogCounts();
676
        return $logCounts['files_uploaded_commons'] ?: 0;
677
    }
678
679
    /**
680
     * Get the total number of revisions the user has sent thanks for.
681
     * @return int
682
     */
683
    public function thanks()
684
    {
685
        $logCounts = $this->getLogCounts();
686
        return $logCounts['thanks-thank'] ?: 0;
687
    }
688
689
    /**
690
     * Get the total number of approvals
691
     * @return int
692
     */
693
    public function approvals()
694
    {
695
        $logCounts = $this->getLogCounts();
696
        $total = $logCounts['review-approve'] +
697
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
698
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
699
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
700
        return $total;
701
    }
702
703
    /**
704
     * Get the total number of patrols performed by the user.
705
     * @return int
706
     */
707
    public function patrols()
708
    {
709
        $logCounts = $this->getLogCounts();
710
        return $logCounts['patrol-patrol'] ?: 0;
711
    }
712
713
    /**
714
     * Get the total number of accounts created by the user.
715
     * @return int
716
     */
717
    public function accountsCreated()
718
    {
719
        $logCounts = $this->getLogCounts();
720
        $create2 = $logCounts['newusers-create2'] ?: 0;
721
        $byemail = $logCounts['newusers-byemail'] ?: 0;
722
        return $create2 + $byemail;
723
    }
724
725
    /**
726
     * Get the given user's total edit counts per namespace.
727
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
728
     */
729
    public function namespaceTotals()
730
    {
731
        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...
732
            return $this->namespaceTotals;
733
        }
734
        $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...
735
        arsort($counts);
736
        $this->namespaceTotals = $counts;
737
        return $counts;
738
    }
739
740
    /**
741
     * Get a summary of the times of day and the days of the week that the user has edited.
742
     * @return string[]
743
     */
744
    public function timeCard()
745
    {
746
        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...
747
            return $this->timeCardData;
748
        }
749
        $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...
750
        $this->timeCardData = $totals;
751
        return $totals;
752
    }
753
754
    /**
755
     * Get the total numbers of edits per month.
756
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
757
     *   so we can mock the current DateTime.
758
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
759
     *   the latter keyed by namespace, year and then month.
760
     */
761
    public function monthCounts($currentTime = null)
762
    {
763
        if (isset($this->monthCounts)) {
764
            return $this->monthCounts;
765
        }
766
767
        // Set to current month if we're not unit-testing
768
        if (!($currentTime instanceof DateTime)) {
769
            $currentTime = new DateTime('last day of this month');
770
        }
771
772
        $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...
773
        $out = [
774
            'yearLabels' => [],  // labels for years
775
            'monthLabels' => [], // labels for months
776
            'totals' => [], // actual totals, grouped by namespace, year and then month
777
        ];
778
779
        /** @var DateTime Keep track of the date of their first edit. */
780
        $firstEdit = new DateTime();
781
782
        // Loop through the database results and fill in the values
783
        //   for the months that we have data for.
784
        foreach ($totals as $total) {
785
            // Keep track of first edit
786
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
787
            if ($date < $firstEdit) {
788
                $firstEdit = $date;
789
            }
790
791
            // Collate the counts by namespace, and then year and month.
792
            $ns = $total['page_namespace'];
793
            if (!isset($out['totals'][$ns])) {
794
                $out['totals'][$ns] = [];
795
            }
796
797
            // Start array for this year if not already present.
798
            if (!isset($out['totals'][$ns][$total['year']])) {
799
                $out['totals'][$ns][$total['year']] = [];
800
            }
801
802
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
803
        }
804
805
        $dateRange = new DatePeriod(
806
            $firstEdit,
807
            new DateInterval('P1M'),
808
            $currentTime->modify('first day of this month')
809
        );
810
811
        foreach ($dateRange as $monthObj) {
812
            $year = (int) $monthObj->format('Y');
813
            $month = (int) $monthObj->format('n');
814
815
            // Fill in labels
816
            $out['monthLabels'][] = $monthObj->format('Y-m');
817
            if (!in_array($year, $out['yearLabels'])) {
818
                $out['yearLabels'][] = $year;
819
            }
820
821
            foreach (array_keys($out['totals']) as $nsId) {
822
                if (!isset($out['totals'][$nsId][$year])) {
823
                    $out['totals'][$nsId][$year] = [];
824
                }
825
826
                if (!isset($out['totals'][$nsId][$year][$month])) {
827
                    $out['totals'][$nsId][$year][$month] = 0;
828
                }
829
            }
830
        }
831
832
        // One more set of loops to sort by year/month
833
        foreach (array_keys($out['totals']) as $nsId) {
834
            ksort($out['totals'][$nsId]);
835
836
            foreach ($out['totals'][$nsId] as &$yearData) {
837
                ksort($yearData);
838
            }
839
        }
840
841
        // Finally, sort the namespaces
842
        ksort($out['totals']);
843
844
        $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...
845
        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...
846
    }
847
848
    /**
849
     * Get the total numbers of edits per year.
850
     * @param null|DateTime [$currentTime] - *USED ONLY FOR UNIT TESTING*
851
     *   so we can mock the current DateTime.
852
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter
853
     *   keyed by namespace then year.
854
     */
855
    public function yearCounts($currentTime = null)
856
    {
857
        if (isset($this->yearCounts)) {
858
            return $this->yearCounts;
859
        }
860
861
        $out = $this->monthCounts($currentTime);
862
863
        foreach ($out['totals'] as $nsId => $years) {
864
            foreach ($years as $year => $months) {
865
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
866
            }
867
        }
868
869
        $this->yearCounts = $out;
870
        return $out;
871
    }
872
873
    /**
874
     * Get the total edit counts for the top n projects of this user.
875
     * @param int $numProjects
876
     * @return mixed[] Each element has 'total' and 'project' keys.
877
     */
878
    public function globalEditCountsTopN($numProjects = 10)
879
    {
880
        // Get counts.
881
        $editCounts = $this->globalEditCounts(true);
882
        // 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...
883
        return array_slice($editCounts, 0, $numProjects);
884
    }
885
886
    /**
887
     * Get the total number of edits excluding the top n.
888
     * @param int $numProjects
889
     * @return int
890
     */
891
    public function globalEditCountWithoutTopN($numProjects = 10)
892
    {
893
        $editCounts = $this->globalEditCounts(true);
894
        $bottomM = array_slice($editCounts, $numProjects);
895
        $total = 0;
896
        foreach ($bottomM as $editCount) {
897
            $total += $editCount['total'];
898
        }
899
        return $total;
900
    }
901
902
    /**
903
     * Get the grand total of all edits on all projects.
904
     * @return int
905
     */
906
    public function globalEditCount()
907
    {
908
        $total = 0;
909
        foreach ($this->globalEditCounts() as $editCount) {
910
            $total += $editCount['total'];
911
        }
912
        return $total;
913
    }
914
915
    /**
916
     * Get the total revision counts for all projects for this user.
917
     * @param bool $sorted Whether to sort the list by total, or not.
918
     * @return mixed[] Each element has 'total' and 'project' keys.
919
     */
920
    public function globalEditCounts($sorted = false)
921
    {
922
        if (empty($this->globalEditCounts)) {
923
            $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...
924
                ->globalEditCounts($this->user, $this->project);
925
        }
926
927
        if ($sorted) {
928
            // Sort.
929
            uasort($this->globalEditCounts, function ($a, $b) {
930
                return $b['total'] - $a['total'];
931
            });
932
        }
933
934
        return $this->globalEditCounts;
935
    }
936
937
    /**
938
     * Get the most recent n revisions across all projects.
939
     * @param int $max The maximum number of revisions to return.
940
     * @return Edit[]
941
     */
942
    public function globalEdits($max)
943
    {
944
        // Collect all projects with any edits.
945
        $projects = [];
946
        foreach ($this->globalEditCounts() as $editCount) {
947
            // Don't query revisions if there aren't any.
948
            if ($editCount['total'] == 0) {
949
                continue;
950
            }
951
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
952
        }
953
954
        // Get all revisions for those projects.
955
        $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...
956
            ->getRevisions($projects, $this->user, $max);
957
        $globalEdits = [];
958
        foreach ($globalRevisionsData as $revision) {
959
            /** @var Project $project */
960
            $project = $projects[$revision['project_name']];
961
            $nsName = '';
962
            if ($revision['page_namespace']) {
963
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
964
            }
965
            $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...
966
                ->getPage($project, $nsName . ':' . $revision['page_title']);
967
            $edit = new Edit($page, $revision);
968
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
969
        }
970
971
        // Sort and prune, before adding more.
972
        krsort($globalEdits);
973
        $globalEdits = array_slice($globalEdits, 0, $max);
974
        return $globalEdits;
975
    }
976
977
    /**
978
     * Get average edit size, and number of large and small edits.
979
     * @return int[]
980
     */
981
    public function getEditSizeData()
982
    {
983
        if (!is_array($this->editSizeData)) {
984
            $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...
985
                ->getEditSizeData($this->project, $this->user);
986
        }
987
        return $this->editSizeData;
988
    }
989
990
    /**
991
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
992
     * This is used to ensure percentages of small and large edits are computed properly.
993
     * @return int
994
     */
995
    public function countLast5000()
996
    {
997
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
998
    }
999
1000
    /**
1001
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
1002
     * @return int
1003
     */
1004
    public function countSmallEdits()
1005
    {
1006
        $editSizeData = $this->getEditSizeData();
1007
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
1008
    }
1009
1010
    /**
1011
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
1012
     * @return int
1013
     */
1014
    public function countLargeEdits()
1015
    {
1016
        $editSizeData = $this->getEditSizeData();
1017
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
1018
    }
1019
1020
    /**
1021
     * Get the average size of the user's past 5000 edits.
1022
     * @return float Size in bytes.
1023
     */
1024
    public function averageEditSize()
1025
    {
1026
        $editSizeData = $this->getEditSizeData();
1027
        if (isset($editSizeData['average_size'])) {
1028
            return round($editSizeData['average_size'], 3);
1029
        } else {
1030
            return 0;
1031
        }
1032
    }
1033
}
1034