Test Failed
Push — dependency-injection ( 7565fa )
by MusikAnimal
07:05
created

TopEditsRepository::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 8
dl 0
loc 13
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Model\Edit;
8
use App\Model\Page;
9
use App\Model\Project;
10
use App\Model\User;
11
use GuzzleHttp\Client;
12
use PDO;
13
use Psr\Cache\CacheItemPoolInterface;
14
use Psr\Log\LoggerInterface;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use Wikimedia\IPUtils;
17
18
/**
19
 * TopEditsRepository is responsible for retrieving data from the database
20
 * about the top-edited pages of a user. It doesn't do any post-processing
21
 * of that information.
22
 * @codeCoverageIgnore
23
 */
24
class TopEditsRepository extends UserRepository
25
{
26
    protected EditRepository $editRepo;
27
    protected UserRepository $userRepo;
28
29
    public function __construct(
30
        ContainerInterface $container,
31
        CacheItemPoolInterface $cache,
32
        Client $guzzle,
33
        LoggerInterface $logger,
34
        bool $isWMF,
35
        int $queryTimeout,
36
        EditRepository $editRepo,
37
        UserRepository $userRepo
38
    ) {
39
        $this->editRepo = $editRepo;
40
        $this->userRepo = $userRepo;
41
        parent::__construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout);
0 ignored issues
show
Bug introduced by
The call to App\Repository\UserRepository::__construct() has too few arguments starting with projectRepo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

41
        parent::/** @scrutinizer ignore-call */ 
42
                __construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
42
    }
43
44
    /**
45
     * Factory to instantiate a new Edit for the given revision.
46
     * @param Page $page
47
     * @param array $revision
48
     * @return Edit
49
     */
50
    public function getEdit(Page $page, array $revision): Edit
51
    {
52
        return new Edit($this->editRepo, $this->userRepo, $page, $revision);
53
    }
54
55
    /**
56
     * Get the top edits by a user in a single namespace.
57
     * @param Project $project
58
     * @param User $user
59
     * @param int $namespace Namespace ID.
60
     * @param int|false $start Start date as Unix timestamp.
61
     * @param int|false $end End date as Unix timestamp.
62
     * @param int $limit Number of edits to fetch.
63
     * @param int $pagination Which page of results to return.
64
     * @return string[] page_namespace, page_title, page_is_redirect,
65
     *   count (number of edits), assessment (page assessment).
66
     */
67
    public function getTopEditsNamespace(
68
        Project $project,
69
        User $user,
70
        int $namespace = 0,
71
        $start = false,
72
        $end = false,
73
        int $limit = 1000,
74
        int $pagination = 0
75
    ): array {
76
        // Set up cache.
77
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_ns');
78
        if ($this->cache->hasItem($cacheKey)) {
79
            return $this->cache->getItem($cacheKey)->get();
80
        }
81
82
        $revDateConditions = $this->getDateConditions($start, $end);
83
        $pageTable = $project->getTableName('page');
84
        $revisionTable = $project->getTableName('revision');
85
86
        $hasPageAssessments = $this->isWMF && $project->hasPageAssessments() && 0 === $namespace;
87
        $paTable = $project->getTableName('page_assessments');
88
        $paSelect = $hasPageAssessments
89
            ?  ", (
90
                    SELECT pa_class
91
                    FROM $paTable
92
                    WHERE pa_page_id = page_id
93
                    AND pa_class != 'Unknown'
94
                    LIMIT 1
95
                ) AS pa_class"
96
            : '';
97
98
        $ipcJoin = '';
99
        $whereClause = 'rev_actor = :actorId';
100
        $params = [];
101
        if ($user->isIpRange()) {
102
            $ipcTable = $project->getTableName('ip_changes');
103
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
104
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
105
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
106
        }
107
108
        $offset = $pagination * $limit;
109
        $sql = "SELECT page_namespace, page_title, page_is_redirect, COUNT(page_title) AS count
110
                    $paSelect
111
                FROM $pageTable
112
                JOIN $revisionTable ON page_id = rev_page
113
                $ipcJoin
114
                WHERE $whereClause
115
                AND page_namespace = :namespace
116
                $revDateConditions
117
                GROUP BY page_namespace, page_title
118
                ORDER BY count DESC
119
                LIMIT $limit
120
                OFFSET $offset";
121
122
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
123
        $result = $resultQuery->fetchAllAssociative();
124
125
        // Cache and return.
126
        return $this->setCache($cacheKey, $result);
127
    }
128
129
    /**
130
     * Count the number of edits in the given namespace.
131
     * @param Project $project
132
     * @param User $user
133
     * @param int|string $namespace
134
     * @param int|false $start Start date as Unix timestamp.
135
     * @param int|false $end End date as Unix timestamp.
136
     * @return mixed
137
     */
138
    public function countEditsNamespace(Project $project, User $user, $namespace, $start = false, $end = false)
139
    {
140
        // Set up cache.
141
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_count_ns');
142
        if ($this->cache->hasItem($cacheKey)) {
143
            return $this->cache->getItem($cacheKey)->get();
144
        }
145
146
        $revDateConditions = $this->getDateConditions($start, $end);
147
        $pageTable = $project->getTableName('page');
148
        $revisionTable = $project->getTableName('revision');
149
150
        $ipcJoin = '';
151
        $whereClause = 'rev_actor = :actorId';
152
        $params = [];
153
        if ($user->isIpRange()) {
154
            $ipcTable = $project->getTableName('ip_changes');
155
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
156
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
157
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
158
        }
159
160
        $sql = "SELECT COUNT(DISTINCT page_id)
161
                FROM $pageTable
162
                JOIN $revisionTable ON page_id = rev_page
163
                $ipcJoin
164
                WHERE $whereClause
165
                AND page_namespace = :namespace
166
                $revDateConditions";
167
168
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
169
170
        // Cache and return.
171
        return $this->setCache($cacheKey, $resultQuery->fetchOne());
172
    }
173
174
    /**
175
     * Get the top edits by a user across all namespaces.
176
     * @param Project $project
177
     * @param User $user
178
     * @param int|false $start Start date as Unix timestamp.
179
     * @param int|false $end End date as Unix timestamp.
180
     * @param int $limit Number of edits to fetch.
181
     * @return string[] page_namespace, page_title, page_is_redirect,
182
     *   count (number of edits), assessment (page assessment).
183
     */
184
    public function getTopEditsAllNamespaces(
185
        Project $project,
186
        User $user,
187
        $start = false,
188
        $end = false,
189
        int $limit = 10
190
    ): array {
191
        // Set up cache.
192
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_all');
193
        if ($this->cache->hasItem($cacheKey)) {
194
            return $this->cache->getItem($cacheKey)->get();
195
        }
196
197
        $revDateConditions = $this->getDateConditions($start, $end);
198
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
199
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
200
        $hasPageAssessments = $this->isWMF && $project->hasPageAssessments();
201
        $pageAssessmentsTable = $this->getTableName($project->getDatabaseName(), 'page_assessments');
202
        $paSelect = $hasPageAssessments
203
            ?  ", (
204
                    SELECT pa_class
205
                    FROM $pageAssessmentsTable
206
                    WHERE pa_page_id = e.page_id
207
                    LIMIT 1
208
                ) AS pa_class"
209
            : ', NULL as pa_class';
210
211
        $ipcJoin = '';
212
        $whereClause = 'rev_actor = :actorId';
213
        $params = [];
214
        if ($user->isIpRange()) {
215
            $ipcTable = $project->getTableName('ip_changes');
216
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
217
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
218
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
219
        }
220
221
        $sql = "SELECT c.page_namespace, e.page_title, c.page_is_redirect, c.count $paSelect
222
                FROM
223
                (
224
                    SELECT b.page_namespace, b.page_is_redirect, b.rev_page, b.count
225
                        ,@rn := if(@ns = b.page_namespace, @rn + 1, 1) AS row_number
226
                        ,@ns := b.page_namespace AS dummy
227
                    FROM
228
                    (
229
                        SELECT page_namespace, page_is_redirect, rev_page, count(rev_page) AS count
230
                        FROM $revisionTable
231
                        $ipcJoin
232
                        JOIN $pageTable ON page_id = rev_page
233
                        WHERE $whereClause
234
                        $revDateConditions
235
                        GROUP BY page_namespace, rev_page
236
                    ) AS b
237
                    JOIN (SELECT @ns := NULL, @rn := 0) AS vars
238
                    ORDER BY b.page_namespace ASC, b.count DESC
239
                ) AS c
240
                JOIN $pageTable e ON e.page_id = c.rev_page
241
                WHERE c.row_number <= $limit";
242
        $resultQuery = $this->executeQuery($sql, $project, $user, 'all', $params);
243
        $result = $resultQuery->fetchAllAssociative();
244
245
        // Cache and return.
246
        return $this->setCache($cacheKey, $result);
247
    }
248
249
    /**
250
     * Get the top edits by a user to a single page.
251
     * @param Page $page
252
     * @param User $user
253
     * @param int|false $start Start date as Unix timestamp.
254
     * @param int|false $end End date as Unix timestamp.
255
     * @return string[][] Each row with keys 'id', 'timestamp', 'minor', 'length',
256
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
257
     */
258
    public function getTopEditsPage(Page $page, User $user, $start = false, $end = false): array
259
    {
260
        // Set up cache.
261
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_page');
262
        if ($this->cache->hasItem($cacheKey)) {
263
            return $this->cache->getItem($cacheKey)->get();
264
        }
265
266
        $results = $this->queryTopEditsPage($page, $user, $start, $end, true);
267
268
        // Now we need to get the most recent revision, since the childrevs stuff excludes it.
269
        $lastRev = $this->queryTopEditsPage($page, $user, $start, $end, false);
270
        if (empty($results) || $lastRev[0]['id'] !== $results[0]['id']) {
271
            $results = array_merge($lastRev, $results);
272
        }
273
274
        // Cache and return.
275
        return $this->setCache($cacheKey, $results);
276
    }
277
278
    /**
279
     * The actual query to get the top edits by the user to the page.
280
     * Because of the way the main query works, we aren't given the most recent revision,
281
     * so we have to call this twice, once with $childRevs set to true and once with false.
282
     * @param Page $page
283
     * @param User $user
284
     * @param int|false $start Start date as Unix timestamp.
285
     * @param int|false $end End date as Unix timestamp.
286
     * @param boolean $childRevs Whether to include child revisions.
287
     * @return array Each row with keys 'id', 'timestamp', 'minor', 'length',
288
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
289
     */
290
    private function queryTopEditsPage(
291
        Page $page,
292
        User $user,
293
        $start = false,
294
        $end = false,
295
        bool $childRevs = false
296
    ): array {
297
        $project = $page->getProject();
298
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
299
        $revTable = $project->getTableName('revision');
300
        $commentTable = $project->getTableName('comment');
301
        $tagTable = $project->getTableName('change_tag');
302
        $tagDefTable = $project->getTableName('change_tag_def');
303
304
        if ($childRevs) {
305
            $childSelect = ", (
306
                    CASE WHEN
307
                        childrevs.rev_sha1 = parentrevs.rev_sha1
308
                        OR (
309
                            SELECT 1
310
                            FROM $tagTable
311
                            WHERE ct_rev_id = revs.rev_id
312
                            AND ct_tag_id = (
313
                                SELECT ctd_id
314
                                FROM $tagDefTable
315
                                WHERE ctd_name = 'mw-reverted'
316
                            )
317
                        )
318
                    THEN 1
319
                    ELSE 0
320
                    END
321
                ) AS `reverted`,
322
                childcomments.comment_text AS `parent_comment`";
323
            $childJoin = "LEFT JOIN $revTable AS childrevs ON (revs.rev_id = childrevs.rev_parent_id)
324
                LEFT OUTER JOIN $commentTable AS childcomments
325
                ON (childrevs.rev_comment_id = childcomments.comment_id)";
326
            $childWhere = 'AND childrevs.rev_page = :pageid';
327
            $childLimit = '';
328
        } else {
329
            $childSelect = ', "" AS parent_comment, 0 AS reverted';
330
            $childJoin = '';
331
            $childWhere = '';
332
            $childLimit = 'LIMIT 1';
333
        }
334
335
        $userId = $this->getProjectsConnection($project)->quote($user->getId($page->getProject()), PDO::PARAM_STR);
336
        $username = $this->getProjectsConnection($project)->quote($user->getUsername(), PDO::PARAM_STR);
337
338
        // IP range handling.
339
        $ipcJoin = '';
340
        $whereClause = 'revs.rev_actor = :actorId';
341
        $params = ['pageid' => $page->getId()];
342
        if ($user->isIpRange()) {
343
            $ipcTable = $project->getTableName('ip_changes');
344
            $ipcJoin = "JOIN $ipcTable ON revs.rev_id = ipc_rev_id";
345
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
346
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
347
        }
348
349
        $sql = "SELECT * FROM (
350
                    SELECT
351
                        revs.rev_id AS id,
352
                        revs.rev_timestamp AS timestamp,
353
                        revs.rev_minor_edit AS minor,
354
                        revs.rev_len AS length,
355
                        (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
356
                        $userId AS user_id,
357
                        $username AS username,
358
                        comments.comment_text AS `comment`
359
                        $childSelect
360
                    FROM $revTable AS revs
361
                    LEFT JOIN $revTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
362
                    $ipcJoin
363
                    LEFT OUTER JOIN $commentTable AS comments ON (revs.rev_comment_id = comments.comment_id)
364
                    $childJoin
365
                    WHERE $whereClause
366
                    $revDateConditions
367
                    AND revs.rev_page = :pageid
368
                    $childWhere
369
                ) a
370
                ORDER BY timestamp DESC
371
                $childLimit";
372
373
        $resultQuery = $this->executeQuery($sql, $project, $user, null, $params);
374
        return $resultQuery->fetchAllAssociative();
375
    }
376
}
377