Completed
Push — master ( c9d0e3...458768 )
by Sam
08:17
created

EditCounterRepository::getRevisionCounts()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 60
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 8.9618
c 0
b 0
f 0
cc 4
eloc 38
nc 4
nop 2

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
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.' . $user->getId($project);
20
        if ($this->cache->hasItem($cacheKey)) {
21
            $msg = "Using logged revision counts";
22
            $this->log->debug($msg, [$project->getDatabaseName(), $user->getUsername()]);
23
            return $this->cache->getItem($cacheKey)->get();
24
        }
25
26
        // Prepare the queries and execute them.
27
        $start = microtime();
28
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
29
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
30
        $queries = [
31
            'deleted' => "SELECT COUNT(ar_id) FROM $archiveTable
32
                WHERE ar_user = :userId",
33
            'live' => "SELECT COUNT(rev_id) FROM $revisionTable
34
                WHERE rev_user = :userId",
35
            'day' => "SELECT COUNT(rev_id) FROM $revisionTable
36
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
37
            'week' => "SELECT COUNT(rev_id) FROM $revisionTable
38
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
39
            'month' => "SELECT COUNT(rev_id) FROM $revisionTable
40
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
41
            'year' => "SELECT COUNT(rev_id) FROM $revisionTable
42
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)",
43
            'small' => "SELECT COUNT(rev_id) FROM $revisionTable
44
                WHERE rev_user = :userId AND rev_len < 20",
45
            'large' => "SELECT COUNT(rev_id) FROM $revisionTable
46
                WHERE rev_user = :userId AND rev_len > 1000",
47
            'with_comments' => "SELECT COUNT(rev_id) FROM $revisionTable
48
                WHERE rev_user = :userId AND rev_comment = ''",
49
            'minor' => "SELECT COUNT(rev_id) FROM $revisionTable
50
                WHERE rev_user = :userId AND rev_minor_edit = 1",
51
        ];
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
        }
61
        $duration = microtime() - $start;
62
        $this->log->debug(
63
            "Retrieved revision counts in $duration",
64
            [$project->getDatabaseName(), $user->getUsername()]
65
        );
66
67
        // 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...
68
        $cacheItem =
69
            $this->cache->getItem($cacheKey)
70
                ->set($revisionCounts)
71
                ->expiresAfter(new \DateInterval('PT10M'));
72
        $this->cache->save($cacheItem);
73
74
        return $revisionCounts;
75
    }
76
77
    /**
78
     * Get the first and last revision dates (in MySQL YYYYMMDDHHMMSS format).
79
     * @return string[] With keys 'first' and 'last'.
80
     */
81
    public function getRevisionDates(Project $project, User $user)
82
    {
83
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
84
        $query = "(SELECT 'first' AS `key`, rev_timestamp AS `date` FROM $revisionTable
85
            WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1)
86
            UNION
87
            (SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
88
            WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1)";
89
        $resultQuery = $this->getProjectsConnection()->prepare($query);
90
        $userId = $user->getId($project);
91
        $resultQuery->bindParam("userId", $userId);
92
        $resultQuery->execute();
93
        $result = $resultQuery->fetchAll();
94
        $out = [];
95
        foreach ($result as $res) {
96
            $out[$res['key']] = $res['date'];
97
        }
98
99
        return $out;
100
    }
101
102
    /**
103
     * Get page counts for the given user.
104
     * @param User $user
105
     * @return int[]
106
     */
107
    public function getPageCounts(Project $project, User $user)
108
    {
109
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
110
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
111
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
112
        $resultQuery = $this->getProjectsConnection()->prepare("
113
            (SELECT 'edited-total' as source, COUNT(rev_page) as value
114
                FROM $revisionTable where rev_user_text=:username)
115
            UNION
116
            (SELECT 'edited-unique' as source, COUNT(distinct rev_page) as value
117
                FROM $revisionTable where rev_user_text=:username)
118
            UNION
119
            (SELECT 'created-live' as source, COUNT(*) as value from $revisionTable
120
                WHERE rev_user_text=:username and rev_parent_id=0)
121
            UNION
122
            (SELECT 'created-deleted' as source, COUNT(*) as value from $archiveTable
123
                WHERE ar_user_text=:username and ar_parent_id=0)
124
            UNION
125
            (SELECT 'moved' as source, count(*) as value from $loggingTable
126
                WHERE log_type='move' and log_action='move' and log_user_text=:username)
127
            ");
128
        $username = $user->getUsername();
129
        $resultQuery->bindParam("username", $username);
130
        $resultQuery->execute();
131
        $results = $resultQuery->fetchAll();
132
133
        $pageCounts = array_combine(array_map(function ($e) {
134
            return $e['source'];
135
        }, $results), array_map(function ($e) {
136
            return $e['value'];
137
        }, $results));
138
139
        return $pageCounts;
140
    }
141
142
    /**
143
     * Get log totals for a user.
144
     * @param Project $project The project.
145
     * @param User $user The user.
146
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
147
     */
148
    public function getLogCounts(Project $project, User $user)
149
    {
150
        $sql = "SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
151
            FROM " . $this->getTableName($project->getDatabaseName(), 'logging') . "
152
            WHERE log_user = :userId
153
            GROUP BY log_type, log_action";
154
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
155
        $userId = $user->getId($project);
156
        $resultQuery->bindParam('userId', $userId);
157
        $resultQuery->execute();
158
        $results = $resultQuery->fetchAll();
159
        $logCounts = array_combine(
160
            array_map(function ($e) {
161
                return $e['source'];
162
            }, $results),
163
            array_map(function ($e) {
164
                return $e['value'];
165
            }, $results)
166
        );
167
168
        // Make sure there is some value for each of the wanted counts.
169
        $requiredCounts = [
170
            'thanks-thank',
171
            'review-approve',
172
            'patrol-patrol',
173
            'block-block',
174
            'block-unblock',
175
            'protect-protect',
176
            'protect-unprotect',
177
            'delete-delete',
178
            'delete-revision',
179
            'delete-restore',
180
            'import-import',
181
            'upload-upload',
182
            'upload-overwrite',
183
        ];
184
        foreach ($requiredCounts as $req) {
185
            if (!isset($logCounts[$req])) {
186
                $logCounts[$req] = 0;
187
            }
188
        }
189
190
        // Add Commons upload count, if applicable.
191
        $logCounts['files_uploaded_commons'] = 0;
192
        if ($this->isLabs()) {
193
            $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
194
                WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
195
            $resultQuery = $this->getProjectsConnection()->prepare($sql);
196
            $resultQuery->bindParam('userId', $userId);
197
            $resultQuery->execute();
198
            $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
199
        }
200
201
        return $logCounts;
202
    }
203
204
    /**
205
     * Get a user's total edit count on one or more project.
206
     * Requires the CentralAuth extension to be installed on the project.
207
     *
208
     * @param string $username The username.
209
     * @param Project $project The project.
210
     * @return mixed[]|boolean Array of total edit counts, or false if none could be found.
211
     */
212
    public function getRevisionCountsAllProjects($username, Project $project)
213
    {
214
        $api = $this->getMediawikiApi($project);
215
        $params = [
216
            'meta' => 'globaluserinfo',
217
            'guiprop' => 'editcount|merged',
218
            'guiuser' => $username,
219
        ];
220
        $query = new SimpleRequest('query', $params);
221
        $result = $api->getRequest($query);
222
        if (!isset($result['query']['globaluserinfo']['merged'])) {
223
            return false;
224
        }
225
        $out = [];
226
        foreach ($result['query']['globaluserinfo']['merged'] as $merged) {
227
            // The array structure here should match what's done in
228
            // EditCounterHelper::getTopProjectsEditCounts()
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% 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...
229
            $out[$merged['wiki']] = [
230
                'total' => $merged['editcount'],
231
            ];
232
        }
233
234
        return $out;
235
    }
236
237
    /**
238
     * Get total edit counts for the top 10 projects for this user.
239
     * @param string $username The username.
240
     * @return string[] Elements are arrays with 'dbName', 'url', 'name', and 'total'.
241
     */
242
    public function getTopProjectsEditCounts($projectUrl, $username, $numProjects = 10)
243
    {
244
        $this->debug("Getting top project edit counts for $username");
0 ignored issues
show
Bug introduced by
The method debug() 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...
245
        $cacheKey = 'topprojectseditcounts.' . $username;
246
        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...
247
            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...
248
        }
249
250
        // First try to get the edit count from the API (if CentralAuth is installed).
251
        /** @var ApiHelper */
252
        $api = $this->container->get('app.api_helper');
253
        $topEditCounts = $api->getEditCount($username, $projectUrl);
254
        if (false === $topEditCounts) {
255
            // If no CentralAuth, fall back to querying each database in turn.
256
            foreach ($this->labsHelper->getProjectsInfo() as $project) {
257
                $this->container->get('logger')->debug('Getting edit count for ' . $project['url']);
258
                $revisionTableName = $this->labsHelper->getTable('revision', $project['dbName']);
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...
259
                $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
260
                $stmt = $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...
261
                $stmt->bindParam("username", $username);
262
                $stmt->execute();
263
                $total = (int)$stmt->fetchColumn();
264
                $topEditCounts[$project['dbName']] = array_merge($project, ['total' => $total]);
265
            }
266
        }
267
        uasort($topEditCounts, function ($a, $b) {
268
            return $b['total'] - $a['total'];
269
        });
270
        $out = array_slice($topEditCounts, 0, $numProjects);
271
272
        // Cache for ten minutes.
273
        $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...
274
275
        return $out;
276
    }
277
278
    /**
279
     * Get the given user's total edit counts per namespace.
280
     * @param string $username The username of the user.
281
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
282
     */
283
    public function getNamespaceTotals($username)
284
    {
285
        $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...
286
        $sql = "SELECT page_namespace, count(rev_id) AS total
287
            FROM " . $this->labsHelper->getTable('revision') . " r
288
                JOIN " . $this->labsHelper->getTable('page') . " p on r.rev_page = p.page_id
289
            WHERE r.rev_user = :id GROUP BY page_namespace";
290
        $resultQuery = $this->replicas->prepare($sql);
291
        $resultQuery->bindParam(":id", $userId);
292
        $resultQuery->execute();
293
        $results = $resultQuery->fetchAll();
294
        $namespaceTotals = array_combine(array_map(function ($e) {
295
            return $e['page_namespace'];
296
        }, $results), array_map(function ($e) {
297
            return $e['total'];
298
        }, $results));
299
300
        return $namespaceTotals;
301
    }
302
303
    /**
304
     * Get this user's most recent 10 edits across all projects.
305
     * @param string $username The username.
306
     * @param integer $topN The number of items to return.
307
     * @param integer $days The number of days to search from each wiki.
308
     * @return string[]
309
     */
310
    public function getRecentGlobalContribs($username, $projects = [], $topN = 10, $days = 30)
311
    {
312
        $allRevisions = [];
313
        foreach ($this->labsHelper->getProjectsInfo($projects) as $project) {
314
            $cacheKey = "globalcontribs.{$project['dbName']}.$username";
315
            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...
316
                $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...
317
            } else {
318
                $sql =
319
                    "SELECT rev_id, rev_timestamp, UNIX_TIMESTAMP(rev_timestamp) AS unix_timestamp, " .
320
                    " rev_minor_edit, rev_deleted, rev_len, rev_parent_id, rev_comment, " .
321
                    " page_title, page_namespace " . " FROM " .
322
                    $this->labsHelper->getTable('revision', $project['dbName']) . "    JOIN " .
323
                    $this->labsHelper->getTable('page', $project['dbName']) .
324
                    "    ON (rev_page = page_id)" .
325
                    " WHERE rev_timestamp > NOW() - INTERVAL $days DAY AND rev_user_text LIKE :username" .
326
                    " ORDER BY rev_timestamp DESC" . " LIMIT 10";
327
                $resultQuery = $this->replicas->prepare($sql);
328
                $resultQuery->bindParam(":username", $username);
329
                $resultQuery->execute();
330
                $revisions = $resultQuery->fetchAll();
331
                $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...
332
            }
333
            if (count($revisions) === 0) {
334
                continue;
335
            }
336
            $revsWithProject = array_map(function (&$item) use ($project) {
337
                $item['project_name'] = $project['wikiName'];
338
                $item['project_url'] = $project['url'];
339
                $item['project_db_name'] = $project['dbName'];
340
                $item['rev_time_formatted'] = date('Y-m-d H:i', $item['unix_timestamp']);
341
342
                return $item;
343
            }, $revisions);
344
            $allRevisions = array_merge($allRevisions, $revsWithProject);
345
        }
346
        usort($allRevisions, function ($a, $b) {
347
            return $b['rev_timestamp'] - $a['rev_timestamp'];
348
        });
349
350
        return array_slice($allRevisions, 0, $topN);
351
    }
352
353
    /**
354
     * Get data for a bar chart of monthly edit totals per namespace.
355
     * @param string $username The username.
356
     * @return string[]
357
     */
358
    public function getMonthCounts($username)
359
    {
360
        $cacheKey = "monthcounts.$username";
361
        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...
362
            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...
363
        }
364
365
        $sql =
366
            "SELECT " . "     YEAR(rev_timestamp) AS `year`," .
367
            "     MONTH(rev_timestamp) AS `month`," . "     page_namespace," .
368
            "     COUNT(rev_id) AS `count` " . " FROM " . $this->labsHelper->getTable('revision') .
369
            "    JOIN " . $this->labsHelper->getTable('page') . " ON (rev_page = page_id)" .
370
            " WHERE rev_user_text = :username" .
371
            " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace " .
372
            " ORDER BY rev_timestamp DESC";
373
        $resultQuery = $this->replicas->prepare($sql);
374
        $resultQuery->bindParam(":username", $username);
375
        $resultQuery->execute();
376
        $totals = $resultQuery->fetchAll();
377
        $out = [
378
            'years' => [],
379
            'namespaces' => [],
380
            'totals' => [],
381
        ];
382
        $out['max_year'] = 0;
383
        $out['min_year'] = date('Y');
384
        foreach ($totals as $total) {
385
            // Collect all applicable years and namespaces.
386
            $out['max_year'] = max($out['max_year'], $total['year']);
387
            $out['min_year'] = min($out['min_year'], $total['year']);
388
            // Collate the counts by namespace, and then year and month.
389
            $ns = $total['page_namespace'];
390
            if (!isset($out['totals'][$ns])) {
391
                $out['totals'][$ns] = [];
392
            }
393
            $out['totals'][$ns][$total['year'] . $total['month']] = $total['count'];
394
        }
395
        // Fill in the blanks (where no edits were made in a given month for a namespace).
396
        for ($y = $out['min_year']; $y <= $out['max_year']; $y++) {
397
            for ($m = 1; $m <= 12; $m++) {
398
                foreach ($out['totals'] as $nsId => &$total) {
399
                    if (!isset($total[$y . $m])) {
400
                        $total[$y . $m] = 0;
401
                    }
402
                }
403
            }
404
        }
405
        $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...
406
407
        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...
408
    }
409
410
    /**
411
     * Get yearly edit totals for this user, grouped by namespace.
412
     * @param string $username
413
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
414
     */
415
    public function getYearCounts($username)
416
    {
417
        $cacheKey = "yearcounts.$username";
418
        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...
419
            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...
420
        }
421
422
        $sql =
423
            "SELECT " . "     SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4) AS `year`," .
424
            "     page_namespace," . "     COUNT(rev_id) AS `count` " . " FROM " .
425
            $this->labsHelper->getTable('revision') . "    JOIN " .
426
            $this->labsHelper->getTable('page') . " ON (rev_page = page_id)" .
427
            " WHERE rev_user_text = :username" .
428
            " GROUP BY SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4), page_namespace " .
429
            " ORDER BY rev_timestamp DESC ";
430
        $resultQuery = $this->replicas->prepare($sql);
431
        $resultQuery->bindParam(":username", $username);
432
        $resultQuery->execute();
433
        $totals = $resultQuery->fetchAll();
434
        $out = [
435
            'years' => [],
436
            'namespaces' => [],
437
            'totals' => [],
438
        ];
439
        foreach ($totals as $total) {
440
            $out['years'][$total['year']] = $total['year'];
441
            $out['namespaces'][$total['page_namespace']] = $total['page_namespace'];
442
            if (!isset($out['totals'][$total['page_namespace']])) {
443
                $out['totals'][$total['page_namespace']] = [];
444
            }
445
            $out['totals'][$total['page_namespace']][$total['year']] = $total['count'];
446
        }
447
        $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...
448
449
        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...
450
    }
451
452
    /**
453
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
454
     * @param string $username The user's username.
455
     * @return string[]
456
     */
457
    public function getTimeCard($username)
458
    {
459
        $cacheKey = "timecard.$username";
460
        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...
461
            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...
462
        }
463
464
        $hourInterval = 2;
465
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval)*$hourInterval";
466
        $sql =
467
            "SELECT " . "     DAYOFWEEK(rev_timestamp) AS `y`, " . "     $xCalc AS `x`, " .
468
            "     COUNT(rev_id) AS `r` " . " FROM " . $this->labsHelper->getTable('revision') .
469
            " WHERE rev_user_text = :username" . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc " .
470
            " ";
471
        $resultQuery = $this->replicas->prepare($sql);
472
        $resultQuery->bindParam(":username", $username);
473
        $resultQuery->execute();
474
        $totals = $resultQuery->fetchAll();
475
        // Scale the radii: get the max, then scale each radius.
476
        // This looks inefficient, but there's a max of 72 elements in this array.
477
        $max = 0;
478
        foreach ($totals as $total) {
479
            $max = max($max, $total['r']);
480
        }
481
        foreach ($totals as &$total) {
482
            $total['r'] = round($total['r'] / $max * 100);
483
        }
484
        $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...
485
486
        return $totals;
487
    }
488
489
    /**
490
     * Get a summary of automated edits made by the given user in their last 1000 edits.
491
     * Will cache the result for 10 minutes.
492
     * @param User $user The user.
493
     * @return integer[] Array of edit counts, keyed by all tool names from
494
     * app/config/semi_automated.yml
495
     * @TODO this is broke
496
     */
497
    public function countAutomatedRevisions(Project $project, User $user)
498
    {
499
        $userId = $user->getId($project);
500
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
501
        if ($this->cache->hasItem($cacheKey)) {
502
            $this->log->debug("Using cache for $cacheKey");
503
            return $this->cache->getItem($cacheKey)->get();
504
        }
505
506
        // Get the most recent 1000 edit summaries.
507
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
508
        $sql = "SELECT rev_comment FROM $revisionTable
509
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
510
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
511
        $resultQuery->bindParam("userId", $userId);
512
        $resultQuery->execute();
513
        $results = $resultQuery->fetchAll();
514
        $out = [];
515
        foreach ($results as $result) {
516
            $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...
517
            if ($toolName) {
518
                if (!isset($out[$toolName])) {
519
                    $out[$toolName] = 0;
520
                }
521
                $out[$toolName]++;
522
            }
523
        }
524
        arsort($out);
525
526
        // Cache for 10 minutes.
527
        $this->log->debug("Saving $cacheKey to cache", [$out]);
528
        $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...
529
530
        return $out;
531
    }
532
}
533