Completed
Push — master ( 1af15d...4bc884 )
by Sam
03:22
created

EditCounterRepository::getTopProjectsEditCounts()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
252
     * @param Project $project The project to start from.
253
     * @return mixed[]|boolean Array of total edit counts, or false if none could be found.
254
     */
255
    public function getRevisionCountsAllProjects(User $user, Project $project)
256
    {
257
        // Set up cache and stopwatch.
258
        $cacheKey = 'globalRevisionCounts.'.$user->getUsername();
259
        if ($this->cache->hasItem($cacheKey)) {
260
            return $this->cache->getItem($cacheKey)->get();
261
        }
262
        $this->stopwatch->start($cacheKey, 'XTools');
263
264
        $api = $this->getMediawikiApi($project);
265
        $params = [
266
            'meta' => 'globaluserinfo',
267
            'guiprop' => 'editcount|merged',
268
            'guiuser' => $user->getUsername(),
269
        ];
270
        $query = new SimpleRequest('query', $params);
271
        $result = $api->getRequest($query);
272
        $out = [];
273
        if (isset($result['query']['globaluserinfo']['merged'])) {
274
            foreach ($result['query']['globaluserinfo']['merged'] as $merged) {
275
                $proj = ProjectRepository::getProject($merged['wiki'], $this->container);
276
                $out[] = [
277
                    'project' => $proj,
278
                    'total' => $merged['editcount'],
279
                ];
280
            }
281
        }
282
        if (empty($out)) {
283
            $out = $this->getRevisionCountsAllProjectsNoCentralAuth($user, $project, $cacheKey);
284
        }
285
286
        // 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...
287
        $cacheItem = $this->cache->getItem($cacheKey)
288
            ->set($out)
289
            ->expiresAfter(new \DateInterval('PT10M'));
290
        $this->cache->save($cacheItem);
291
        $this->stopwatch->stop($cacheKey);
292
293
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $out; (array) is incompatible with the return type documented by Xtools\EditCounterReposi...visionCountsAllProjects of type array|boolean.

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...
294
    }
295
296
    /**
297
     * Get total edit counts for the top 10 projects for this user.
298
     * @param string $username The username.
0 ignored issues
show
Bug introduced by
There is no parameter named $username. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
299
     * @return string[] Elements are arrays with 'dbName', 'url', 'name', and 'total'.
300
     */
301
    protected function getRevisionCountsAllProjectsNoCentralAuth(
302
        User $user,
303
        Project $project,
304
        $stopwatchName
305
    ) {
306
        $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...
307
        $topEditCounts = [];
308
        foreach ($allProjects as $projectMeta) {
309
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
310
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
311
            $stmt = $this->getProjectsConnection()->prepare($sql);
312
            $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...
313
            $stmt->execute();
314
            $total = (int)$stmt->fetchColumn();
315
            $project = ProjectRepository::getProject($projectMeta['dbName'], $this->container);
316
            $topEditCounts[] = [
317
                'project' => $project,
318
                'total' => $total,
319
            ];
320
            $this->stopwatch->lap($stopwatchName);
321
        }
322
        return $topEditCounts;
323
    }
324
325
    /**
326
     * Get the given user's total edit counts per namespace.
327
     * @param string $username The username of the user.
328
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
329
     */
330
    public function getNamespaceTotals($username)
331
    {
332
        $userId = $this->getUserId($username);
0 ignored issues
show
Bug introduced by
The method getUserId() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
333
        $sql = "SELECT page_namespace, count(rev_id) AS total
334
            FROM " . $this->labsHelper->getTable('revision') . " r
0 ignored issues
show
Bug introduced by
The property labsHelper does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
335
                JOIN " . $this->labsHelper->getTable('page') . " p on r.rev_page = p.page_id
336
            WHERE r.rev_user = :id GROUP BY page_namespace";
337
        $resultQuery = $this->replicas->prepare($sql);
0 ignored issues
show
Bug introduced by
The property replicas does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
338
        $resultQuery->bindParam(":id", $userId);
339
        $resultQuery->execute();
340
        $results = $resultQuery->fetchAll();
341
        $namespaceTotals = array_combine(array_map(function ($e) {
342
            return $e['page_namespace'];
343
        }, $results), array_map(function ($e) {
344
            return $e['total'];
345
        }, $results));
346
347
        return $namespaceTotals;
348
    }
349
350
    /**
351
     * Get this user's most recent 10 edits across all projects.
352
     * @param string $username The username.
353
     * @param integer $topN The number of items to return.
354
     * @param integer $days The number of days to search from each wiki.
355
     * @return string[]
356
     */
357
    public function getRecentGlobalContribs($username, $projects = [], $topN = 10, $days = 30)
358
    {
359
        $allRevisions = [];
360
        foreach ($this->labsHelper->getProjectsInfo($projects) as $project) {
361
            $cacheKey = "globalcontribs.{$project['dbName']}.$username";
362
            if ($this->cacheHas($cacheKey)) {
0 ignored issues
show
Bug introduced by
The method cacheHas() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
363
                $revisions = $this->cacheGet($cacheKey);
0 ignored issues
show
Bug introduced by
The method cacheGet() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
364
            } else {
365
                $sql =
366
                    "SELECT rev_id, rev_timestamp, UNIX_TIMESTAMP(rev_timestamp) AS unix_timestamp, " .
367
                    " rev_minor_edit, rev_deleted, rev_len, rev_parent_id, rev_comment, " .
368
                    " page_title, page_namespace " . " FROM " .
369
                    $this->labsHelper->getTable('revision', $project['dbName']) . "    JOIN " .
370
                    $this->labsHelper->getTable('page', $project['dbName']) .
371
                    "    ON (rev_page = page_id)" .
372
                    " WHERE rev_timestamp > NOW() - INTERVAL $days DAY AND rev_user_text LIKE :username" .
373
                    " ORDER BY rev_timestamp DESC" . " LIMIT 10";
374
                $resultQuery = $this->replicas->prepare($sql);
375
                $resultQuery->bindParam(":username", $username);
376
                $resultQuery->execute();
377
                $revisions = $resultQuery->fetchAll();
378
                $this->cacheSave($cacheKey, $revisions, 'PT15M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
379
            }
380
            if (count($revisions) === 0) {
381
                continue;
382
            }
383
            $revsWithProject = array_map(function (&$item) use ($project) {
384
                $item['project_name'] = $project['wikiName'];
385
                $item['project_url'] = $project['url'];
386
                $item['project_db_name'] = $project['dbName'];
387
                $item['rev_time_formatted'] = date('Y-m-d H:i', $item['unix_timestamp']);
388
389
                return $item;
390
            }, $revisions);
391
            $allRevisions = array_merge($allRevisions, $revsWithProject);
392
        }
393
        usort($allRevisions, function ($a, $b) {
394
            return $b['rev_timestamp'] - $a['rev_timestamp'];
395
        });
396
397
        return array_slice($allRevisions, 0, $topN);
398
    }
399
400
    /**
401
     * Get data for a bar chart of monthly edit totals per namespace.
402
     * @param string $username The username.
403
     * @return string[]
404
     */
405
    public function getMonthCounts($username)
406
    {
407
        $cacheKey = "monthcounts.$username";
408
        if ($this->cacheHas($cacheKey)) {
0 ignored issues
show
Bug introduced by
The method cacheHas() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
409
            return $this->cacheGet($cacheKey);
0 ignored issues
show
Bug introduced by
The method cacheGet() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
410
        }
411
412
        $sql =
413
            "SELECT " . "     YEAR(rev_timestamp) AS `year`," .
414
            "     MONTH(rev_timestamp) AS `month`," . "     page_namespace," .
415
            "     COUNT(rev_id) AS `count` " . " FROM " . $this->labsHelper->getTable('revision') .
416
            "    JOIN " . $this->labsHelper->getTable('page') . " ON (rev_page = page_id)" .
417
            " WHERE rev_user_text = :username" .
418
            " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace " .
419
            " ORDER BY rev_timestamp DESC";
420
        $resultQuery = $this->replicas->prepare($sql);
421
        $resultQuery->bindParam(":username", $username);
422
        $resultQuery->execute();
423
        $totals = $resultQuery->fetchAll();
424
        $out = [
425
            'years' => [],
426
            'namespaces' => [],
427
            'totals' => [],
428
        ];
429
        $out['max_year'] = 0;
430
        $out['min_year'] = date('Y');
431
        foreach ($totals as $total) {
432
            // Collect all applicable years and namespaces.
433
            $out['max_year'] = max($out['max_year'], $total['year']);
434
            $out['min_year'] = min($out['min_year'], $total['year']);
435
            // Collate the counts by namespace, and then year and month.
436
            $ns = $total['page_namespace'];
437
            if (!isset($out['totals'][$ns])) {
438
                $out['totals'][$ns] = [];
439
            }
440
            $out['totals'][$ns][$total['year'] . $total['month']] = $total['count'];
441
        }
442
        // Fill in the blanks (where no edits were made in a given month for a namespace).
443
        for ($y = $out['min_year']; $y <= $out['max_year']; $y++) {
444
            for ($m = 1; $m <= 12; $m++) {
445
                foreach ($out['totals'] as $nsId => &$total) {
446
                    if (!isset($total[$y . $m])) {
447
                        $total[$y . $m] = 0;
448
                    }
449
                }
450
            }
451
        }
452
        $this->cacheSave($cacheKey, $out, 'PT10M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
453
454
        return $out;
0 ignored issues
show
Best Practice introduced by
The expression return $out; seems to be an array, but some of its elements' types (array) are incompatible with the return type documented by Xtools\EditCounterRepository::getMonthCounts of type string[].

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...
455
    }
456
457
    /**
458
     * Get yearly edit totals for this user, grouped by namespace.
459
     * @param string $username
460
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
461
     */
462
    public function getYearCounts($username)
463
    {
464
        $cacheKey = "yearcounts.$username";
465
        if ($this->cacheHas($cacheKey)) {
0 ignored issues
show
Bug introduced by
The method cacheHas() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
466
            return $this->cacheGet($cacheKey);
0 ignored issues
show
Bug introduced by
The method cacheGet() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
467
        }
468
469
        $sql =
470
            "SELECT " . "     SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4) AS `year`," .
471
            "     page_namespace," . "     COUNT(rev_id) AS `count` " . " FROM " .
472
            $this->labsHelper->getTable('revision') . "    JOIN " .
473
            $this->labsHelper->getTable('page') . " ON (rev_page = page_id)" .
474
            " WHERE rev_user_text = :username" .
475
            " GROUP BY SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4), page_namespace " .
476
            " ORDER BY rev_timestamp DESC ";
477
        $resultQuery = $this->replicas->prepare($sql);
478
        $resultQuery->bindParam(":username", $username);
479
        $resultQuery->execute();
480
        $totals = $resultQuery->fetchAll();
481
        $out = [
482
            'years' => [],
483
            'namespaces' => [],
484
            'totals' => [],
485
        ];
486
        foreach ($totals as $total) {
487
            $out['years'][$total['year']] = $total['year'];
488
            $out['namespaces'][$total['page_namespace']] = $total['page_namespace'];
489
            if (!isset($out['totals'][$total['page_namespace']])) {
490
                $out['totals'][$total['page_namespace']] = [];
491
            }
492
            $out['totals'][$total['page_namespace']][$total['year']] = $total['count'];
493
        }
494
        $this->cacheSave($cacheKey, $out, 'PT10M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
495
496
        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\EditCounterRepository::getYearCounts of type string[].

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...
497
    }
498
499
    /**
500
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
501
     * @param string $username The user's username.
502
     * @return string[]
503
     */
504
    public function getTimeCard($username)
505
    {
506
        $cacheKey = "timecard.$username";
507
        if ($this->cacheHas($cacheKey)) {
0 ignored issues
show
Bug introduced by
The method cacheHas() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
508
            return $this->cacheGet($cacheKey);
0 ignored issues
show
Bug introduced by
The method cacheGet() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
509
        }
510
511
        $hourInterval = 2;
512
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval)*$hourInterval";
513
        $sql =
514
            "SELECT " . "     DAYOFWEEK(rev_timestamp) AS `y`, " . "     $xCalc AS `x`, " .
515
            "     COUNT(rev_id) AS `r` " . " FROM " . $this->labsHelper->getTable('revision') .
516
            " WHERE rev_user_text = :username" . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc " .
517
            " ";
518
        $resultQuery = $this->replicas->prepare($sql);
519
        $resultQuery->bindParam(":username", $username);
520
        $resultQuery->execute();
521
        $totals = $resultQuery->fetchAll();
522
        // Scale the radii: get the max, then scale each radius.
523
        // This looks inefficient, but there's a max of 72 elements in this array.
524
        $max = 0;
525
        foreach ($totals as $total) {
526
            $max = max($max, $total['r']);
527
        }
528
        foreach ($totals as &$total) {
529
            $total['r'] = round($total['r'] / $max * 100);
530
        }
531
        $this->cacheSave($cacheKey, $totals, 'PT10M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
532
533
        return $totals;
534
    }
535
536
    /**
537
     * Get a summary of automated edits made by the given user in their last 1000 edits.
538
     * Will cache the result for 10 minutes.
539
     * @param User $user The user.
540
     * @return integer[] Array of edit counts, keyed by all tool names from
541
     * app/config/semi_automated.yml
542
     * @TODO this is broke
543
     */
544
    public function countAutomatedRevisions(Project $project, User $user)
545
    {
546
        $userId = $user->getId($project);
547
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
548
        if ($this->cache->hasItem($cacheKey)) {
549
            $this->log->debug("Using cache for $cacheKey");
550
            return $this->cache->getItem($cacheKey)->get();
551
        }
552
553
        // Get the most recent 1000 edit summaries.
554
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
555
        $sql = "SELECT rev_comment FROM $revisionTable
556
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
557
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
558
        $resultQuery->bindParam("userId", $userId);
559
        $resultQuery->execute();
560
        $results = $resultQuery->fetchAll();
561
        $out = [];
562
        foreach ($results as $result) {
563
            $toolName = $this->getTool($result['rev_comment']);
0 ignored issues
show
Bug introduced by
The method getTool() does not exist on Xtools\EditCounterRepository. Did you maybe mean getToolsConnection()?

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...
564
            if ($toolName) {
565
                if (!isset($out[$toolName])) {
566
                    $out[$toolName] = 0;
567
                }
568
                $out[$toolName]++;
569
            }
570
        }
571
        arsort($out);
572
573
        // Cache for 10 minutes.
574
        $this->log->debug("Saving $cacheKey to cache", [$out]);
575
        $this->cacheSave($cacheKey, $out, 'PT10M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
576
577
        return $out;
578
    }
579
}
580