Passed
Pull Request — master (#158)
by MusikAnimal
04:58
created

TopEdits::getNumTopEdits()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the TopEdits class.
4
 */
5
6
namespace Xtools;
7
8
use DateTime;
9
10
/**
11
 * TopEdits returns the top-edited pages by a user.
12
 */
13
class TopEdits extends Model
14
{
15
    /** @var Project The project. */
16
    protected $project;
17
18
    /** @var User The user. */
19
    protected $user;
20
21
    /** @var Page The page (if applicable). */
22
    protected $page;
23
24
    /** @var string[]|Edit[] Top edits, either to a page or across namespaces. */
25
    protected $topEdits = [];
26
27
    /** @var int Number of rows to fetch. */
28
    protected $limit;
29
30
    /** @var int Which namespace we are querying for. */
31
    protected $namespace;
32
33
    /** @var int Number of bytes added across all top edits. */
34
    protected $totalAdded = 0;
35
36
    /** @var int Number of bytes removed across all top edits. */
37
    protected $totalRemoved = 0;
38
39
    /** @var int Number of top edits marked as minor. */
40
    protected $totalMinor = 0;
41
42
    /** @var int Number of automated top edits. */
43
    protected $totalAutomated = 0;
44
45
    /** @var int Number of reverted top edits. */
46
    protected $totalReverted = 0;
47
48
    const DEFAULT_LIMIT_SINGLE_NAMESPACE = 100;
49
    const DEFAULT_LIMIT_ALL_NAMESPACES = 20;
50
51
    /**
52
     * TopEdits constructor.
53
     * @param Project $project
54
     * @param User $user
55
     * @param Page $page
56
     * @param string|int Namespace ID or 'all'.
0 ignored issues
show
Bug introduced by
The type Xtools\Namespace was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
57
     * @param int $limit Number of rows to fetch. This defaults to
58
     *   DEFAULT_LIMIT_SINGLE_NAMESPACE if $this->namespace is a single namespace (int),
59
     *   and DEFAULT_LIMIT_ALL_NAMESPACES if $this->namespace is 'all'.
60
     */
61 4
    public function __construct(Project $project, User $user, Page $page = null, $namespace = 0, $limit = null)
62
    {
63 4
        $this->project = $project;
64 4
        $this->user = $user;
65 4
        $this->page = $page;
66 4
        $this->namespace = $namespace === 'all' ? 'all' : (int)$namespace;
0 ignored issues
show
Documentation Bug introduced by
It seems like $namespace === 'all' ? 'all' : (int)$namespace can also be of type string. However, the property $namespace is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
67
68 4
        if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
69 3
            $this->limit = $limit;
70
        } else {
71 2
            $this->limit = $this->namespace === 'all'
72 1
                ? self::DEFAULT_LIMIT_ALL_NAMESPACES
73 2
                : self::DEFAULT_LIMIT_SINGLE_NAMESPACE;
74
        }
75 4
    }
76
77
    /**
78
     * Get the limit set on number of rows to fetch.
79
     * @return int
80
     */
81 1
    public function getLimit()
82
    {
83 1
        return $this->limit;
84
    }
85
86
    /**
87
     * Get the namespace set on the instance.
88
     * @return int|string Namespace ID or 'all'.
89
     */
90 1
    public function getNamespace()
91
    {
92 1
        return $this->namespace;
93
    }
94
95
    /**
96
     * Get total number of bytes added.
97
     * @return int
98
     */
99 1
    public function getTotalAdded()
100
    {
101 1
        return $this->totalAdded;
102
    }
103
104
    /**
105
     * Get total number of bytes removed.
106
     * @return int
107
     */
108 1
    public function getTotalRemoved()
109
    {
110 1
        return $this->totalRemoved;
111
    }
112
113
    /**
114
     * Get total number of edits marked as minor.
115
     * @return int
116
     */
117 1
    public function getTotalMinor()
118
    {
119 1
        return $this->totalMinor;
120
    }
121
122
    /**
123
     * Get total number of automated edits.
124
     * @return int
125
     */
126 1
    public function getTotalAutomated()
127
    {
128 1
        return $this->totalAutomated;
129
    }
130
131
    /**
132
     * Get total number of edits that were reverted.
133
     * @return int
134
     */
135 1
    public function getTotalReverted()
136
    {
137 1
        return $this->totalReverted;
138
    }
139
140
    /**
141
     * Get the top edits data.
142
     * @return string[]|Edit[]
143
     */
144 3
    public function getTopEdits()
145
    {
146 3
        return $this->topEdits;
147
    }
148
149
    /**
150
     * Get the total number of top edits.
151
     * @return int
152
     */
153 1
    public function getNumTopEdits()
154
    {
155 1
        return count($this->topEdits);
156
    }
157
158
    /**
159
     * Get the averate time between edits (in days).
160
     * @return double
161
     */
162 1
    public function getAtbe()
163
    {
164 1
        $firstDateTime = $this->topEdits[0]->getTimestamp();
165 1
        $lastDateTime = end($this->topEdits)->getTimestamp();
166 1
        $secs = $firstDateTime->getTimestamp() - $lastDateTime->getTimestamp();
167 1
        $days = $secs / (60 * 60 * 24);
168 1
        return $days / count($this->topEdits);
169
    }
170
171
    /**
172
     * Fetch and store all the data we need to show the TopEdits view.
173
     * This is the public method that should be called before using
174
     * the getter methods.
175
     */
176 3
    public function prepareData()
177
    {
178 3
        if (isset($this->page)) {
179 1
            $this->topEdits = $this->getTopEditsPage();
180
        } else {
181 2
            $this->topEdits = $this->getTopEditsNamespace();
182
        }
183 3
    }
184
185
    /**
186
     * Get the top edits by a user in the given namespace, or 'all' namespaces.
187
     * @return string[] Results keyed by namespace.
188
     */
189 2
    private function getTopEditsNamespace()
190
    {
191 2
        if ($this->namespace === 'all') {
0 ignored issues
show
introduced by
The condition $this->namespace === 'all' can never be true.
Loading history...
192 1
            $pages = $this->getRepository()->getTopEditsAllNamespaces(
193 1
                $this->project,
194 1
                $this->user,
195 1
                $this->limit
196
            );
197
        } else {
198 1
            $pages = $this->getRepository()->getTopEditsNamespace(
199 1
                $this->project,
200 1
                $this->user,
201 1
                $this->namespace,
202 1
                $this->limit
203
            );
204
        }
205
206 2
        return $this->formatTopPagesNamespace($pages);
207
    }
208
209
    /**
210
     * Get the top edits to the given page.
211
     * @return Edit[]
212
     */
213 1
    private function getTopEditsPage()
214
    {
215 1
        $revs = $this->getRepository()->getTopEditsPage(
216 1
            $this->page,
217 1
            $this->user
218
        );
219
220 1
        return $this->formatTopEditsPage($revs);
221
    }
222
223
    /**
224
     * Format the results for top edits to a single page. This method also computes
225
     * totals for added/removed text, automated and reverted edits.
226
     * @param  string[] $revs As returned by TopEditsRepository::getTopEditsPage.
227
     * @return Edit[]
228
     */
229 1
    private function formatTopEditsPage($revs)
230
    {
231 1
        $edits = [];
232
233 1
        $aeh = $this->getRepository()
234 1
            ->getContainer()
235 1
            ->get('app.automated_edits_helper');
236
237 1
        foreach ($revs as $revision) {
238
            // Check if the edit was reverted based on the edit summary of the following edit.
239
            // If so, update $revision so that when an Edit is instantiated, it will
240
            // have the 'reverted' option set.
241 1
            if ($aeh->isRevert($revision['parent_comment'], $this->project->getDomain())) {
242 1
                $revision['reverted'] = 1;
243
            }
244
245 1
            $edit = $this->getEditAndIncrementCounts($revision);
0 ignored issues
show
Bug introduced by
$revision of type string is incompatible with the type string[] expected by parameter $revision of Xtools\TopEdits::getEditAndIncrementCounts(). ( Ignorable by Annotation )

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

245
            $edit = $this->getEditAndIncrementCounts(/** @scrutinizer ignore-type */ $revision);
Loading history...
246
247 1
            $edits[] = $edit;
248
        }
249
250 1
        return $edits;
251
    }
252
253
    /**
254
     * Create an Edit instance for the given revision, and increment running totals.
255
     * This is used by self::formatTopEditsPage().
256
     * @param  string[] $revision Revision row as retrieved from the database.
257
     * @return Edit
258
     */
259 1
    private function getEditAndIncrementCounts($revision)
260
    {
261 1
        $edit = new Edit($this->page, $revision);
262
263 1
        if ($edit->isAutomated($this->getRepository()->getContainer())) {
264 1
            $this->totalAutomated++;
265
        }
266
267 1
        if ($edit->isMinor()) {
268 1
            $this->totalMinor++;
269
        }
270
271 1
        if ($edit->isReverted()) {
272 1
            $this->totalReverted++;
273
        } else {
274
            // Length changes don't count if they were reverted.
275 1
            if ($revision['length_change'] > 0) {
276 1
                $this->totalAdded += $revision['length_change'];
277
            } else {
278 1
                $this->totalRemoved += $revision['length_change'];
279
            }
280
        }
281
282 1
        return $edit;
283
    }
284
285
    /**
286
     * Format the results returned from the database.
287
     * @param  string[] $pages As returned by TopEditsRepository::getTopEditsNamespace
288
     *                         or TopEditsRepository::getTopEditsAllNamespaces.
289
     * @return string[] Same as input but with 'displaytitle' and 'page_title_ns'.
290
     */
291 2
    private function formatTopPagesNamespace($pages)
292
    {
293
        /** @var string[] The top edited pages, keyed by namespace ID. */
294 2
        $topEditedPages = [];
295
296
        /** @var string[] Display titles of the pages, which need to be fetched ahead of time. */
297 2
        $displayTitles = $this->getDisplayTitles($pages);
298
299 2
        foreach ($pages as $page) {
300 2
            $nsId = (int) $page['page_namespace'];
301 2
            $nsTitle = $nsId > 0 ? $this->project->getNamespaces()[$page['page_namespace']] . ':' : '';
302 2
            $pageTitle = $nsTitle . $page['page_title'];
303 2
            $page['displaytitle'] = $displayTitles[$pageTitle];
304
305
            // $page['page_title'] is retained without the namespace
306
            //  so we can link to TopEdits for that page.
307 2
            $page['page_title_ns'] = $pageTitle;
308
309 2
            if (isset($topEditedPages[$nsId])) {
310 2
                $topEditedPages[$nsId][] = $page;
311
            } else {
312 2
                $topEditedPages[$nsId] = [$page];
313
            }
314
        }
315
316 2
        return $topEditedPages;
317
    }
318
319
    /**
320
     * Get the display titles of the given pages.
321
     * @param  string[] $topPages As returned by $this->getRepository()->getTopEdits()
322
     * @return string[] Keys are the original supplied titles, and values are the display titles.
323
     */
324 2
    private function getDisplayTitles($topPages)
325
    {
326 2
        $namespaces = $this->project->getNamespaces();
327
328
        // First extract page titles including namespace.
329 2
        $pageTitles = array_map(function ($page) use ($namespaces) {
330
            // If non-mainspace, prepend namespace to the titles.
331 2
            $ns = $page['page_namespace'];
332 2
            $nsTitle = $ns > 0 ? $namespaces[$page['page_namespace']] . ':' : '';
333 2
            return $nsTitle . $page['page_title'];
334 2
        }, $topPages);
335
336 2
        return $this->getRepository()->getDisplayTitles($this->project, $pageTitles);
337
    }
338
}
339