Completed
Push — master ( 65ce64...a47571 )
by Sam
02:52
created

EditCounterRepository::getRevisions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 54
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 54
rs 9.0306
cc 4
eloc 29
nc 4
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file contains only the EditCounterRepository class.
4
 */
5
6
namespace Xtools;
7
8
use AppBundle\Helper\AutomatedEditsHelper;
9
use DateInterval;
10
use DateTime;
11
use Mediawiki\Api\SimpleRequest;
12
13
/**
14
 * An EditCounterRepository is responsible for retrieving edit count information from the
15
 * databases and API. It doesn't do any post-processing of that information.
16
 */
17
class EditCounterRepository extends Repository
18
{
19
20
    /**
21
     * Get revision counts for the given user.
22
     * @param Project $project The project.
23
     * @param User $user The user.
24
     * @returns string[] With keys: 'deleted', 'live', 'total', 'first', 'last', '24h', '7d', '30d',
25
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits'.
26
     */
27
    public function getRevisionCounts(Project $project, User $user)
28
    {
29
        // Set up cache.
30
        $cacheKey = 'revisioncounts.' . $project->getDatabaseName() . '.' . $user->getUsername();
31
        if ($this->cache->hasItem($cacheKey)) {
32
            return $this->cache->getItem($cacheKey)->get();
33
        }
34
35
        // Prepare the queries and execute them.
36
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
37
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
38
        $queries = [
39
            'deleted' => "SELECT COUNT(ar_id) FROM $archiveTable
40
                WHERE ar_user = :userId",
41
            'live' => "SELECT COUNT(rev_id) FROM $revisionTable
42
                WHERE rev_user = :userId",
43
            'day' => "SELECT COUNT(rev_id) FROM $revisionTable
44
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
45
            'week' => "SELECT COUNT(rev_id) FROM $revisionTable
46
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
47
            'month' => "SELECT COUNT(rev_id) FROM $revisionTable
48
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
49
            'year' => "SELECT COUNT(rev_id) FROM $revisionTable
50
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)",
51
            'small' => "SELECT COUNT(rev_id) FROM $revisionTable
52
                WHERE rev_user = :userId AND rev_len < 20",
53
            'large' => "SELECT COUNT(rev_id) FROM $revisionTable
54
                WHERE rev_user = :userId AND rev_len > 1000",
55
            'with_comments' => "SELECT COUNT(rev_id) FROM $revisionTable
56
                WHERE rev_user = :userId AND rev_comment = ''",
57
            'minor' => "SELECT COUNT(rev_id) FROM $revisionTable
58
                WHERE rev_user = :userId AND rev_minor_edit = 1",
59
            'average_size' => "SELECT AVG(rev_len) FROM $revisionTable
60
                WHERE rev_user = :userId",
61
        ];
62
        $this->stopwatch->start($cacheKey);
63
        $revisionCounts = [];
64
        foreach ($queries as $varName => $query) {
65
            $resultQuery = $this->getProjectsConnection()->prepare($query);
66
            $userId = $user->getId($project);
67
            $resultQuery->bindParam("userId", $userId);
68
            $resultQuery->execute();
69
            $val = $resultQuery->fetchColumn();
70
            $revisionCounts[$varName] = $val ?: 0;
71
            $this->stopwatch->lap($cacheKey);
72
        }
73
74
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
75
        $this->stopwatch->stop($cacheKey);
76
        $cacheItem = $this->cache->getItem($cacheKey)
77
                ->set($revisionCounts)
78
                ->expiresAfter(new DateInterval('PT10M'));
79
        $this->cache->save($cacheItem);
80
81
        return $revisionCounts;
82
    }
83
84
    /**
85
     * Get the first and last revision dates (in MySQL YYYYMMDDHHMMSS format).
86
     * @param Project $project The project.
87
     * @param User $user The user.
88
     * @return string[] With keys 'first' and 'last'.
89
     */
90
    public function getRevisionDates(Project $project, User $user)
91
    {
92
        // Set up cache.
93
        $cacheKey = 'revisiondates.' . $project->getDatabaseName() . '.' . $user->getUsername();
94
        if ($this->cache->hasItem($cacheKey)) {
95
            return $this->cache->getItem($cacheKey)->get();
96
        }
97
98
        $this->stopwatch->start($cacheKey);
99
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
100
        $query = "(SELECT 'first' AS `key`, rev_timestamp AS `date` FROM $revisionTable
101
            WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1)
102
            UNION
103
            (SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
104
            WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1)";
105
        $resultQuery = $this->getProjectsConnection()->prepare($query);
106
        $userId = $user->getId($project);
107
        $resultQuery->bindParam("userId", $userId);
108
        $resultQuery->execute();
109
        $result = $resultQuery->fetchAll();
110
        $revisionDates = [];
111
        foreach ($result as $res) {
112
            $revisionDates[$res['key']] = $res['date'];
113
        }
114
115
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
116
        $cacheItem = $this->cache->getItem($cacheKey)
117
                ->set($revisionDates)
118
                ->expiresAfter(new DateInterval('PT10M'));
119
        $this->cache->save($cacheItem);
120
        $this->stopwatch->stop($cacheKey);
121
        return $revisionDates;
122
    }
123
124
    /**
125
     * Get page counts for the given user, both for live and deleted pages/revisions.
126
     * @param Project $project The project.
127
     * @param User $user The user.
128
     * @return int[] With keys: edited-live, edited-deleted, created-live, created-deleted.
129
     */
130
    public function getPageCounts(Project $project, User $user)
131
    {
132
        // Set up cache.
133
        $cacheKey = 'pagecounts.'.$project->getDatabaseName().'.'.$user->getUsername();
134
        if ($this->cache->hasItem($cacheKey)) {
135
            return $this->cache->getItem($cacheKey)->get();
136
        }
137
        $this->stopwatch->start($cacheKey, 'XTools');
138
139
        // Build and execute query.
140
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
141
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
142
        $resultQuery = $this->getProjectsConnection()->prepare("
143
            (SELECT 'edited-live' AS source, COUNT(DISTINCT rev_page) AS value
144
                FROM $revisionTable
145
                WHERE rev_user_text=:username)
146
            UNION
147
            (SELECT 'edited-deleted' AS source, COUNT(DISTINCT ar_page_id) AS value
148
                FROM $archiveTable
149
                WHERE ar_user_text=:username)
150
            UNION
151
            (SELECT 'created-live' AS source, COUNT(DISTINCT rev_page) AS value
152
                FROM $revisionTable
153
                WHERE rev_user_text=:username AND rev_parent_id=0)
154
            UNION
155
            (SELECT 'created-deleted' AS source, COUNT(DISTINCT ar_page_id) AS value
156
                FROM $archiveTable
157
                WHERE ar_user_text=:username AND ar_parent_id=0)
158
            ");
159
        $username = $user->getUsername();
160
        $resultQuery->bindParam("username", $username);
161
        $resultQuery->execute();
162
        $results = $resultQuery->fetchAll();
163
164
        $pageCounts = array_combine(
165
            array_map(function ($e) {
166
                return $e['source'];
167
            }, $results),
168
            array_map(function ($e) {
169
                return $e['value'];
170
            }, $results)
171
        );
172
173
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
174
        $cacheItem = $this->cache->getItem($cacheKey)
175
            ->set($pageCounts)
176
            ->expiresAfter(new DateInterval('PT10M'));
177
        $this->cache->save($cacheItem);
178
        $this->stopwatch->stop($cacheKey);
179
        return $pageCounts;
180
    }
181
182
    /**
183
     * Get log totals for a user.
184
     * @param Project $project The project.
185
     * @param User $user The user.
186
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
187
     */
188
    public function getLogCounts(Project $project, User $user)
189
    {
190
        // Set up cache.
191
        $cacheKey = 'logcounts.'.$project->getDatabaseName().'.'.$user->getUsername();
192
        if ($this->cache->hasItem($cacheKey)) {
193
            return $this->cache->getItem($cacheKey)->get();
194
        }
195
        $this->stopwatch->start($cacheKey, 'XTools');
196
197
        // Query.
198
        $userNamespaceId = 2;
199
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
200
        $sql = "
201
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
202
            FROM $loggingTable
203
            WHERE log_user = :userId
204
            GROUP BY log_type, log_action
205
        ) UNION
206
        (SELECT 'users-unblocked' AS source, COUNT(DISTINCT log_title) AS value
207
            FROM $loggingTable
208
            WHERE log_user = :userId
209
                AND log_type = 'block'
210
                AND log_action = 'unblock'
211
                AND log_namespace = $userNamespaceId
212
        )";
213
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
214
        $userId = $user->getId($project);
215
        $resultQuery->bindParam('userId', $userId);
216
        $resultQuery->execute();
217
        $results = $resultQuery->fetchAll();
218
        $logCounts = array_combine(
219
            array_map(function ($e) {
220
                return $e['source'];
221
            }, $results),
222
            array_map(function ($e) {
223
                return $e['value'];
224
            }, $results)
225
        );
226
227
        // Make sure there is some value for each of the wanted counts.
228
        $requiredCounts = [
229
            'thanks-thank',
230
            'review-approve',
231
            'patrol-patrol',
232
            'block-block',
233
            'block-reblock',
234
            'block-unblock',
235
            'users-unblocked', // Second query above.
236
            'protect-protect',
237
            'protect-unprotect',
238
            'move-move',
239
            'delete-delete',
240
            'delete-revision',
241
            'delete-restore',
242
            'import-import',
243
            'import-interwiki',
244
            'import-upload',
245
            'upload-upload',
246
            'upload-overwrite',
247
        ];
248
        foreach ($requiredCounts as $req) {
249
            if (!isset($logCounts[$req])) {
250
                $logCounts[$req] = 0;
251
            }
252
        }
253
254
        // Add Commons upload count, if applicable.
255
        $logCounts['files_uploaded_commons'] = 0;
256
        if ($this->isLabs()) {
257
            $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
258
                WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
259
            $resultQuery = $this->getProjectsConnection()->prepare($sql);
260
            $resultQuery->bindParam('userId', $userId);
261
            $resultQuery->execute();
262
            $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
263
        }
264
265
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
266
        $cacheItem = $this->cache->getItem($cacheKey)
267
            ->set($logCounts)
268
            ->expiresAfter(new DateInterval('PT10M'));
269
        $this->cache->save($cacheItem);
270
        $this->stopwatch->stop($cacheKey);
271
272
        return $logCounts;
0 ignored issues
show
Best Practice introduced by
The expression return $logCounts; seems to be an array, but some of its elements' types (boolean) are incompatible with the return type documented by Xtools\EditCounterRepository::getLogCounts of type integer[].

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 new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return '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...
273
    }
274
275
    /**
276
     * Get data for all blocks set by the given user.
277
     * @param Project $project
278
     * @param User $user
279
     * @return array
280
     */
281 View Code Duplication
    public function getBlocksSet(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
282
    {
283
        $ipblocksTable = $this->getTableName($project->getDatabaseName(), 'ipblocks');
284
        $sql = "SELECT * FROM $ipblocksTable WHERE ipb_by = :userId";
285
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
286
        $userId = $user->getId($project);
287
        $resultQuery->bindParam('userId', $userId);
288
        $resultQuery->execute();
289
        return $resultQuery->fetchAll();
290
    }
291
292
    /**
293
     * Get data for all blocks set on the given user.
294
     * @param Project $project
295
     * @param User $user
296
     * @return array
297
     */
298 View Code Duplication
    public function getBlocksReceived(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
    {
300
        $ipblocksTable = $this->getTableName($project->getDatabaseName(), 'ipblocks');
301
        $sql = "SELECT * FROM $ipblocksTable WHERE ipb_user = :userId";
302
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
303
        $userId = $user->getId($project);
304
        $resultQuery->bindParam('userId', $userId);
305
        $resultQuery->execute();
306
        return $resultQuery->fetchAll();
307
    }
308
309
    /**
310
     * Get a user's total edit count on all projects.
311
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
312
     * @see EditCounterRepository::globalEditCountsFromDatabases()
313
     * @param User $user The user.
314
     * @param Project $project The project to start from.
315
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
316
     */
317
    public function globalEditCounts(User $user, Project $project)
318
    {
319
        // Get the edit counts from CentralAuth or database.
320
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
321
        if ($editCounts === false) {
322
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
323
        }
324
325
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
326
        $project->getRepository()->getAll();
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 getAll() 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...
327
328
        // Compile the output.
329
        $out = [];
330
        foreach ($editCounts as $editCount) {
331
            $out[] = [
332
                'project' => ProjectRepository::getProject($editCount['dbname'], $this->container),
333
                'total' => $editCount['total'],
334
            ];
335
        }
336
        return $out;
337
    }
338
339
    /**
340
     * Get a user's total edit count on one or more project.
341
     * Requires the CentralAuth extension to be installed on the project.
342
     *
343
     * @param User $user The user.
344
     * @param Project $project The project to start from.
345
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
346
     */
347
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
348
    {
349
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
350
        // Set up cache and stopwatch.
351
        $cacheKey = 'globalRevisionCounts.'.$user->getUsername();
352
        if ($this->cache->hasItem($cacheKey)) {
353
            return $this->cache->getItem($cacheKey)->get();
354
        }
355
        $this->stopwatch->start($cacheKey, 'XTools');
356
357
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
358
        $project->getRepository()->getAll();
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 getAll() 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...
359
360
        $api = $this->getMediawikiApi($project);
361
        $params = [
362
            'meta' => 'globaluserinfo',
363
            'guiprop' => 'editcount|merged',
364
            'guiuser' => $user->getUsername(),
365
        ];
366
        $query = new SimpleRequest('query', $params);
367
        $result = $api->getRequest($query);
368
        if (!isset($result['query']['globaluserinfo']['merged'])) {
369
            return [];
370
        }
371
        $out = [];
372
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
373
            $out[] = [
374
                'dbname' => $result['wiki'],
375
                'total' => $result['editcount'],
376
            ];
377
        }
378
379
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
380
        $cacheItem = $this->cache->getItem($cacheKey)
381
            ->set($out)
382
            ->expiresAfter(new DateInterval('PT10M'));
383
        $this->cache->save($cacheItem);
384
        $this->stopwatch->stop($cacheKey);
385
386
        return $out;
387
    }
388
389
    /**
390
     * Get total edit counts from all projects for this user.
391
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
392
     * @param User $user The user.
393
     * @param Project $project The project to start from.
394
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
395
     */
396
    protected function globalEditCountsFromDatabases(User $user, Project $project)
397
    {
398
        $stopwatchName = 'globalRevisionCounts.'.$user->getUsername();
399
        $allProjects = $project->getRepository()->getAll();
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 getAll() 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...
400
        $topEditCounts = [];
401
        foreach ($allProjects as $projectMeta) {
402
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
403
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
404
            $stmt = $this->getProjectsConnection()->prepare($sql);
405
            $stmt->bindParam("username", $user->getUsername());
0 ignored issues
show
Bug introduced by
$user->getUsername() cannot be passed to bindparam() as the parameter $variable expects a reference.
Loading history...
406
            $stmt->execute();
407
            $total = (int)$stmt->fetchColumn();
408
            $topEditCounts[] = [
409
                'dbname' => $projectMeta['dbName'],
410
                'total' => $total,
411
            ];
412
            $this->stopwatch->lap($stopwatchName);
413
        }
414
        return $topEditCounts;
415
    }
416
417
    /**
418
     * Get the given user's total edit counts per namespace on the given project.
419
     * @param Project $project The project.
420
     * @param User $user The user.
421
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
422
     */
423
    public function getNamespaceTotals(Project $project, User $user)
424
    {
425
        // Cache?
426
        $userId = $user->getId($project);
427
        $cacheKey = "ec.namespacetotals.{$project->getDatabaseName()}.$userId";
428
        $this->stopwatch->start($cacheKey, 'XTools');
429
        if ($this->cache->hasItem($cacheKey)) {
430
            return $this->cache->getItem($cacheKey)->get();
431
        }
432
433
        // Query.
434
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
435
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
436
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
437
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
438
            WHERE r.rev_user = :id
439
            GROUP BY page_namespace";
440
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
441
        $resultQuery->bindParam(":id", $userId);
442
        $resultQuery->execute();
443
        $results = $resultQuery->fetchAll();
444
        $namespaceTotals = array_combine(array_map(function ($e) {
445
            return $e['page_namespace'];
446
        }, $results), array_map(function ($e) {
447
            return $e['total'];
448
        }, $results));
449
450
        // Cache and return.
451
        $cacheItem = $this->cache->getItem($cacheKey);
452
        $cacheItem->set($namespaceTotals);
453
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
454
        $this->cache->save($cacheItem);
455
        $this->stopwatch->stop($cacheKey);
456
        return $namespaceTotals;
457
    }
458
459
    /**
460
     * Get revisions by this user.
461
     * @param Project[] $projects The projects.
462
     * @param User $user The user.
463
     * @param int $lim The maximum number of revisions to fetch from each project.
464
     * @return array|mixed
465
     */
466
    public function getRevisions($projects, User $user, $lim = 40)
467
    {
468
        // Check cache.
469
        $username = $user->getUsername();
470
        $cacheKey = "globalcontribs.$username";
471
        $this->stopwatch->start($cacheKey, 'XTools');
472
        if ($this->cache->hasItem($cacheKey)) {
473
            return $this->cache->getItem($cacheKey)->get();
474
        }
475
476
        // Assemble queries.
477
        $queries = [];
478
        foreach ($projects as $project) {
479
            $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
480
            $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
481
            $sql = "SELECT
482
                    '".$project->getDatabaseName()."' AS project_name,
483
                    revs.rev_id AS id,
484
                    revs.rev_timestamp AS timestamp,
485
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
486
                    revs.rev_minor_edit AS minor,
487
                    revs.rev_deleted AS deleted,
488
                    revs.rev_len AS length,
489
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
490
                    revs.rev_parent_id AS parent_id,
491
                    revs.rev_comment AS comment,
492
                    revs.rev_user_text AS username,
493
                    page.page_title,
494
                    page.page_namespace
495
                FROM $revisionTable AS revs
496
                    JOIN $pageTable AS page ON (rev_page = page_id)
497
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
498
                WHERE revs.rev_user_text LIKE :username
499
                ORDER BY revs.rev_timestamp DESC";
500
            if (is_numeric($lim)) {
501
                $sql .= " LIMIT $lim";
502
            }
503
            $queries[] = $sql;
504
        }
505
        $sql = "(\n" . join("\n) UNION (\n", $queries) . ")\n";
506
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
507
        $resultQuery->bindParam(":username", $username);
508
        $resultQuery->execute();
509
        $revisions = $resultQuery->fetchAll();
510
511
        // Cache this.
512
        $cacheItem = $this->cache->getItem($cacheKey);
513
        $cacheItem->set($revisions);
514
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
515
        $this->cache->save($cacheItem);
516
517
        $this->stopwatch->stop($cacheKey);
518
        return $revisions;
519
    }
520
521
    /**
522
     * Get data for a bar chart of monthly edit totals per namespace.
523
     * @param Project $project The project.
524
     * @param User $user The user.
525
     * @return string[]
526
     */
527 View Code Duplication
    public function getMonthCounts(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
528
    {
529
        $username = $user->getUsername();
530
        $cacheKey = "monthcounts.$username";
531
        $this->stopwatch->start($cacheKey, 'XTools');
532
        if ($this->cache->hasItem($cacheKey)) {
533
            return $this->cache->getItem($cacheKey)->get();
534
        }
535
536
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
537
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
538
        $sql =
539
            "SELECT "
540
            . "     YEAR(rev_timestamp) AS `year`,"
541
            . "     MONTH(rev_timestamp) AS `month`,"
542
            . "     page_namespace,"
543
            . "     COUNT(rev_id) AS `count` "
544
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
545
            . " WHERE rev_user_text = :username"
546
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace "
547
            . " ORDER BY rev_timestamp DESC";
548
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
549
        $resultQuery->bindParam(":username", $username);
550
        $resultQuery->execute();
551
        $totals = $resultQuery->fetchAll();
552
        
553
        $cacheItem = $this->cache->getItem($cacheKey);
554
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
555
        $cacheItem->set($totals);
556
        $this->cache->save($cacheItem);
557
558
        $this->stopwatch->stop($cacheKey);
559
        return $totals;
560
    }
561
562
    /**
563
     * Get yearly edit totals for this user, grouped by namespace.
564
     * @param Project $project The project.
565
     * @param User $user The user.
566
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
567
     */
568 View Code Duplication
    public function getYearCounts(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
569
    {
570
        $username = $user->getUsername();
571
        $cacheKey = "yearcounts.$username";
572
        $this->stopwatch->start($cacheKey, 'XTools');
573
        if ($this->cache->hasItem($cacheKey)) {
574
            return $this->cache->getItem($cacheKey)->get();
575
        }
576
577
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
578
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
579
        $sql = "SELECT "
580
            . "     YEAR(rev_timestamp) AS `year`,"
581
            . "     page_namespace,"
582
            . "     COUNT(rev_id) AS `count` "
583
            . " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
584
            . " WHERE rev_user_text = :username"
585
            . " GROUP BY YEAR(rev_timestamp), page_namespace "
586
            . " ORDER BY rev_timestamp DESC ";
587
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
588
        $resultQuery->bindParam(":username", $username);
589
        $resultQuery->execute();
590
        $totals = $resultQuery->fetchAll();
591
592
        $cacheItem = $this->cache->getItem($cacheKey);
593
        $cacheItem->set($totals);
594
        $cacheItem->expiresAfter(new DateInterval('P10M'));
595
        $this->cache->save($cacheItem);
596
597
        $this->stopwatch->stop($cacheKey);
598
        return $totals;
599
    }
600
601
    /**
602
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
603
     * @param Project $project
604
     * @param User $user
605
     * @return string[]
606
     */
607
    public function getTimeCard(Project $project, User $user)
608
    {
609
        $username = $user->getUsername();
610
        $cacheKey = "timecard.".$username;
611
        $this->stopwatch->start($cacheKey, 'XTools');
612
        if ($this->cache->hasItem($cacheKey)) {
613
            return $this->cache->getItem($cacheKey)->get();
614
        }
615
616
        $hourInterval = 2;
617
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
618
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
619
        $sql = "SELECT "
620
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
621
            . "     $xCalc AS `x`, "
622
            . "     COUNT(rev_id) AS `r` "
623
            . " FROM $revisionTable"
624
            . " WHERE rev_user_text = :username"
625
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
626
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
627
        $resultQuery->bindParam(":username", $username);
628
        $resultQuery->execute();
629
        $totals = $resultQuery->fetchAll();
630
        // Scale the radii: get the max, then scale each radius.
631
        // This looks inefficient, but there's a max of 72 elements in this array.
632
        $max = 0;
633
        foreach ($totals as $total) {
634
            $max = max($max, $total['r']);
635
        }
636
        foreach ($totals as &$total) {
637
            $total['r'] = round($total['r'] / $max * 100);
638
        }
639
        $cacheItem = $this->cache->getItem($cacheKey);
640
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
641
        $cacheItem->set($totals);
642
        $this->cache->save($cacheItem);
643
644
        $this->stopwatch->stop($cacheKey);
645
        return $totals;
646
    }
647
648
    /**
649
     * Get a summary of automated edits made by the given user in their last 1000 edits.
650
     * Will cache the result for 10 minutes.
651
     * @param Project $project The project.
652
     * @param User $user The user.
653
     * @return integer[] Array of edit counts, keyed by all tool names from
654
     * app/config/semi_automated.yml
655
     * @TODO This currently uses AutomatedEditsHelper but that could probably be refactored.
656
     */
657
    public function countAutomatedRevisions(Project $project, User $user)
658
    {
659
        $userId = $user->getId($project);
660
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
661
        $this->stopwatch->start($cacheKey, 'XTools');
662
        if ($this->cache->hasItem($cacheKey)) {
663
            $this->log->debug("Using cache for $cacheKey");
664
            return $this->cache->getItem($cacheKey)->get();
665
        }
666
667
        /** @var AutomatedEditsHelper $automatedEditsHelper */
668
        $automatedEditsHelper = $this->container->get("app.automated_edits_helper");
669
670
        // Get the most recent 1000 edit summaries.
671
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
672
        $sql = "SELECT rev_comment FROM $revisionTable
673
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
674
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
675
        $resultQuery->bindParam("userId", $userId);
676
        $resultQuery->execute();
677
        $results = $resultQuery->fetchAll();
678
        $editCounts = [];
679
        foreach ($results as $result) {
680
            $toolName = $automatedEditsHelper->getTool($result['rev_comment']);
681
            if ($toolName) {
682
                if (!isset($editCounts[$toolName])) {
683
                    $editCounts[$toolName] = 0;
684
                }
685
                $editCounts[$toolName]++;
686
            }
687
        }
688
        arsort($editCounts);
689
690
        // Cache for 10 minutes.
691
        $this->log->debug("Saving $cacheKey to cache", [$editCounts]);
692
        $cacheItem = $this->cache->getItem($cacheKey);
693
        $cacheItem->set($editCounts);
694
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
695
        $this->cache->save($cacheItem);
696
697
        $this->stopwatch->stop($cacheKey);
698
        return $editCounts;
699
    }
700
}
701