Passed
Push — master ( 08c602...04f48f )
by MusikAnimal
11:06
created

GlobalContribsRepository::getRevisions()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 65
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 32
c 2
b 0
f 0
nc 13
nop 7
dl 0
loc 65
rs 8.7857

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
declare(strict_types = 1);
3
4
namespace AppBundle\Repository;
5
6
use AppBundle\Model\Project;
7
use AppBundle\Model\User;
8
use Symfony\Component\DependencyInjection\ContainerInterface;
9
10
/**
11
 * A GlobalContribsRepository is responsible for retrieving information from the database for the GlobalContribs tool.
12
 * @codeCoverageIgnore
13
 */
14
class GlobalContribsRepository extends Repository
15
{
16
    /** @var Project CentralAuth project (meta.wikimedia for WMF installation). */
17
    protected $caProject;
18
19
    /**
20
     * Create Project and ProjectRepository once we have the container.
21
     * @param ContainerInterface $container
22
     */
23
    public function setContainer(ContainerInterface $container): void
24
    {
25
        parent::setContainer($container);
26
27
        $this->caProject = ProjectRepository::getProject(
28
            $this->container->getParameter('central_auth_project'),
29
            $this->container
30
        );
31
        $this->caProject->getRepository()
32
            ->setContainer($this->container);
33
    }
34
35
    /**
36
     * Get a user's edit count for each project.
37
     * @see GlobalContribsRepository::globalEditCountsFromCentralAuth()
38
     * @see GlobalContribsRepository::globalEditCountsFromDatabases()
39
     * @param User $user The user.
40
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int). Null if anon (too slow).
41
     */
42
    public function globalEditCounts(User $user): ?array
43
    {
44
        if ($user->isAnon()) {
45
            return null;
46
        }
47
48
        // Get the edit counts from CentralAuth or database.
49
        $editCounts = $this->globalEditCountsFromCentralAuth($user);
50
51
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
52
        $this->caProject->getRepository()->getAll();
0 ignored issues
show
Bug introduced by
The method getAll() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\ProjectRepository. ( Ignorable by Annotation )

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

52
        $this->caProject->getRepository()->/** @scrutinizer ignore-call */ getAll();
Loading history...
53
54
        // Compile the output.
55
        $out = [];
56
        foreach ($editCounts as $editCount) {
57
            $out[] = [
58
                'dbName' => $editCount['dbName'],
59
                'total' => $editCount['total'],
60
                'project' => ProjectRepository::getProject($editCount['dbName'], $this->container),
61
            ];
62
        }
63
        return $out;
64
    }
65
66
    /**
67
     * Get a user's total edit count on one or more project.
68
     * Requires the CentralAuth extension to be installed on the project.
69
     * @param User $user The user.
70
     * @return mixed[]|false Elements are arrays with 'dbName' (string), and 'total' (int). False for logged out users.
71
     */
72
    protected function globalEditCountsFromCentralAuth(User $user)
73
    {
74
        if (true === $user->isAnon()) {
75
            return false;
76
        }
77
78
        // Set up cache.
79
        $cacheKey = $this->getCacheKey(func_get_args(), 'gc_globaleditcounts');
80
        if ($this->cache->hasItem($cacheKey)) {
81
            return $this->cache->getItem($cacheKey)->get();
82
        }
83
84
        $params = [
85
            'meta' => 'globaluserinfo',
86
            'guiprop' => 'editcount|merged',
87
            'guiuser' => $user->getUsername(),
88
        ];
89
        $result = $this->executeApiRequest($this->caProject, $params);
90
        if (!isset($result['query']['globaluserinfo']['merged'])) {
91
            return [];
92
        }
93
        $out = [];
94
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
95
            $out[] = [
96
                'dbName' => $result['wiki'],
97
                'total' => $result['editcount'],
98
            ];
99
        }
100
101
        // Cache and return.
102
        return $this->setCache($cacheKey, $out);
103
    }
104
105
    /**
106
     * Loop through the given dbNames and create Project objects for each.
107
     * @param array $dbNames
108
     * @return Project[] Keyed by database name.
109
     */
110
    private function formatProjects(array $dbNames): array
111
    {
112
        $projects = [];
113
114
        foreach ($dbNames as $dbName) {
115
            $projects[$dbName] = ProjectRepository::getProject($dbName, $this->container);
116
        }
117
118
        return $projects;
119
    }
120
121
    /**
122
     * Get all Projects on which the user has made at least one edit.
123
     * @param User $user
124
     * @return Project[]
125
     */
126
    public function getProjectsWithEdits(User $user): array
127
    {
128
        if ($user->isAnon()) {
129
            $dbNames = array_keys($this->getDbNamesAndActorIds($user));
130
        } else {
131
            $dbNames = [];
132
133
            foreach ($this->globalEditCountsFromCentralAuth($user) as $projectMeta) {
134
                if ($projectMeta['total'] > 0) {
135
                    $dbNames[] = $projectMeta['dbName'];
136
                }
137
            }
138
        }
139
140
        return $this->formatProjects($dbNames);
141
    }
142
143
    /**
144
     * Get projects that the user has made at least one edit on, and the associated actor ID.
145
     * @param User $user
146
     * @param string[] $dbNames Loop over these projects instead of all of them.
147
     * @return mixed[] Keys are database names, values are actor IDs.
148
     */
149
    public function getDbNamesAndActorIds(User $user, ?array $dbNames = null): array
150
    {
151
        // Check cache.
152
        $cacheKey = $this->getCacheKey(func_get_args(), 'gc_db_names_actor_ids');
153
        if ($this->cache->hasItem($cacheKey)) {
154
            return $this->cache->getItem($cacheKey)->get();
155
        }
156
157
        if (!$dbNames) {
158
            $dbNames = array_column($this->caProject->getRepository()->getAll(), 'dbName');
159
        }
160
161
        $sqlParts = [];
162
163
        foreach ($dbNames as $dbName) {
164
            // actor_revision table only includes users who have made at least one edit.
165
            $actorTable = $this->getTableName($dbName, 'actor', 'revision');
166
            $sqlParts []= "SELECT '$dbName' AS `dbName`, actor_id
167
                           FROM $actorTable WHERE actor_name = :actor";
168
        }
169
170
        $sql = implode(' UNION ', $sqlParts);
171
        $resultQuery = $this->executeProjectsQuery($sql, [
172
            'actor' => $user->getUsername(),
173
        ]);
174
175
        $actorIds = [];
176
        while ($row = $resultQuery->fetch()) {
177
            $actorIds[$row['dbName']] = (int)$row['actor_id'];
178
        }
179
180
        return $this->setCache($cacheKey, $actorIds);
181
    }
182
183
    /**
184
     * Get revisions by this user across the given Projects.
185
     * @param string[] $dbNames Database names of projects to iterate over.
186
     * @param User $user The user.
187
     * @param int|string $namespace Namespace ID or 'all' for all namespaces.
188
     * @param string $start Start date in a format accepted by strtotime().
189
     * @param string $end Start date in a format accepted by strtotime().
190
     * @param int $limit The maximum number of revisions to fetch from each project.
191
     * @param int $offset Offset results by this number of rows.
192
     * @return array|mixed
193
     */
194
    public function getRevisions(
195
        array $dbNames,
196
        User $user,
197
        $namespace = 'all',
198
        $start = '',
199
        $end = '',
200
        int $limit = 30,
201
        int $offset = 0
202
    ) {
203
        // Check cache.
204
        $cacheKey = $this->getCacheKey(func_get_args(), 'gc_revisions');
205
        if ($this->cache->hasItem($cacheKey)) {
206
            return $this->cache->getItem($cacheKey)->get();
207
        }
208
209
        $username = $this->getProjectsConnection()->quote($user->getUsername(), \PDO::PARAM_STR);
210
        $actorIds = $this->getDbNamesAndActorIds($user, $dbNames);
211
        $namespaceCond = 'all' === $namespace
212
            ? ''
213
            : 'AND page_namespace = '.(int)$namespace;
214
        $revDateConditions = $this->getDateConditions($start, $end, 'revs.');
215
216
        // Assemble queries.
217
        $queries = [];
218
        $projectRepo = $this->caProject->getRepository();
219
        foreach ($dbNames as $dbName) {
220
            if (isset($actorIds[$dbName])) {
221
                $revisionTable = $projectRepo->getTableName($dbName, 'revision');
222
                $pageTable = $projectRepo->getTableName($dbName, 'page');
223
                $commentTable = $projectRepo->getTableName($dbName, 'comment', 'revision');
224
    
225
                $sql = "SELECT
226
                        '$dbName' AS dbName,
227
                        revs.rev_id AS id,
228
                        revs.rev_timestamp AS timestamp,
229
                        UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
230
                        revs.rev_minor_edit AS minor,
231
                        revs.rev_deleted AS deleted,
232
                        revs.rev_len AS length,
233
                        (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
234
                        revs.rev_parent_id AS parent_id,
235
                        $username AS username,
236
                        page.page_title,
237
                        page.page_namespace,
238
                        comment_text AS `comment`
239
                    FROM $revisionTable AS revs
240
                        JOIN $pageTable AS page ON (rev_page = page_id)
241
                        LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
242
                        LEFT OUTER JOIN $commentTable ON revs.rev_comment_id = comment_id
243
                    WHERE revs.rev_actor = ".$actorIds[$dbName]."
244
                        $namespaceCond
245
                        $revDateConditions";
246
                $queries[] = $sql;
247
            }
248
        }
249
        $sql = "SELECT * FROM ((\n" . join("\n) UNION (\n", $queries) . ")) a ORDER BY timestamp DESC LIMIT $limit";
250
251
        if (is_numeric($offset)) {
0 ignored issues
show
introduced by
The condition is_numeric($offset) is always true.
Loading history...
252
            $sql .= " OFFSET $offset";
253
        }
254
255
        $revisions = $this->executeProjectsQuery($sql)->fetchAll();
256
257
        // Cache and return.
258
        return $this->setCache($cacheKey, $revisions);
259
    }
260
}
261