Completed
Push — master ( c332a3...b7b580 )
by Sam
02:32
created

EditCounterHelper::getRevisionCounts()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 94
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 94
rs 5.4672
c 0
b 0
f 0
cc 8
eloc 47
nc 14
nop 1

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 AppBundle\Helper;
4
5
use AppBundle\Twig\AppExtension;
6
use DateInterval;
7
use Doctrine\DBAL\Connection;
8
use Exception;
9
use Symfony\Component\DependencyInjection\Container;
10
use Symfony\Component\VarDumper\VarDumper;
11
12
class EditCounterHelper extends HelperBase
13
{
14
15
    /** @var Container */
16
    protected $container;
17
18
    /** @var Connection */
19
    protected $replicas;
20
21
    /** @var LabsHelper */
22
    protected $labsHelper;
23
24
    public function __construct(Container $container)
25
    {
26
        $this->container = $container;
27
        $this->replicas = $container->get('doctrine')->getManager('replicas')->getConnection();
28
        $this->labsHelper = $container->get('app.labs_helper');
29
    }
30
31
    /**
32
     * Get the ID of a user.
33
     * @param string $usernameOrIp The username or IP address.
34
     * @return integer The user's ID.
35
     */
36
    public function getUserId($usernameOrIp)
37
    {
38
        $userTable = $this->labsHelper->getTable('user');
39
        $sql = "SELECT user_id FROM $userTable WHERE user_name = :username LIMIT 1";
40
        $resultQuery = $this->replicas->prepare($sql);
41
        $resultQuery->bindParam("username", $usernameOrIp);
42
        $resultQuery->execute();
43
        $userId = (int)$resultQuery->fetchColumn();
44
        return $userId;
45
    }
46
47
    /**
48
     * Get total edit counts for the top 10 projects for this user.
49
     * @param string $username The username.
50
     * @return string[] Elements are arrays with 'dbName', 'url', 'name', and 'total'.
51
     */
52
    public function getTopProjectsEditCounts($username, $numProjects = 10)
53
    {
54
        $topEditCounts = [];
55
        foreach ($this->labsHelper->allProjects() as $project) {
56
            // Get total edit count from DB otherwise.
57
            $revisionTableName = $this->labsHelper->getTable('revision', $project['dbName']);
58
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
59
            $stmt = $this->replicas->prepare($sql);
60
            $stmt->bindParam("username", $username);
61
            $stmt->execute();
62
            $total = (int)$stmt->fetchColumn();
63
            $topEditCounts[$project['dbName']] = array_merge($project, ['total' => $total]);
64
        }
65
        uasort($topEditCounts, function ($a, $b) {
66
            return $b['total'] - $a['total'];
67
        });
68
        return array_slice($topEditCounts, 0, $numProjects);
69
    }
70
71
    /**
72
     * Get revision counts for the given user.
73
     * @param integer $userId The user's ID.
74
     * @returns string[] With keys: 'archived', 'total', 'first', 'last', '24h', '7d', '30d', and
75
     * '365d'.
76
     * @throws Exception
77
     */
78
    public function getRevisionCounts($userId)
79
    {
80
        // Set up cache.
81
        $cacheKey = 'revisioncounts.'.$userId;
82
        if ($this->cacheHas($cacheKey)) {
83
            return $this->cacheGet($cacheKey);
84
        }
85
86
        // Prepare the query and execute
87
        $archiveTable = $this->labsHelper->getTable('archive');
88
        $revisionTable = $this->labsHelper->getTable('revision');
89
        $resultQuery = $this->replicas->prepare("
90
            (SELECT 'deleted' as source, COUNT(ar_id) AS value FROM $archiveTable
91
                WHERE ar_user = :userId)
92
            UNION
93
            (SELECT 'live' as source, COUNT(rev_id) AS value FROM $revisionTable
94
                WHERE rev_user = :userId)
95
            UNION
96
            (SELECT 'first' as source, rev_timestamp FROM $revisionTable
97
                WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1)
98
            UNION
99
            (SELECT 'last' as source, rev_timestamp FROM $revisionTable
100
                WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1)
101
            UNION
102
            (SELECT '24h' as source, COUNT(rev_id) as value FROM $revisionTable
103
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(),INTERVAL 24 HOUR))
104
            UNION
105
            (SELECT '7d' as source, COUNT(rev_id) as value FROM $revisionTable
106
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(),INTERVAL 7 DAY))
107
            UNION
108
            (SELECT '30d' as source, COUNT(rev_id) as value FROM $revisionTable
109
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(),INTERVAL 30 DAY))
110
            UNION
111
            (SELECT '365d' as source, COUNT(rev_id) as value FROM $revisionTable
112
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(),INTERVAL 365 DAY))
113
            UNION
114
            (SELECT 'small' AS source, COUNT(rev_id) AS value FROM $revisionTable
115
                WHERE rev_user = :userId AND rev_len < 20)
116
            UNION
117
            (SELECT 'large' AS source, COUNT(rev_id) AS value FROM $revisionTable
118
                WHERE rev_user = :userId AND rev_len > 1000)
119
            UNION
120
            (SELECT 'with_comments' AS source, COUNT(rev_id) AS value FROM $revisionTable
121
                WHERE rev_user = :userId AND rev_comment = '')
122
            UNION
123
            (SELECT 'minor_edits' AS source, COUNT(rev_id) AS value FROM $revisionTable
124
                WHERE rev_user = :userId AND rev_minor_edit = 1)
125
            ");
126
        $resultQuery->bindParam("userId", $userId);
127
        $resultQuery->execute();
128
        $results = $resultQuery->fetchAll();
129
130
        // Unknown user - This is a dirty hack that should be fixed.
131
        if (count($results) < 8) {
132
            throw new Exception("Unable to get all revision counts for user $userId");
133
        }
134
135
        $revisionCounts = array_combine(
136
            array_map(function ($e) {
137
                return $e['source'];
138
            }, $results),
139
            array_map(function ($e) {
140
                return $e['value'];
141
            }, $results)
142
        );
143
144
        // Count the number of days, accounting for when there's zero or one edit.
145
        $revisionCounts['days'] = 0;
146
        if (isset($revisionCounts['first']) && isset($revisionCounts['last'])) {
147
            $editingTimeInSeconds = ceil($revisionCounts['last'] - $revisionCounts['first']);
148
            $revisionCounts['days'] = $editingTimeInSeconds ? $editingTimeInSeconds/(60*60*24) : 1;
149
        }
150
151
        // Format the first and last dates.
152
        $revisionCounts['first'] = isset($revisionCounts['first'])
153
            ? date('Y-m-d H:i', strtotime($revisionCounts['first']))
154
            : 0;
155
        $revisionCounts['last'] = isset($revisionCounts['last'])
156
            ? date('Y-m-d H:i', strtotime($revisionCounts['last']))
157
            : 0;
158
159
        // Sum deleted and live to make the total.
160
        $revisionCounts['total'] = $revisionCounts['deleted'] + $revisionCounts['live'];
161
162
        // Calculate the average number of live edits per day.
163
        $revisionCounts['avg_per_day'] = round(
164
            $revisionCounts['live'] / $revisionCounts['days'],
165
            3
166
        );
167
168
        // 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...
169
        $this->cacheSave($cacheKey, $revisionCounts, 'PT10M');
0 ignored issues
show
Documentation introduced by
$revisionCounts is of type array<string,?,{"avg_per_day":"double"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
170
        return $revisionCounts;
171
    }
172
173
    /**
174
     *
175
     * @param $username
176
     * @return integer
177
     */
178
    public function getPageCounts($username, $totalRevisions)
179
    {
180
        $resultQuery = $this->replicas->prepare("
181
            SELECT 'unique' as source, COUNT(distinct rev_page) as value
182
                FROM ".$this->labsHelper->getTable('revision')." where rev_user_text=:username
183
            UNION
184
            SELECT 'created-live' as source, COUNT(*) as value from ".$this->labsHelper->getTable('revision')."
185
                WHERE rev_user_text=:username and rev_parent_id=0
186
            UNION
187
            SELECT 'created-deleted' as source, COUNT(*) as value from "
188
                                                .$this->labsHelper->getTable('archive')."
189
                WHERE ar_user_text=:username and ar_parent_id=0
190
            UNION
191
            SELECT 'moved' as source, count(*) as value from ".$this->labsHelper->getTable('logging')."
192
                WHERE log_type='move' and log_action='move' and log_user_text=:username
193
            ");
194
        $resultQuery->bindParam("username", $username);
195
        $resultQuery->execute();
196
        $results = $resultQuery->fetchAll();
197
198
        $pageCounts = array_combine(
199
            array_map(function ($e) {
200
                return $e['source'];
201
            }, $results),
202
            array_map(function ($e) {
203
                return $e['value'];
204
            }, $results)
205
        );
206
207
        // Total created.
208
        $pageCounts['created'] = $pageCounts['created-live'] + $pageCounts['created-deleted'];
209
210
        // Calculate the average number of edits per page.
211
        $pageCounts['edits_per_page'] = 0;
212
        if ($pageCounts['unique'] && $totalRevisions) {
213
            $pageCounts['edits_per_page'] = round($totalRevisions / $pageCounts['unique'], 3);
214
        }
215
216
        return $pageCounts;
217
    }
218
219
    /**
220
     * Get log totals for a user.
221
     * @param integer $userId The user ID.
222
     * @return integer[] Keys are log-action string, values are counts.
223
     */
224
    public function getLogCounts($userId)
225
    {
226
        $sql = "SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
227
            FROM ".$this->labsHelper->getTable('logging')."
228
            WHERE log_user = :userId
229
            GROUP BY log_type, log_action";
230
        $resultQuery = $this->replicas->prepare($sql);
231
        $resultQuery->bindParam('userId', $userId);
232
        $resultQuery->execute();
233
        $results = $resultQuery->fetchAll();
234
        $logCounts = array_combine(
235
            array_map(function ($e) {
236
                return $e['source'];
237
            }, $results),
238
            array_map(function ($e) {
239
                return $e['value'];
240
            }, $results)
241
        );
242
243
        // Make sure there is some value for each of the wanted counts.
244
        $requiredCounts = [
245
            'thanks-thank', 'review-approve', 'patrol-patrol','block-block', 'block-unblock',
246
            'protect-protect', 'protect-unprotect', 'delete-delete', 'delete-revision',
247
            'delete-restore', 'import-import', 'upload-upload', 'upload-overwrite',
248
        ];
249
        foreach ($requiredCounts as $req) {
250
            if (!isset($logCounts[$req])) {
251
                $logCounts[$req] = 0;
252
            }
253
        }
254
255
        // Merge approvals together.
256
        $logCounts['review-approve'] = $logCounts['review-approve'] +
257
            (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
258
            (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
259
            (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
260
261
        // Add Commons upload count, if applicable.
262
        $logCounts['files_uploaded_commons'] = 0;
263
        if ($this->labsHelper->isLabs()) {
264
            $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
265
                WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
266
            $resultQuery = $this->replicas->prepare($sql);
267
            $resultQuery->bindParam('userId', $userId);
268
            $resultQuery->execute();
269
            $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
270
        }
271
272
        return $logCounts;
273
    }
274
275
    /**
276
     * Get the given user's total edit counts per namespace.
277
     * @param integer $userId The ID of the user.
278
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
279
     */
280
    public function getNamespaceTotals($userId)
281
    {
282
        $sql = "SELECT page_namespace, count(rev_id) AS total
283
            FROM ".$this->labsHelper->getTable('revision') ." r
284
                JOIN ".$this->labsHelper->getTable('page')." p on r.rev_page = p.page_id
285
            WHERE r.rev_user = :id GROUP BY page_namespace";
286
        $resultQuery = $this->replicas->prepare($sql);
287
        $resultQuery->bindParam(":id", $userId);
288
        $resultQuery->execute();
289
        $results = $resultQuery->fetchAll();
290
        $namespaceTotals = array_combine(
291
            array_map(function ($e) {
292
                return $e['page_namespace'];
293
            }, $results),
294
            array_map(function ($e) {
295
                return $e['total'];
296
            }, $results)
297
        );
298
        return $namespaceTotals;
299
    }
300
301
    /**
302
     * Get this user's most recent 10 edits across all projects.
303
     * @param string $username The username.
304
     * @param integer $contribCount The number of items to return.
305
     * @param integer $days The number of days to search from each wiki.
306
     * @return string[]
307
     */
308
    public function getRecentGlobalContribs($username, $contribCount = 10, $days = 30)
309
    {
310
        $allRevisions = [];
311
        foreach ($this->labsHelper->allProjects() as $project) {
312
            $cacheKey = "globalcontribs.{$project['dbName']}.$username";
313
            if ($this->cacheHas($cacheKey)) {
314
                $revisions = $this->cacheGet($cacheKey);
315
            } else {
316
                $sql =
317
                    "SELECT rev_id, rev_comment, rev_timestamp, rev_minor_edit, rev_deleted, "
318
                    . "     rev_len, rev_parent_id, page_title "
319
                    . " FROM " . $this->labsHelper->getTable('revision', $project['dbName'])
320
                    . "    JOIN " . $this->labsHelper->getTable('page', $project['dbName'])
321
                    . "    ON (rev_page = page_id)"
322
                    . " WHERE rev_timestamp > NOW() - INTERVAL $days DAY AND rev_user_text LIKE :username"
323
                    . " ORDER BY rev_timestamp DESC"
324
                    . " LIMIT 10";
325
                $resultQuery = $this->replicas->prepare($sql);
326
                $resultQuery->bindParam(":username", $username);
327
                $resultQuery->execute();
328
                $revisions = $resultQuery->fetchAll();
329
                $this->cacheSave($cacheKey, $revisions, 'PT15M');
0 ignored issues
show
Documentation introduced by
$revisions is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
330
            }
331
            if (count($revisions) === 0) {
332
                continue;
333
            }
334
            $revsWithProject = array_map(
335
                function (&$item) use ($project) {
336
                    $item['project_name'] = $project['name'];
337
                    $item['project_url'] = $project['url'];
338
                    $item['project_db_name'] = $project['dbName'];
339
                    return $item;
340
                },
341
                $revisions
342
            );
343
            $allRevisions = array_merge($allRevisions, $revsWithProject);
344
        }
345
        usort($allRevisions, function ($a, $b) {
346
            return $b['rev_timestamp'] - $a['rev_timestamp'];
347
        });
348
        return array_slice($allRevisions, 0, $contribCount);
349
    }
350
351
    /**
352
     * Get data for a bar chart of monthly edit totals per namespace.
353
     * @param string $username The username.
354
     * @return string[]
355
     */
356
    public function getMonthCounts($username)
357
    {
358
        $cacheKey = "monthcounts.$username";
359
        if ($this->cacheHas($cacheKey)) {
360
            return $this->cacheGet($cacheKey);
361
        }
362
363
        $sql = "SELECT "
364
            . "     YEAR(rev_timestamp) AS `year`,"
365
            . "     MONTH(rev_timestamp) AS `month`,"
366
            . "     page_namespace,"
367
            . "     COUNT(rev_id) AS `count` "
368
            . " 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
Documentation introduced by
$out is of type array<string,array|integ...,{"min_year":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
406
        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 AppBundle\Helper\EditCounterHelper::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...
407
    }
408
409
    /**
410
     * Get yearly edit totals for this user, grouped by namespace.
411
     * @param string $username
412
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
413
     */
414
    public function getYearlyTotalsByNamespace($username)
415
    {
416
        $cacheKey = "yearcounts.$username";
417
        if ($this->cacheHas($cacheKey)) {
418
            return $this->cacheGet($cacheKey);
419
        }
420
421
        $sql = "SELECT "
422
            . "     SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4) AS `year`,"
423
            . "     page_namespace,"
424
            . "     COUNT(rev_id) AS `count` "
425
            . " FROM " . $this->labsHelper->getTable('revision')
426
            . "    JOIN " . $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
Documentation introduced by
$out is of type array<string,array,{"yea...ray","totals":"array"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
448
        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 AppBundle\Helper\EditCou...YearlyTotalsByNamespace 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...
449
    }
450
451
    /**
452
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
453
     * @param string $username The user's username.
454
     * @return string[]
455
     */
456
    public function getTimeCard($username)
457
    {
458
        $cacheKey = "timecard.$username";
459
        if ($this->cacheHas($cacheKey)) {
460
            return $this->cacheGet($cacheKey);
461
        }
462
463
        $hourInterval = 2;
464
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval)*$hourInterval";
465
        $sql = "SELECT "
466
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
467
            . "     $xCalc AS `x`, "
468
            . "     COUNT(rev_id) AS `r` "
469
            . " FROM " . $this->labsHelper->getTable('revision')
470
            . " WHERE rev_user_text = :username"
471
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc "
472
            . " ";
473
        $resultQuery = $this->replicas->prepare($sql);
474
        $resultQuery->bindParam(":username", $username);
475
        $resultQuery->execute();
476
        $totals = $resultQuery->fetchAll();
477
        // Scale the radii: get the max, then scale each radius.
478
        // This looks inefficient, but there's a max of 72 elements in this array.
479
        $max = 0;
480
        foreach ($totals as $total) {
481
            $max = max($max, $total['r']);
482
        }
483
        foreach ($totals as &$total) {
484
            $total['r'] = round($total['r'] / $max * 100);
485
        }
486
        $this->cacheSave($cacheKey, $totals, 'PT10M');
0 ignored issues
show
Documentation introduced by
$totals is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
487
        return $totals;
488
    }
489
}
490